diff --git a/MusicTypeChat.ServiceInterface/Data/Contact.cs b/MusicTypeChat.ServiceInterface/Data/Contact.cs deleted file mode 100644 index f330c96..0000000 --- a/MusicTypeChat.ServiceInterface/Data/Contact.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using MusicTypeChat.ServiceModel.Types; - -namespace MusicTypeChat.ServiceInterface.Data; - -/// -/// Example of using separate DataModel and DTO -/// -public class Contact -{ - public int Id { get; set; } - public int UserAuthId { get; set; } - public Title Title { get; set; } - public string? Name { get; set; } - public string? Color { get; set; } - public FilmGenre? FavoriteGenre { get; set; } - public int Age { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime ModifiedDate { get; set; } -} diff --git a/MusicTypeChat.ServiceInterface/MusicService.cs b/MusicTypeChat.ServiceInterface/MusicService.cs index 9d90a33..1660344 100644 --- a/MusicTypeChat.ServiceInterface/MusicService.cs +++ b/MusicTypeChat.ServiceInterface/MusicService.cs @@ -57,7 +57,7 @@ public async Task Post(ProcessSpotifyCommand request) private async Task ProcessStep(TypeChatStep step, T prog) where T : TypeChatProgramBase,new() { var func = step.Func; - var args = step.Args ?? new List(); + var args = step.Args ?? new(); var method = typeof(T).GetMethod(func); if (method == null) diff --git a/MusicTypeChat.ServiceInterface/MyServices.cs b/MusicTypeChat.ServiceInterface/MyServices.cs index 550edcb..7f4fbda 100644 --- a/MusicTypeChat.ServiceInterface/MyServices.cs +++ b/MusicTypeChat.ServiceInterface/MyServices.cs @@ -1,6 +1,5 @@ using ServiceStack; using MusicTypeChat.ServiceModel; -using MusicTypeChat.ServiceModel.Types; using ServiceStack.Gpt; using ServiceStack.OrmLite; using ServiceStack.Text; @@ -14,26 +13,6 @@ public object Any(Hello request) { return new HelloResponse { Result = $"Hello, {request.Name}!" }; } - - public async Task Any(AdminData request) - { - var tables = new (string Label, Type Type)[] - { - ("Bookings", typeof(Booking)), - ("Coupons", typeof(Coupon)), - }; - var dialect = Db.GetDialectProvider(); - var totalSql = tables.Map(x => $"SELECT '{x.Label}', COUNT(*) FROM {dialect.GetQuotedTableName(x.Type.GetModelMetadata())}") - .Join(" UNION "); - var results = await Db.DictionaryAsync(totalSql); - - return new AdminDataResponse { - PageStats = tables.Map(x => new PageStats { - Label = x.Label, - Total = results[x.Label], - }) - }; - } public ISpeechToText SpeechToText { get; set; } public IAutoQueryDb AutoQuery { get; set; } @@ -83,9 +62,6 @@ void WriteJsonFile(string path, string json) catch (Exception ignore) {} }); } - - - } diff --git a/MusicTypeChat.ServiceInterface/PromptProvider.cs b/MusicTypeChat.ServiceInterface/PromptProvider.cs index 8b5ddf0..99084c1 100644 --- a/MusicTypeChat.ServiceInterface/PromptProvider.cs +++ b/MusicTypeChat.ServiceInterface/PromptProvider.cs @@ -1,20 +1,14 @@ -using System.Diagnostics; -using MusicTypeChat.ServiceModel; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ChatCompletion; -using MusicTypeChat.ServiceModel; -using ServiceStack; +using ServiceStack; using ServiceStack.Gpt; using ServiceStack.Script; -using ServiceStack.Text; namespace MusicTypeChat.ServiceInterface; -public class MusicChatPromptProvider : IPromptProvider +public class MusicPromptProvider : IPromptProvider { public AppConfig Config { get; set; } - public MusicChatPromptProvider(AppConfig config) + public MusicPromptProvider(AppConfig config) { Config = config; } @@ -88,4 +82,4 @@ public class SiteConfig public string Bucket { get; set; } public string RecognizerId { get; set; } public string PhraseSetId { get; set; } -} \ No newline at end of file +} diff --git a/MusicTypeChat.ServiceInterface/TodosServices.cs b/MusicTypeChat.ServiceInterface/TodosServices.cs deleted file mode 100644 index 6a87e11..0000000 --- a/MusicTypeChat.ServiceInterface/TodosServices.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; -using MusicTypeChat.ServiceModel; -using ServiceStack; - -namespace MusicTypeChat.ServiceInterface; - -public class TodosServices : Service -{ - public IAutoQueryData AutoQuery { get; set; } - - static readonly PocoDataSource Todos = PocoDataSource.Create(new Todo[] - { - new () { Id = 1, Text = "Learn" }, - new () { Id = 2, Text = "Vue.mjs", IsFinished = true }, - new () { Id = 3, Text = "ServiceStack" }, - }, nextId: x => x.Select(e => e.Id).Max()); - - public object Get(QueryTodos query) - { - var db = Todos.ToDataSource(query, Request); - return AutoQuery.Execute(query, AutoQuery.CreateQuery(query, Request, db), db); - } - - public Todo Post(CreateTodo request) - { - var newTodo = new Todo { Id = Todos.NextId(), Text = request.Text }; - Todos.Add(newTodo); - return newTodo; - } - - public Todo Put(UpdateTodo request) - { - var todo = request.ConvertTo(); - Todos.TryUpdateById(todo, todo.Id); - return todo; - } - - public void Delete(DeleteTodos request) => Todos.TryDeleteByIds(request.Ids); -} diff --git a/MusicTypeChat.ServiceModel/Bookings.cs b/MusicTypeChat.ServiceModel/Bookings.cs deleted file mode 100644 index c5c15af..0000000 --- a/MusicTypeChat.ServiceModel/Bookings.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Complete declarative AutoQuery services for Bookings CRUD example: -// https://docs.servicestack.net/autoquery-crud-bookings - -using System; -using ServiceStack; -using ServiceStack.DataAnnotations; - -namespace MusicTypeChat.ServiceModel; - -[Icon(Svg = Icons.Booking)] -[Description("Booking Details")] -[Notes("Captures a Persons Name & Room Booking information")] -public class Booking : AuditBase -{ - [AutoIncrement] - public int Id { get; set; } - public string Name { get; set; } = default!; - public RoomType RoomType { get; set; } - public int RoomNumber { get; set; } - [IntlDateTime(DateStyle.Long)] - public DateTime BookingStartDate { get; set; } - [IntlRelativeTime] - public DateTime? BookingEndDate { get; set; } - [IntlNumber(Currency = NumberCurrency.USD)] - public decimal Cost { get; set; } - - [Ref(Model = nameof(Coupon), RefId = nameof(Coupon.Id), RefLabel = nameof(Coupon.Description))] - [References(typeof(Coupon))] - public string? CouponId { get; set; } - - [Reference] - public Coupon Discount { get; set; } - public string? Notes { get; set; } - public bool? Cancelled { get; set; } -} - -public enum RoomType -{ - Single, - Double, - Queen, - Twin, - Suite, -} - -[Tag("bookings"), Description("Find Bookings")] -[Notes("Find out how to quickly create a C# Bookings App from Scratch")] -[Route("/bookings", "GET")] -[Route("/bookings/{Id}", "GET")] -[AutoApply(Behavior.AuditQuery)] -public class QueryBookings : QueryDb -{ - public int? Id { get; set; } -} - -// Uncomment below to enable DeletedBookings API to view deleted bookings: -// [Route("/bookings/deleted")] -// [AutoFilter(QueryTerm.Ensure, nameof(AuditBase.DeletedDate), Template = SqlTemplate.IsNotNull)] -// public class DeletedBookings : QueryDb {} - -[Tag("bookings"), Description("Create a new Booking")] -[LocodeCss(Field="col-span-12 sm:col-span-6", Fieldset = "grid grid-cols-8 gap-2", Form = "border overflow-hidden max-w-screen-lg")] -[ExplorerCss(Field="col-span-12 sm:col-span-6", Fieldset = "grid grid-cols-6 gap-8", Form = "border border-indigo-500 overflow-hidden max-w-screen-lg")] -[Route("/bookings", "POST")] -[ValidateHasRole("Employee")] -[AutoApply(Behavior.AuditCreate)] -public class CreateBooking : ICreateDb, IReturn -{ - [Description("Name this Booking is for"), ValidateNotEmpty] - public string Name { get; set; } = default!; - public RoomType RoomType { get; set; } - [ValidateGreaterThan(0)] - public int RoomNumber { get; set; } - [ValidateGreaterThan(0)] - public decimal Cost { get; set; } - [Required] - public DateTime BookingStartDate { get; set; } - public DateTime? BookingEndDate { get; set; } - [Input(Type = "textarea")] - public string? Notes { get; set; } - public string? CouponId { get; set; } -} - -[Tag("bookings"), Description("Update an existing Booking")] -[Notes("Find out how to quickly create a C# Bookings App from Scratch")] -[Route("/booking/{Id}", "PATCH")] -[ValidateHasRole("Employee")] -[AutoApply(Behavior.AuditModify)] -public class UpdateBooking : IPatchDb, IReturn -{ - public int Id { get; set; } - public string? Name { get; set; } - public RoomType? RoomType { get; set; } - [ValidateGreaterThan(0)] - public int? RoomNumber { get; set; } - [ValidateGreaterThan(0)] - public decimal? Cost { get; set; } - public DateTime? BookingStartDate { get; set; } - public DateTime? BookingEndDate { get; set; } - [Input(Type = "textarea")] - public string? Notes { get; set; } - public string? CouponId { get; set; } - public bool? Cancelled { get; set; } -} - -[Tag("bookings"), Description("Delete a Booking")] -[Route("/booking/{Id}", "DELETE")] -[ValidateHasRole("Manager")] -[AutoApply(Behavior.AuditSoftDelete)] -public class DeleteBooking : IDeleteDb, IReturnVoid -{ - public int Id { get; set; } -} - - -[Description("Discount Coupons")] -[Icon(Svg = Icons.Coupon)] -public class Coupon -{ - public string Id { get; set; } = default!; - public string Description { get; set; } = default!; - public int Discount { get; set; } - public DateTime ExpiryDate { get; set; } -} - -[Tag("bookings"), Description("Find Coupons")] -[Route("/coupons", "GET")] -public class QueryCoupons : QueryDb -{ - public string Id { get; set; } -} - -[Tag("bookings")] -[Route("/coupons", "POST")] -[ValidateHasRole("Employee")] -public class CreateCoupon : ICreateDb, IReturn -{ - [ValidateNotEmpty] - public string Description { get; set; } = default!; - [ValidateGreaterThan(0)] - public int Discount { get; set; } - [ValidateNotNull] - public DateTime ExpiryDate { get; set; } -} - -[Tag("bookings")] -[Route("/coupons/{Id}", "PATCH")] -[ValidateHasRole("Employee")] -public class UpdateCoupon : IPatchDb, IReturn -{ - public string Id { get; set; } - [ValidateNotEmpty] - public string? Description { get; set; } - [ValidateNotNull, ValidateGreaterThan(0)] - public int? Discount { get; set; } - [ValidateNotNull] - public DateTime? ExpiryDate { get; set; } -} - -[Tag("bookings"), Description("Delete a Coupon")] -[Route("/coupons/{Id}", "DELETE")] -[ValidateHasRole("Manager")] -public class DeleteCoupon : IDeleteDb, IReturnVoid -{ - public string Id { get; set; } -} - -public class AdminData : IGet, IReturn {} - -public class PageStats -{ - public string Label { get; set; } - public int Total { get; set; } -} - -public class AdminDataResponse -{ - public List PageStats { get; set; } -} diff --git a/MusicTypeChat.ServiceModel/Gpt.cs b/MusicTypeChat.ServiceModel/Gpt.cs new file mode 100644 index 0000000..ddc9fd7 --- /dev/null +++ b/MusicTypeChat.ServiceModel/Gpt.cs @@ -0,0 +1,108 @@ +using ServiceStack; +using ServiceStack.AI; +using ServiceStack.DataAnnotations; + +namespace MusicTypeChat.ServiceModel; + +[Icon(Svg = Icons.Recording)] +public class Recording +{ + [AutoIncrement] + public int Id { get; set; } + public string Feature { get; set; } + public string Provider { get; set; } + public string Path { get; set; } + public string? Transcript { get; set; } + public float? TranscriptConfidence { get; set; } + public string? TranscriptResponse { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime? TranscribeStart { get; set; } + public DateTime? TranscribeEnd { get; set; } + public int? TranscribeDurationMs { get; set; } + public int? DurationMs { get; set; } + public string? IpAddress { get; set; } + public string? Error { get; set; } +} + +[Icon(Svg = Icons.Chat)] +public class Chat +{ + [AutoIncrement] + public int Id { get; set; } + public string Feature { get; set; } + public string Provider { get; set; } + public string Request { get; set; } + public string Prompt { get; set; } + public string Schema { get; set; } + public string? ChatResponse { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime? ChatStart { get; set; } + public DateTime? ChatEnd { get; set; } + public int? ChatDurationMs { get; set; } + public string? IpAddress { get; set; } + public string? Error { get; set; } +} + +[Tag(Tags.Gpt)] +[Route("/{Feature}/schema")] +public class GetSchema : IReturn +{ + [ValidateNotEmpty] + public string Feature { get; set; } +} + +[Tag(Tags.Gpt)] +[Route("/{Feature}/prompt")] +public class GetPrompt : IReturn +{ + [ValidateNotEmpty] + public string Feature { get; set; } + public string UserMessage { get; set; } +} + +[Tag(Tags.Gpt)] +[Route("/{Feature}/phrases")] +public class GetPhrases : IReturn +{ + [ValidateNotEmpty] + public string Feature { get; set; } +} + +[ValidateIsAdmin] +[Tag(Tags.Gpt)] +[Route("/{Feature}/speech/init")] +public class InitSpeech : IReturnVoid +{ + [ValidateNotEmpty] + public string Feature { get; set; } +} + +[Tag(Tags.Gpt)] +public class QueryRecordings : QueryDb {} + +[Tag(Tags.Gpt)] +[AutoPopulate(nameof(Recording.CreatedDate), Eval = "utcNow")] +[AutoPopulate(nameof(Recording.IpAddress), Eval = "Request.RemoteIp")] +public class CreateRecording : ICreateDb, IReturn +{ + [ValidateNotEmpty] + public string Feature { get; set; } + [Input(Type="file"), UploadTo("recordings")] + public string Path { get; set; } +} + +[Tag(Tags.Gpt)] +public class QueryChats : QueryDb +{ +} + +[Tag(Tags.Gpt)] +[AutoPopulate(nameof(Recording.CreatedDate), Eval = "utcNow")] +[AutoPopulate(nameof(Recording.IpAddress), Eval = "Request.RemoteIp")] +public class CreateChat : ICreateDb, IReturn +{ + [ValidateNotEmpty] + public string Feature { get; set; } + public string UserMessage { get; set; } + public TypeChatTranslator? Translator { get; set; } +} diff --git a/MusicTypeChat.ServiceModel/Music.cs b/MusicTypeChat.ServiceModel/Music.cs index f936441..5132b06 100644 --- a/MusicTypeChat.ServiceModel/Music.cs +++ b/MusicTypeChat.ServiceModel/Music.cs @@ -1,5 +1,4 @@ using System.Runtime.Serialization; -using MusicTypeChat.ServiceModel.Types; using ServiceStack; namespace MusicTypeChat.ServiceModel; diff --git a/MusicTypeChat.ServiceModel/Tags.cs b/MusicTypeChat.ServiceModel/Tags.cs new file mode 100644 index 0000000..591858e --- /dev/null +++ b/MusicTypeChat.ServiceModel/Tags.cs @@ -0,0 +1,7 @@ +namespace MusicTypeChat.ServiceModel; + +public static class Tags +{ + public const string Gpt = nameof(Gpt); + public const string Music = nameof(Music); +} diff --git a/MusicTypeChat.ServiceModel/Todos.cs b/MusicTypeChat.ServiceModel/Todos.cs deleted file mode 100644 index 2d3c938..0000000 --- a/MusicTypeChat.ServiceModel/Todos.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.Model; - -namespace MusicTypeChat.ServiceModel; - -[Tag("todos")] -[Route("/todos", "GET")] -public class QueryTodos : QueryData -{ - public int? Id { get; set; } - public List? Ids { get; set; } - public string? TextContains { get; set; } -} - -[Tag("todos")] -[Route("/todos", "POST")] -public class CreateTodo : IPost, IReturn -{ - [ValidateNotEmpty] - public string Text { get; set; } = default!; -} - -[Tag("todos")] -[Route("/todos/{Id}", "PUT")] -public class UpdateTodo : IPut, IReturn -{ - public long Id { get; set; } - [ValidateNotEmpty] - public string Text { get; set; } = default!; - public bool IsFinished { get; set; } -} - -[Tag("todos")] -[Route("/todos", "DELETE")] -public class DeleteTodos : IDelete, IReturnVoid -{ - public List Ids { get; set; } -} - -public class Todo : IHasId -{ - public long Id { get; set; } - public string Text { get; set; } = default!; - public bool IsFinished { get; set; } -} diff --git a/MusicTypeChat.ServiceModel/Types/Contact.cs b/MusicTypeChat.ServiceModel/Types/Contact.cs deleted file mode 100644 index 42bcec1..0000000 --- a/MusicTypeChat.ServiceModel/Types/Contact.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ServiceStack.DataAnnotations; - -namespace MusicTypeChat.ServiceModel.Types; - -public class Contact -{ - public int Id { get; set; } - public int UserAuthId { get; set; } - public Title Title { get; set; } - public string? Name { get; set; } - public string? Color { get; set; } - public FilmGenre? FavoriteGenre { get; set; } - public int Age { get; set; } -} - -public enum Title -{ - Unspecified=0, - [Description("Mr.")] Mr, - [Description("Mrs.")] Mrs, - [Description("Miss.")] Miss -} - -public enum FilmGenre -{ - Action, - Adventure, - Comedy, - Drama, -} diff --git a/MusicTypeChat.ServiceModel/Types/README.md b/MusicTypeChat.ServiceModel/Types/README.md deleted file mode 100644 index e3d08e8..0000000 --- a/MusicTypeChat.ServiceModel/Types/README.md +++ /dev/null @@ -1 +0,0 @@ -As part of our [Physical Project Structure](https://docs.servicestack.net/physical-project-structure) convention we recommend maintaining any shared non Request/Response DTOs in the `ServiceModel.Types` namespace. \ No newline at end of file diff --git a/MusicTypeChat.ServiceModel/Types/Recording.cs b/MusicTypeChat.ServiceModel/Types/Recording.cs deleted file mode 100644 index b821c75..0000000 --- a/MusicTypeChat.ServiceModel/Types/Recording.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ServiceStack; -using ServiceStack.DataAnnotations; - -namespace MusicTypeChat.ServiceModel.Types; - -[Icon(Svg = Icons.Recording)] -public class Recording -{ - [AutoIncrement] - public int Id { get; set; } - public string Path { get; set; } - public string? Transcript { get; set; } - public float? TranscriptConfidence { get; set; } - public string? TranscriptResponse { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime? TranscribeStart { get; set; } - public DateTime? TranscribeEnd { get; set; } - public int? TranscribeDurationMs { get; set; } - public int? DurationMs { get; set; } - public string? IpAddress { get; set; } - public string? Error { get; set; } -} \ No newline at end of file diff --git a/MusicTypeChat/Configure.Gpt.cs b/MusicTypeChat/Configure.Gpt.cs index fc46708..3fe3f52 100644 --- a/MusicTypeChat/Configure.Gpt.cs +++ b/MusicTypeChat/Configure.Gpt.cs @@ -14,7 +14,7 @@ public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddSingleton(c => - new MusicChatPromptProvider(c.Resolve())); + new MusicPromptProvider(c.Resolve())); // Call Open AI Chat API directly without going through node TypeChat var gptProvider = context.Configuration.GetValue("TypeChatProvider"); diff --git a/MusicTypeChat/Configure.Markdown.cs b/MusicTypeChat/Configure.Markdown.cs deleted file mode 100644 index f152ed5..0000000 --- a/MusicTypeChat/Configure.Markdown.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Globalization; -using System.Text; -using Markdig; -using Markdig.Syntax; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.AspNetCore.Mvc.ViewEngines; -using ServiceStack.IO; -using ServiceStack.Logging; -using ServiceStack.Text; - -[assembly: HostingStartup(typeof(MusicTypeChat.ConfigureMarkdown))] - -namespace MusicTypeChat; - -public class ConfigureMarkdown : IHostingStartup -{ - public void Configure(IWebHostBuilder builder) => builder - .ConfigureServices(services => - { - services.AddSingleton(); - services.AddSingleton(); - }) - .ConfigureAppHost( - appHost => appHost.Plugins.Add(new CleanUrlsFeature()), - afterPluginsLoaded: appHost => - { - var blogPosts = appHost.Resolve(); - blogPosts.VirtualFiles = appHost.GetVirtualFileSource(); - blogPosts.LoadPosts("_blog/posts"); - }); -} - -public class BlogPosts -{ - private readonly ILogger log; - private readonly RazorPagesEngine razorPages; - public BlogPosts(ILogger log, RazorPagesEngine razorPages) - { - this.log = log; - this.razorPages = razorPages; - } - public IVirtualFiles VirtualFiles { get; set; } = default!; - public List Posts { get; set; } = new(); - - public string PagesPath { get; set; } = "/Pages/Posts/Index.cshtml"; - public string PagePath { get; set; } = "/Pages/Posts/Post.cshtml"; - - public string FallbackProfileUrl { get; set; } = Svg.ToDataUri(Svg.Create(Svg.Body.User, stroke:"none").Replace("fill='currentColor'","fill='#0891b2'")); - public string FallbackSplashUrl { get; set; } = "https://source.unsplash.com/random/2000x1000/?stationary"; - - public Dictionary AuthorProfileUrls { get; set; } = new() - { - ["Lucy Bates"] = "/img/authors/author1.svg", - ["Gayle Smith"] = "/img/authors/author2.svg", - ["Brandon Foley"] = "/img/authors/author3.svg", - }; - - public string GetAuthorProfileUrl(string? name) => name != null && AuthorProfileUrls.TryGetValue(name, out var url) - ? url - : FallbackProfileUrl; - - public List GetPosts(string? author = null, string? tag = null) - { - IEnumerable latestPosts = Posts - .Where(x => x.Date < DateTime.UtcNow); - if (author != null) - latestPosts = latestPosts.Where(x => x.Author == author); - if (tag != null) - latestPosts = latestPosts.Where(x => x.Tags.Contains(tag)); - return latestPosts.OrderByDescending(x => x.Date).ToList(); - } - - public string GetPostLink(MarkdownFileInfo post) => $"/posts/{post.Slug}"; - - public string GetPostsLink() => "/posts"; - public string GetAuthorLink(string author) => GetPostsLink().AddQueryParam("author", author); - public string GetTagLink(string tag) => GetPostsLink().AddQueryParam("tag", tag); - public string GetDateLabel(DateTime? date) => X.Map(date ?? DateTime.UtcNow, d => d.ToString("MMMM d, yyyy"))!; - public string GetDateTimestamp(DateTime? date) => X.Map(date ?? DateTime.UtcNow, d => d.ToString("O"))!; - - public MarkdownFileInfo? FindPostBySlug(string name) => Posts.FirstOrDefault(x => x.Slug == name); - - public MarkdownFileInfo? Load(string path) => Load(path, CreatePipeline()); - - public MarkdownFileInfo? Load(string path, MarkdownPipeline pipeline) - { - var file = VirtualFiles.GetFile(path) - ?? throw new FileNotFoundException(path.LastRightPart('/')); - var content = file.ReadAllText(); - - var writer = new StringWriter(); - var renderer = new Markdig.Renderers.HtmlRenderer(writer); - pipeline.Setup(renderer); - - var document = Markdown.Parse(content, pipeline); - renderer.Render(document); - - var block = document - .Descendants() - .FirstOrDefault(); - - var doc = block? - .Lines // StringLineGroup[] - .Lines // StringLine[] - .Select(x => $"{x}\n") - .ToList() - .Select(x => x.Replace("---", string.Empty)) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => KeyValuePairs.Create(x.LeftPart(':').Trim(), x.RightPart(':').Trim())) - .ToObjectDictionary() - .ConvertTo(); - - if (doc?.Title == null) - { - log.LogWarning("No frontmatter found for {0}, ignoring...", file.VirtualPath); - return null; - } - - doc.Path = file.VirtualPath; - doc.Slug = file.Name.RightPart('_').LastLeftPart('.'); - doc.FileName = file.Name; - doc.HtmlFileName = $"{file.Name.RightPart('_').LastLeftPart('.')}.html"; - var datePart = file.Name.LeftPart('_'); - if (!DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, - DateTimeStyles.AdjustToUniversal, out var date)) - { - log.LogWarning("Could not parse date '{0}', ignoring...", datePart); - return null; - } - - doc.Date = date; - doc.Content = content; - doc.WordCount = WordCount(content); - doc.LineCount = LineCount(content); - writer.Flush(); - doc.Preview = writer.ToString(); - - var page = razorPages.GetView(PagePath); - var model = new Pages.Posts.PostModel(this) { Static = true }.Populate(doc); - doc.HtmlPage = RenderToHtml(page.View, model); - return doc; - } - - public MarkdownPipeline CreatePipeline() - { - var pipeline = new MarkdownPipelineBuilder() - .UseYamlFrontMatter() - .UseAdvancedExtensions() - .Build(); - return pipeline; - } - - public void LoadPosts(string fromDirectory) - { - Posts.Clear(); - var fs = VirtualFiles ?? throw new NullReferenceException($"{nameof(VirtualFiles)} is not populated"); - var files = fs.GetDirectory(fromDirectory).GetAllFiles().ToList(); - var log = LogManager.GetLogger(GetType()); - log.InfoFormat("Found {0} posts", files.Count); - - var pipeline = CreatePipeline(); - - foreach (var file in files) - { - try - { - var doc = Load(file.VirtualPath, pipeline); - if (doc == null) - continue; - - Posts.Add(doc); - } - catch (Exception e) - { - log.Error(e, "Couldn't load {0}: {1}", file.VirtualPath, e.Message); - } - } - } - - public void RenderToFile(IView? page, PageModel model, string renderTo) => - VirtualFiles.WriteFile(renderTo, RenderToHtml(page, model)); - public async Task RenderToFileAsync(IView? page, PageModel model, string renderTo, CancellationToken token=default) => - await VirtualFiles.WriteFileAsync(renderTo, await RenderToHtmlAsync(page, model, token), token); - - public string RenderToHtml(IView? page, PageModel model) - { - using var ms = MemoryStreamFactory.GetStream(); - razorPages.WriteHtmlAsync(ms, page, model).GetAwaiter().GetResult(); // No better way to run Async on Startup - ms.Position = 0; - var html = Encoding.UTF8.GetString(ms.ReadFullyAsMemory().Span); - return html; - } - - public async Task RenderToHtmlAsync(IView? page, PageModel model, CancellationToken token=default) - { - using var ms = MemoryStreamFactory.GetStream(); - await razorPages.WriteHtmlAsync(ms, page, model); - ms.Position = 0; - var html = Encoding.UTF8.GetString((await ms.ReadFullyAsMemoryAsync(token)).Span); - return html; - } - - public string GetSummarySplash(MarkdownFileInfo post) - { - var splash = post.Splash ?? FallbackSplashUrl; - return splash.StartsWith("https://images.unsplash.com") - ? splash.LeftPart('?') + "?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1679&q=80" - : splash; - } - - public int WordsPerMin { get; set; } = 225; - public char[] WordBoundaries { get; set; } = { ' ', '.', '?', '!', '(', ')', '[', ']' }; - public int WordCount(string str) => str.Split(WordBoundaries, StringSplitOptions.RemoveEmptyEntries).Length; - public int LineCount(string str) => str.CountOccurrencesOf('\n'); - public int MinutesRead(int? words) => (int)Math.Ceiling((words ?? 1) / (double)WordsPerMin); -} - -public class MarkdownFileInfo -{ - public string Path { get; set; } = default!; - public string? Slug { get; set; } - public string? FileName { get; set; } - public string? HtmlFileName { get; set; } - public string? Title { get; set; } - public string? Summary { get; set; } - public string? Splash { get; set; } - public string? Author { get; set; } - public List Tags { get; set; } = new(); - public DateTime? Date { get; set; } - public string? Content { get; set; } - public string? Preview { get; set; } - public string? HtmlPage { get; set; } - public int? WordCount { get; set; } - public int? LineCount { get; set; } -} diff --git a/MusicTypeChat/Migrations/Migration1000.cs b/MusicTypeChat/Migrations/Migration1000.cs index 32cb3c2..bbde7aa 100644 --- a/MusicTypeChat/Migrations/Migration1000.cs +++ b/MusicTypeChat/Migrations/Migration1000.cs @@ -1,82 +1,55 @@ -using System.Data; -using ServiceStack; -using ServiceStack.DataAnnotations; +using ServiceStack.DataAnnotations; using ServiceStack.OrmLite; namespace MusicTypeChat.Migrations; public class Migration1000 : MigrationBase { - public class Booking : AuditBase + public class Recording { [AutoIncrement] public int Id { get; set; } - public string Name { get; set; } = default!; - public RoomType RoomType { get; set; } - public int RoomNumber { get; set; } - public DateTime BookingStartDate { get; set; } - public DateTime? BookingEndDate { get; set; } - public decimal Cost { get; set; } - public string? Notes { get; set; } - public bool? Cancelled { get; set; } - - [References(typeof(Coupon))] - public string? CouponId { get; set; } + public string Feature { get; set; } + public string Provider { get; set; } + public string Path { get; set; } + public string? Transcript { get; set; } + public float? TranscriptConfidence { get; set; } + public string? TranscriptResponse { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime? TranscribeStart { get; set; } + public DateTime? TranscribeEnd { get; set; } + public int? TranscribeDurationMs { get; set; } + public int? DurationMs { get; set; } + public string? IpAddress { get; set; } + public string? Error { get; set; } } - public class Coupon + public class Chat { - public string Id { get; set; } = default!; - public string Description { get; set; } = default!; - public int Discount { get; set; } - public DateTime ExpiryDate { get; set; } - } - - public enum RoomType - { - Queen, - Double, - Suite, - } - + [AutoIncrement] + public int Id { get; set; } + public string Feature { get; set; } + public string Provider { get; set; } + public string Request { get; set; } + public string Prompt { get; set; } + public string Schema { get; set; } + public string? ChatResponse { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime? ChatStart { get; set; } + public DateTime? ChatEnd { get; set; } + public int? ChatDurationMs { get; set; } + public string? IpAddress { get; set; } + public string? Error { get; set; } + } public override void Up() { - Db.CreateTable(); - Db.CreateTable(); - - new[] { 5, 10, 15, 20, 25, 30, 40, 50, 60, 70, }.Each(percent => { - Db.Insert(new Coupon { - Id = $"BOOK{percent}", - Description = $"{percent}% off", - Discount = percent, - ExpiryDate = DateTime.UtcNow.AddDays(30) - }); - }); - - CreateBooking(Db, "First Booking!", RoomType.Queen, 10, 100, "BOOK10", "employee@email.com"); - CreateBooking(Db, "Booking 2", RoomType.Double, 12, 120, "BOOK25", "manager@email.com"); - CreateBooking(Db, "Booking the 3rd", RoomType.Suite, 13, 130, null, "employee@email.com"); + Db.CreateTable(); + Db.CreateTable(); } - - public void CreateBooking(IDbConnection? db, - string name, RoomType type, int roomNo, decimal cost, string? couponId, string by) => - db.Insert(new Booking { - Name = name, - RoomType = type, - RoomNumber = roomNo, - Cost = cost, - BookingStartDate = DateTime.UtcNow.AddDays(roomNo), - BookingEndDate = DateTime.UtcNow.AddDays(roomNo + 7), - CouponId = couponId, - CreatedBy = by, - CreatedDate = DateTime.UtcNow, - ModifiedBy = by, - ModifiedDate = DateTime.UtcNow, - }); public override void Down() { - Db.DropTable(); - Db.DropTable(); + Db.DropTable(); + Db.DropTable(); } -} +} \ No newline at end of file diff --git a/MusicTypeChat/Migrations/Migration1001.cs b/MusicTypeChat/Migrations/Migration1001.cs deleted file mode 100644 index baf97df..0000000 --- a/MusicTypeChat/Migrations/Migration1001.cs +++ /dev/null @@ -1,34 +0,0 @@ -using ServiceStack.DataAnnotations; -using ServiceStack.OrmLite; - -namespace MusicTypeChat.Migrations; - -public class Recording -{ - [AutoIncrement] - public int Id { get; set; } - public string Path { get; set; } - public string? Transcript { get; set; } - public float? TranscriptConfidence { get; set; } - public string? TranscriptResponse { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime? TranscribeStart { get; set; } - public DateTime? TranscribeEnd { get; set; } - public int? TranscribeDurationMs { get; set; } - public int? DurationMs { get; set; } - public string? IpAddress { get; set; } - public string? Error { get; set; } -} - -public class Migration1001 : MigrationBase -{ - public override void Up() - { - Db.CreateTable(); - } - - public override void Down() - { - Db.DropTable(); - } -} \ No newline at end of file diff --git a/MusicTypeChat/MusicTypeChat.csproj b/MusicTypeChat/MusicTypeChat.csproj index c93b4b5..8eeae70 100644 --- a/MusicTypeChat/MusicTypeChat.csproj +++ b/MusicTypeChat/MusicTypeChat.csproj @@ -50,4 +50,18 @@ + + + <_ContentIncludedByDefault Remove="Pages\Posts\Post.cshtml" /> + <_ContentIncludedByDefault Remove="Pages\Admin\Bookings.cshtml" /> + <_ContentIncludedByDefault Remove="Pages\Admin\Coupons.cshtml" /> + <_ContentIncludedByDefault Remove="Pages\Admin\Index.cshtml" /> + <_ContentIncludedByDefault Remove="Pages\Admin\_Layout.cshtml" /> + <_ContentIncludedByDefault Remove="wwwroot\img\authors\author1.svg" /> + <_ContentIncludedByDefault Remove="wwwroot\img\authors\author2.svg" /> + <_ContentIncludedByDefault Remove="wwwroot\img\authors\author3.svg" /> + + + + diff --git a/MusicTypeChat/Pages/Admin/Bookings.cshtml b/MusicTypeChat/Pages/Admin/Bookings.cshtml deleted file mode 100644 index e0ad72d..0000000 --- a/MusicTypeChat/Pages/Admin/Bookings.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@page -@{ - ViewData["Title"] = "Bookings"; -} - - diff --git a/MusicTypeChat/Pages/Admin/Coupons.cshtml b/MusicTypeChat/Pages/Admin/Coupons.cshtml deleted file mode 100644 index 44075fd..0000000 --- a/MusicTypeChat/Pages/Admin/Coupons.cshtml +++ /dev/null @@ -1,6 +0,0 @@ -@page -@{ - ViewData["Title"] = "Coupons"; -} - - diff --git a/MusicTypeChat/Pages/Admin/Index.cshtml b/MusicTypeChat/Pages/Admin/Index.cshtml deleted file mode 100644 index bf9668c..0000000 --- a/MusicTypeChat/Pages/Admin/Index.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@page -@{ - ViewData["Title"] = "Dashboard"; - var adminData = await Html.Gateway().SendAsync(new AdminData()); -} - -
-
- @foreach (var item in adminData.PageStats) - { - -
Total @item.Label
-
@item.Total
-
- } -
-
- -

- Go to Client Rendered Admin -

diff --git a/MusicTypeChat/Pages/Admin/_Layout.cshtml b/MusicTypeChat/Pages/Admin/_Layout.cshtml deleted file mode 100644 index de33826..0000000 --- a/MusicTypeChat/Pages/Admin/_Layout.cshtml +++ /dev/null @@ -1,137 +0,0 @@ -@inherits Microsoft.AspNetCore.Mvc.Razor.RazorPage - - -@{ - if (!await Html.EnsureRoleAsync("Admin")) - { - IgnoreBody(); - return; - } - - var user = await Html.GetRequest().GetSessionAsync(); - (string label, string href, string icon)[] sections = - { - ("Dashboard", "/admin/", Icons.Dashboard), - ("Bookings", "/admin/bookings", Icons.Booking), - ("Coupons", "/admin/coupons", Icons.Coupon), - }; - - var dev = HostContext.AppHost.IsDevelopmentEnvironment(); -} - - - - - @ViewData["Title"] - Admin - - - @if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) { - - } - @Html.ImportMap(new() { - ["vue"] = ("/lib/mjs/vue.mjs", "/lib/mjs/vue.min.mjs"), - ["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "/lib/mjs/servicestack-client.min.mjs"), - ["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "/lib/mjs/servicestack-vue.min.mjs"), - }) - - @if (dev) { - - } - - - - -
-
- -
- -
-
-
- -
- - - - -
-
-

@ViewData["Title"]

- @RenderBody() -
-
-
- - - - -@await RenderSectionAsync("Scripts", required: false) - - - \ No newline at end of file diff --git a/MusicTypeChat/Pages/Blog.cshtml b/MusicTypeChat/Pages/Blog.cshtml deleted file mode 100644 index 6c8596c..0000000 --- a/MusicTypeChat/Pages/Blog.cshtml +++ /dev/null @@ -1,61 +0,0 @@ -@page "/blog" -@model BlogModel -@inject BlogPosts BlogPosts -@{ - ViewData["Title"] = "Blog"; -} - -
-
-
-
-
-
-

From the blog

-

- Writing on Software Design, .NET and JavaScript. -

-
-
- @foreach (var post in BlogPosts.GetPosts(author:Model.Author,tag:Model.Tag)) - { -
-
- - - -
-
- -
- -
-

- @post.Author -

-
- - - @BlogPosts.MinutesRead(post.WordCount) min read -
-
-
-
-
- } -
-
-
diff --git a/MusicTypeChat/Pages/Blog.cshtml.cs b/MusicTypeChat/Pages/Blog.cshtml.cs deleted file mode 100644 index 4a4773f..0000000 --- a/MusicTypeChat/Pages/Blog.cshtml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace MusicTypeChat.Pages; - -public class BlogModel : PageModel -{ - [FromQuery] - public string? Author { get; set; } - [FromQuery] - public string? Tag { get; set; } -} \ No newline at end of file diff --git a/MusicTypeChat/Pages/BookingsAuto.cshtml b/MusicTypeChat/Pages/BookingsAuto.cshtml deleted file mode 100644 index c1c933f..0000000 --- a/MusicTypeChat/Pages/BookingsAuto.cshtml +++ /dev/null @@ -1,59 +0,0 @@ -@page "/bookings-auto" -@{ - if (!await Html.EnsureRoleAsync("Employee")) return; - ViewData["Title"] = "Bookings AutoQueryGrid"; -} - -
-

- @ViewData["Title"] -

- -
- - @await Html.PartialAsync("BookingsVideos") -
- - \ No newline at end of file diff --git a/MusicTypeChat/Pages/BookingsData.cshtml b/MusicTypeChat/Pages/BookingsData.cshtml deleted file mode 100644 index a458aae..0000000 --- a/MusicTypeChat/Pages/BookingsData.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@page "/bookings-data" -@{ - if (!await Html.EnsureRoleAsync("Employee")) return; - ViewData["Title"] = "Bookings DataGrid"; -} - -
-

- @ViewData["Title"] -

-
- - @await Html.PartialAsync("BookingsVideos") -
- - - diff --git a/MusicTypeChat/Pages/Index.cshtml b/MusicTypeChat/Pages/Index.cshtml index 345ee89..acdb5f0 100644 --- a/MusicTypeChat/Pages/Index.cshtml +++ b/MusicTypeChat/Pages/Index.cshtml @@ -1,165 +1,24 @@ @page @model IndexModel -@inject BlogPosts BlogPosts @{ ViewData["Title"] = "Home page"; }
-

Vue.mjs

-
-

A Modern .NET + JS App with - JS modules, - Vue.js, - htmx & Markdown -

-
+

Music TypeChat

-
- @Html.SrcComponent("MusicAudio.mjs", size:"w-8 h-8 mt-2") +
-
-@{ - var posts = BlogPosts.GetPosts().Take(3).ToList(); - var post = posts.FirstOrDefault(); - if (post != null) - { -
-
-
- - Cover Image for @post.Title - -
-
-
-
-

- @post.Title -

-
- -
-
-
-

@post.Summary

-
- Author -
@post.Author
-
-
-
-
- } - - void RenderPost(MarkdownFileInfo post) - { -
-
-
- - Cover Image for @post.Title - -
-
-

- @post.Title -

-
- -
-

@post.Summary

-
- @post.Author -
@post.Author
-
-
- } - - var morePosts = posts.Skip(1).ToList(); - if (morePosts.Count == 2) - { -
-

More Stories

-
- @{ RenderPost(morePosts[0]); } - @{ RenderPost(morePosts[1]); } -
-
- } -} -
- -
-

- Getting Started -

-
-
-
-
-
- @Html.SrcComponent("GettingStarted.mjs") -
-
-
- -
-
-
-

- Simple, Modern JavaScript Development -

-

- Leverage Modern Browser features for Simple, Fast App development -

-
- -
    - @{ - void NavListItem(string title, string childContent) - { -
  • -
    -

    - - - @title - - -

    -

    - @Html.Raw(childContent) -

    -
    -
  • - } - NavListItem("Simple", "No node_modules, no build tools, no client side routing, no heavy client state"); - NavListItem("Fast", $"Initial SSR HTML & JSON for fast First Contentful Paint (FCP)"); - NavListItem("Lightweight", "Only load JS each page needs, built-in optimizer for optimal dev & prod bundles"); - NavListItem("Modern", "Use latest Vue3, htmx or any preferred 3rd Party libraries"); - NavListItem("Rapid Development", "View code & style changes instantly on Save with dotnet watch"); - NavListItem("Maintainable", "Logically structure & encapsulate components using native JavaScript Modules"); - NavListItem("Type Safety", "Use JSDoc and Library TypeScript definitions for IDE static analysis"); - NavListItem("SEO Friendly", "Use Server Side Rendering for SEO-friendly Markdown and HTML content"); - NavListItem("Tailwind", "Productive, maintainable, responsive-first styling"); - } -
-
-
- diff --git a/MusicTypeChat/Pages/Posts/Post.cshtml b/MusicTypeChat/Pages/Posts/Post.cshtml deleted file mode 100644 index 92a31b9..0000000 --- a/MusicTypeChat/Pages/Posts/Post.cshtml +++ /dev/null @@ -1,67 +0,0 @@ -@page "/posts/{Slug}" -@model PostModel -@inject BlogPosts BlogPosts - -@{ - ViewData["Title"] = Model.Title; -} - - - -
- @if (!string.IsNullOrEmpty(Model.Title)) - { -
-

- @Model.Title -

- -
-
- @Model.Title Background -
-
-
- @foreach (var tag in Model.Tags) - { - @tag - } -
- @if (Model.Date != null) - { -
-
- - - @BlogPosts.MinutesRead(Model.WordCount) min read -
-
- } -
-
- @Html.Raw(Model.HtmlContent) -
-
-
- } - else - { -

- Post was not found -

- } -
- - - - - - diff --git a/MusicTypeChat/Pages/Posts/Post.cshtml.cs b/MusicTypeChat/Pages/Posts/Post.cshtml.cs deleted file mode 100644 index 1dca89f..0000000 --- a/MusicTypeChat/Pages/Posts/Post.cshtml.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace MusicTypeChat.Pages.Posts; - -public class PostModel : PageModel -{ - public BlogPosts BlogPosts { get; } - public PostModel(BlogPosts blogPosts) => BlogPosts = blogPosts; - - [FromQuery] - public bool Static { get; set; } - [FromRoute] - public string? Slug { get; set; } - public string? Title { get; set; } - public string? Author { get; set; } - public string? Splash { get; set; } - public string? AuthorProfileUrl { get; set; } - public List Tags { get; set; } = new(); - public DateTime? Date { get; set; } - public int? WordCount { get; set; } - public string? HtmlContent { get; set; } - - public void OnGet() - { - if (string.IsNullOrEmpty(Title) && Slug != null) - { - var doc = BlogPosts.FindPostBySlug(Slug); - if (doc != null && HostContext.AppHost.IsDevelopmentEnvironment()) - doc = BlogPosts.Load(doc.Path); - if (doc != null) - { - Populate(doc); - } - } - } - - public PostModel Populate(MarkdownFileInfo doc) - { - Title = doc.Title; - Tags = doc.Tags; - Author = doc.Author; - AuthorProfileUrl = BlogPosts.GetAuthorProfileUrl(doc.Author); - Splash = doc.Splash ?? BlogPosts.FallbackSplashUrl; - Date = doc.Date; - HtmlContent = doc.Preview; - WordCount = doc.WordCount; - return this; - } -} diff --git a/MusicTypeChat/Pages/Privacy.cshtml b/MusicTypeChat/Pages/Privacy.cshtml deleted file mode 100644 index a92998a..0000000 --- a/MusicTypeChat/Pages/Privacy.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@page -@model PrivacyModel -@{ - ViewData["Title"] = "Privacy Policy"; -} -

@ViewData["Title"]

- -

Use this page to detail your site's privacy policy.

\ No newline at end of file diff --git a/MusicTypeChat/Pages/Privacy.cshtml.cs b/MusicTypeChat/Pages/Privacy.cshtml.cs deleted file mode 100644 index 909cfb4..0000000 --- a/MusicTypeChat/Pages/Privacy.cshtml.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace MusicTypeChat.Pages; - -public class PrivacyModel : PageModel -{ - private readonly ILogger _logger; - - public PrivacyModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - } -} \ No newline at end of file diff --git a/MusicTypeChat/Pages/Profile.cshtml b/MusicTypeChat/Pages/Profile.cshtml deleted file mode 100644 index a31aef3..0000000 --- a/MusicTypeChat/Pages/Profile.cshtml +++ /dev/null @@ -1,24 +0,0 @@ -@page -@{ - ViewBag.Title = "Profile"; -} - -@if (!await Html.EnsureAuthenticatedAsync()) { return; } -@{ - var user = await GetSessionAsync(); -} - -
- -
@user.DisplayName
-
@(UserSession.UserName ?? UserSession.Email)
-
- @foreach (var role in await user.GetRolesAsync(AuthRepositoryAsync)) - { - - @role - - } -
- Sign Out -
diff --git a/MusicTypeChat/Pages/Shared/BookingsVideos.cshtml b/MusicTypeChat/Pages/Shared/BookingsVideos.cshtml deleted file mode 100644 index 68890ae..0000000 --- a/MusicTypeChat/Pages/Shared/BookingsVideos.cshtml +++ /dev/null @@ -1,50 +0,0 @@ -
- -

- Manage Bookings in - Admin, - Client Admin, - Locode or - API Explorer -

- -
-

- Instantly Manage your data using AutoQueryGrid Vue -

-

- This walkthrough explores the ServiceStack Vue 3 library and the functionality of the AutoQueryGrid component. - The AutoQueryGrid component simplifies the integration of AutoQuery services by generating a customizable UI. -

- -
- -
-

- Create a multi-user Booking system in minutes -

-

- The Bookings APIs are built using - AutoQuery CRUD, - allowing for rapid development of typed CRUD Services using only declarative POCO DTOs, enabling - developing entire - audited - & verifiable - data-driven systems in mins - more... -

- -
- -
-

- Vue 3 Tailwind Components Library -

-

- In this video, we demonstrate using the @@servicestack/vue components library for Vue.js 3 - Multipage Apps (MPAs) along with the vue-mjs template. -

- -
- -
\ No newline at end of file diff --git a/MusicTypeChat/Pages/TodoMvc.cshtml b/MusicTypeChat/Pages/TodoMvc.cshtml deleted file mode 100644 index db19f8e..0000000 --- a/MusicTypeChat/Pages/TodoMvc.cshtml +++ /dev/null @@ -1,83 +0,0 @@ -@page -@{ - ViewData["Title"] = "Todo MVC"; -} - -
-
-

- @ViewData["Title"] -

-
-
-
- @Html.SrcPage("TodoMvc.mjs") -
-

- Manage Todos in API Explorer -

-
- - - - - diff --git a/MusicTypeChat/wwwroot/Pages/Bookings.mjs b/MusicTypeChat/wwwroot/Pages/Bookings.mjs deleted file mode 100644 index 44e9bad..0000000 --- a/MusicTypeChat/wwwroot/Pages/Bookings.mjs +++ /dev/null @@ -1,96 +0,0 @@ -import { computed, ref, watch } from "vue" -import { useClient, useAuth, useFormatters } from "@servicestack/vue" -import { QueryBookings } from "../../mjs/dtos.mjs" - -export default { - template:/*html*/` -
- - - - - New Booking - - - - - - - - - - - - - -
-
- - -
- -
-
`, - props: { bookings:Array }, - setup(props) { - const create = ref(false) - const editId = ref() - const edit = ref() - const expandAbout = ref(false) - const bookings = ref(props.bookings || []) - - const client = useClient() - const { currency } = useFormatters() - const { hasRole } = useAuth() - const canDelete = computed(() => hasRole('Manager')) - - async function refresh() { - const api = await client.api(new QueryBookings()) - if (api.succeeded) { - bookings.value = api.response.results || [] - } - } - - /** @param {{ create?: boolean, editId?:number }} [args] */ - function reset(args={}) { - create.value = args.create ?? false - editId.value = args.editId ?? undefined - } - - function done() { - refresh() - reset() - } - - watch(editId, async () => { - if (editId.value) { - const api = await client.api(new QueryBookings({ id: editId.value })) - if (api.succeeded) { - edit.value = api.response.results[0] - return - } - } - edit.value = null - }) - - return { create, editId, edit, canDelete, expandAbout, bookings, reset, done, currency } - } -} diff --git a/MusicTypeChat/wwwroot/Pages/TodoMvc.mjs b/MusicTypeChat/wwwroot/Pages/TodoMvc.mjs deleted file mode 100644 index 9d28e4d..0000000 --- a/MusicTypeChat/wwwroot/Pages/TodoMvc.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import { reactive } from 'vue' -import { $1 } from "@servicestack/client" -import { client } from "../mjs/app.mjs" -import { Todo, QueryTodos, CreateTodo, UpdateTodo, DeleteTodos } from "../mjs/dtos.mjs" - -let store = { - /** @type {Todo[]} */ - todos: [], - newTodo:'', - filter: 'all', - error:null, - finishedTodos() { return this.todos.filter(x => x.isFinished) }, - unfinishedTodos() { return this.todos.filter(x => !x.isFinished) }, - filteredTodos() { - return this.filter === 'finished' - ? this.finishedTodos() - : this.filter === 'unfinished' - ? this.unfinishedTodos() - : this.todos - }, - async refreshTodos(errorStatus) { - this.error = errorStatus - let api = await client.api(new QueryTodos()) - if (api.succeeded) { - this.todos = api.response.results - } - }, - async addTodo() { - this.todos.push(new Todo({ text:this.newTodo })) - let api = await client.api(new CreateTodo({ text:this.newTodo })) - if (api.succeeded) - this.newTodo = '' - return this.refreshTodos(api.error) - }, - async removeTodo(id) { - this.todos = this.todos.filter(x => x.id !== id) - let api = await client.api(new DeleteTodos({ ids:[id] })) - await this.refreshTodos(api.error) - }, - async removeFinishedTodos() { - let ids = this.todos.filter(x => x.isFinished).map(x => x.id) - if (ids.length === 0) return - this.todos = this.todos.filter(x => !x.isFinished) - let api = await client.api(new DeleteTodos({ ids })) - await this.refreshTodos(api.error) - }, - async toggleTodo(id) { - const todo = this.todos.find(x => x.id === id) - todo.isFinished = !todo.isFinished - let api = await client.api(new UpdateTodo(todo)) - await this.refreshTodos(api.error) - }, - changeFilter(filter) { - this.filter = filter - } -} -store = reactive(store) - -const FilterTab = { - template:/*html*/``, - props:{ filter:String }, - setup(props) { - return { store } - }, -} - -export default { - components: { FilterTab }, - template: $1('#TodoMvc-template'), - props: { todos: Array }, - setup(props) { - store.todos = props.todos || [] - return { - store, - } - } -} diff --git a/MusicTypeChat/wwwroot/_blog/posts/2022-12-31_typography.md b/MusicTypeChat/wwwroot/_blog/posts/2022-12-31_typography.md deleted file mode 100644 index 5351b57..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2022-12-31_typography.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Tailwind Typography -summary: tailwindcss/typography enabled for Markdown pages -tags: tailwind,markdown -splash: https://images.unsplash.com/photo-1497250681960-ef046c08a56e?crop=entropy&fit=crop&h=1000&w=2000 -author: Jesse O'Reilly ---- - -

- Until now, trying to style an article, document, or blog post with Tailwind has been a tedious - task that required a keen eye for typography and a lot of complex custom CSS. -

- -By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. - -We get lots of complaints about it actually, with people regularly asking us things like: - -> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? - -We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either — you want them to look _awesome_, not awful. - -The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. - -It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: - -```html -
-

Garlic bread with cheese: What the science tells us

-

- For years parents have espoused the health benefits of eating garlic bread with cheese to their - children, with the food earning such an iconic status in our culture that kids will often dress - up as warm, cheesy loaf for Halloween. -

-

- But a recent study shows that the celebrated appetizer may be linked to a series of rabies cases - springing up around the country. -

- -
-``` - -For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). - ---- - -## What to expect from here on out - -What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. - -It's important to cover all of these use cases for a few reasons: - -1. We want everything to look good out of the box. -2. Really just the first reason, that's the whole point of the plugin. -3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. - -Now we're going to try out another header style. - -### Typography should be easy - -So that's a header for you — with any luck if we've done our job correctly that will look pretty reasonable. - -Something a wise person once told me about typography is: - -> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. - -It's probably important that images look okay here by default as well: - -
- -
- Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of - classical Latin literature from 45 BC, making it over 2000 years old. -
-
- -Now I'm going to show you an example of an unordered list to make sure that looks good, too: - -- So here is the first item in this list. -- In this example we're keeping the items short. -- Later, we'll use longer, more complex list items. - -And that's the end of this section. diff --git a/MusicTypeChat/wwwroot/_blog/posts/2023-01-01_deploy.md b/MusicTypeChat/wwwroot/_blog/posts/2023-01-01_deploy.md deleted file mode 100644 index 8e322a6..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2023-01-01_deploy.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Deployment with GitHub Actions -summary: Configuring your GitHub repo for SSH and CDN deployments -tags: github-actions,devops -splash: https://images.unsplash.com/photo-1485841890310-6a055c88698a?crop=entropy&fit=crop&h=1000&w=2000 -author: Gayle Smith ---- - -# ServiceStack GitHub Action Deployments - -The [release.yml](https://github.com/NetCoreTemplates/razor-tailwind/blob/main/.github/workflows/release.yml) -in this template enables GitHub Actions CI deployment to a dedicated server with SSH access. - -## Overview -`release.yml` is designed to work with a ServiceStack app deploying directly to a single server via SSH. A docker image is built and stored on GitHub's `ghcr.io` docker registry when a GitHub Release is created. - -GitHub Actions specified in `release.yml` then copy files remotely via scp and use `docker-compose` to run the app remotely via SSH. - -## What's the process of `release.yml`? - -![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ghr-vanilla-diagram.png) - -## Deployment server setup - -To get this working, a server needs to be setup with the following: - -- SSH access -- docker -- docker-compose -- ports 443 and 80 for web access of your hosted application - -This can be your own server or any cloud hosted server like Digital Ocean, AWS, Azure etc. We use [Hetzner Cloud](http://cloud.hetzner.com/) -to deploy all ServiceStack's [GitHub Project Templates]( https://github.com/NetCoreTemplates/) as it was the -[best value US cloud provider](https://servicestack.net/blog/finding-best-us-value-cloud-provider) we've found. - -When setting up your server, you'll want to use a dedicated SSH key for access to be used by GitHub Actions. GitHub Actions will need the *private* SSH key within a GitHub Secret to authenticate. This can be done via ssh-keygen and copying the public key to the authorized clients on the server. - -To let your server handle multiple ServiceStack applications and automate the generation and management of TLS certificates, an additional docker-compose file is provided in this template, `nginx-proxy-compose.yml`. This docker-compose file is ready to run and can be copied to the deployment server. - -For example, once copied to remote `~/nginx-proxy-compose.yml`, the following command can be run on the remote server. - -``` -docker-compose -f ~/nginx-proxy-compose.yml up -d -``` - -This will run an nginx reverse proxy along with a companion container that will watch for additional containers in the same docker network and attempt to initialize them with valid TLS certificates. - -### GitHub Actions secrets - -The `release.yml` uses the following secrets. - -| Required Secrets | Description | -| -- | -- | -| `DEPLOY_HOST` | Hostname used to SSH deploy .NET App to, this can either be an IP address or subdomain with A record pointing to the server | -| `DEPLOY_USERNAME` | Username to log in with via SSH e.g, **ubuntu**, **ec2-user**, **root** | -| `DEPLOY_KEY` | SSH private key used to remotely access deploy .NET App | -| `LETSENCRYPT_EMAIL` | Email required for Let's Encrypt automated TLS certificates | - -These secrets can use the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) for ease of creation. Eg, using the GitHub CLI the following can be set. - -```bash -gh secret set DEPLOY_HOST -b"" -gh secret set DEPLOY_USERNAME -b"" -gh secret set DEPLOY_KEY < key.pem # DEPLOY_KEY -gh secret set LETSENCRYPT_EMAIL -b"" -``` - -These secrets are used to populate variables within GitHub Actions and other configuration files. - -## Deployments - -A published version of your .NET App created with the standard dotnet publish tool: - -```yaml -dotnet publish -c Release -``` - -is used to build a production build of your .NET App inside the standard `Dockerfile` for dockerizing .NET Applications. - -Additional custom deployment tasks can be added to your project's package.json **postinstall** script which also gets run at deployment. - -If preferred additional MS Build tasks can be run by passing in custom parameters in the publish command, e.g: - -```yaml -dotnet publish -c Release /p:APP_TASKS=prerender -``` - -Which your `MusicTypeChat.csproj` can detect with a target that checks for it: - -```xml - - - - - - - -``` - -## Pushing updates and rollbacks - -By default, deployments occur on commit to your main branch. A new Docker image for your ServiceStack API is produced, pushed to GHCR.io and hosted on your Linux server with Docker Compose. - -The template also will run the release process on the creation of a GitHub Release making it easier to switch to manual production releases. - -Additionally, the `release.yml` workflow can be run manually specifying a version. This enables production rollbacks based on previously tagged releases. -A release must have already been created for the rollback build to work, it doesn't create a new Docker build based on previous code state, only redeploys as existing Docker image. diff --git a/MusicTypeChat/wwwroot/_blog/posts/2023-01-10_vs.md b/MusicTypeChat/wwwroot/_blog/posts/2023-01-10_vs.md deleted file mode 100644 index a0185ae..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2023-01-10_vs.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Develop using Visual Studio -summary: Exploring development workflow in VS Code and Visual Studio .NET -tags: c#,dev -splash: https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000 -author: Lucy Bates ---- - -A popular alternative development environment to our preferred [JetBrains Rider](rider) IDE is to use -Visual Studio, the primary issue with this is that VS Code is a better IDE with richer support for JavaScript and npm -projects whilst Visual Studio is a better IDE for C# Projects. - -Essentially this is why we recommend Rider where it's best at both, where both C# and JS/TypeScript projects can -be developed from within the same solution. - -### Developing with just VS Code - - - - - -If you prefer the dev UX of a lightweight text editor or your C# project isn't large, than VS Code on its own -can provide a great development UX which is also what [Vue recommends themselves](https://v3.vuejs.org/api/sfc-tooling.html#ide-support), -to be used together with the [Volar extension](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar). - -VSCode's [Integrated Terminal](https://code.visualstudio.com/docs/editor/integrated-terminal) has great multi-terminal -support you can toggle between the editor and terminal with `Ctrl+` or open a new Terminal Window with -Ctrl+Shift+` to run Tailwind with: - -```bash -$ npm run ui:dev -``` - -Then in a new Terminal Window, start a new watched .NET App with: - -```bash -$ dotnet watch -``` - -With both projects started you can open a browser tab running at `https://localhost:5001` where it -will automatically reload itself at every `Ctrl+S` save point. - -#### Useful VS Code extensions - -We recommend these extensions below to enhance the development experience of this template: - - - [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) - Add Intellisense for Tailwind classes - - [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) - Add HTML Syntax Highlighting in string literals - -### Using Visual Studio - - - - - -As your C# project grows you'll want to consider running the back-end C# Solution with Visual Studio .NET with its -much improved intelli-sense, navigation, tests runner & debug capabilities. - -As we've never had a satisfactory experience trying develop npm or JS/TypeScript projects with VS.NET, we'd recommend only -using VS.NET for C# and Razor and continuing to use VSCode for everything else. - -If you'd prefer to use Visual Studio for front-end development we recommend moving all JS to external files for a better -Dev UX, e.g: - -```html - -``` - -### Deploying to Production - -This template also includes the necessary GitHub Actions to deploy this Apps production static assets to GitHub Pages CDN, -for more info, checkout [GitHub Actions Deployments](deploy). - -### Get Started - -If you're new to Vue 3 a good place to start is [Vue 3 Composition API](https://vuejs.org/api/composition-api-setup.html). diff --git a/MusicTypeChat/wwwroot/_blog/posts/2023-01-11_prerendering.md b/MusicTypeChat/wwwroot/_blog/posts/2023-01-11_prerendering.md deleted file mode 100644 index 1478d3a..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2023-01-11_prerendering.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: Prerendering Razor Pages -summary: Improving Blog Performance with Prerendering -tags: c#,dev,markdown -splash: https://images.unsplash.com/photo-1522526886914-6e8d4fd91399?crop=entropy&fit=crop&h=1000&w=2000 -author: Lucy Bates ---- - -Prerendering static content is a popular technique used by [JAMStack](https://jamstack.org) Apps to improve the -performance, reliability and scalability of Web Apps that's able to save unnecessary computation at runtime by -generating static content at deployment which can be optionally hosted from a CDN for even greater performance. - -As such we thought it a valuable technique to include in this template to show how it can be easily achieved -within a Razor Pages Application. Since prerendered content is only updated at deployment, it's primarily only -useful for static content like this Blog which is powered by the static markdown content in -[_blog/posts](https://github.com/NetCoreTemplates/vue-mjs/tree/prerender/MusicTypeChat/wwwroot/_blog/posts) whose content -is prerendered to: - - - [/blog](/blog) - -### Parsing Markdown Files - -All the functionality to load and render Markdown files is maintained in -[Configure.Markdown.cs](https://github.com/NetCoreTemplates/vue-mjs/blob/prerender/MusicTypeChat/Configure.Markdown.cs), -most of which is spent populating the POCO below from the content and frontmatter of each Markdown file: - -```csharp -public class MarkdownFileInfo -{ - public string Path { get; set; } = default!; - public string? Slug { get; set; } - public string? FileName { get; set; } - public string? HtmlFileName { get; set; } - public string? Title { get; set; } - public string? Summary { get; set; } - public string? Splash { get; set; } - public string? Author { get; set; } - public List Tags { get; set; } = new(); - public DateTime? Date { get; set; } - public string? Content { get; set; } - public string? Preview { get; set; } - public string? HtmlPage { get; set; } - public int? WordCount { get; set; } - public int? LineCount { get; set; } -} -``` - -Which uses the popular [Markdig](https://github.com/xoofx/markdig) library to parse the frontmatter into a -Dictionary that it populates the POCO with using the built-in [Automapping](https://docs.servicestack.net/auto-mapping): - -```csharp -var content = VirtualFiles.GetFile(path).ReadAllText(); -var document = Markdown.Parse(content, pipeline); -var block = document - .Descendants() - .FirstOrDefault(); -var doc = block? - .Lines // StringLineGroup[] - .Lines // StringLine[] - .Select(x => $"{x}\n") - .ToList() - .Select(x => x.Replace("---", string.Empty)) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => KeyValuePairs.Create(x.LeftPart(':').Trim(), x.RightPart(':').Trim())) - .ToObjectDictionary() - .ConvertTo(); -``` - -Since this is a [Jekyll inspired blog](https://jekyllrb.com/docs/step-by-step/08-blogging/) it derives the **date** and **slug** for each -post from its file name which has the nice property of maintaining markdown blog posts in chronological order: - -```csharp -doc.Slug = file.Name.RightPart('_').LastLeftPart('.'); -doc.HtmlFileName = $"{file.Name.RightPart('_').LastLeftPart('.')}.html"; - -var datePart = file.Name.LeftPart('_'); -if (DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, - DateTimeStyles.AdjustToUniversal, out var date)) -{ - doc.Date = date; -} -``` - -The rendering itself is done using Markdig's `HtmlRenderer` which renders the Markdown content into a HTML fragment: - -```csharp -var pipeline = new MarkdownPipelineBuilder() - .UseYamlFrontMatter() - .UseAdvancedExtensions() - .Build(); -var writer = new StringWriter(); -var renderer = new Markdig.Renderers.HtmlRenderer(writer); -pipeline.Setup(renderer); -//... - -renderer.Render(document); -writer.Flush(); -doc.Preview = writer.ToString(); -``` - -At this point we've populated Markdown Blog Posts into a POCO which is the data source used to implement all the blog's functionality. - -We can now start prerendering entire HTML Pages by rendering the markdown inside the -[Post.cshtml](https://github.com/NetCoreTemplates/vue-mjs/blob/prerender/MusicTypeChat/Pages/Posts/Post.cshtml) Razor Page by populating its PageModel -from the `MarkdownFileInfo` POCO. It also sets a `Static` flag that tells the Razor Page that this page is being statically rendered so -it can render the appropriate links. - -```csharp -var page = razorPages.GetView("/Pages/Posts/Post.cshtml"); -var model = new Pages.Posts.PostModel(this) { Static = true }.Populate(doc); -doc.HtmlPage = RenderToHtml(page.View, model); - -public string RenderToHtml(IView? page, PageModel model) -{ - using var ms = MemoryStreamFactory.GetStream(); - razorPages.WriteHtmlAsync(ms, page, model).GetAwaiter().GetResult(); - ms.Position = 0; - var html = Encoding.UTF8.GetString(ms.ReadFullyAsMemory().Span); - return html; -} -``` - -The use of `GetResult()` on an async method isn't ideal, but something we have to live with until there's a better way -to run async code on Startup. - -The actual rendering of the Razor Page is done with ServiceStack's `RazorPagesEngine` feature which sets up the necessary -Http, View and Page contexts to render Razor Pages, registered in ASP.NET Core's IOC at: - -```csharp -.ConfigureServices(services => { - services.AddSingleton(); -}) -``` - -The process of saving the prerendered content is then simply a matter of saving the rendered Razor Page at the preferred locations, -done for each post and the [/blog](/blog) index page using the -[Posts/Index.cshtml](https://github.com/NetCoreTemplates/vue-mjs/blob/prerender/MusicTypeChat/Pages/Posts/Index.cshtml) Razor Page: - -```csharp -foreach (var file in files) -{ - // prerender /blog/{slug}.html - if (renderTo != null) - { - log.InfoFormat("Writing {0}/{1}...", renderTo, doc.HtmlFileName); - fs.WriteFile($"{renderTo}/{doc.HtmlFileName}", doc.HtmlPage); - } -} - -// prerender /blog/index.html -if (renderTo != null) -{ - log.InfoFormat("Writing {0}/index.html...", renderTo); - RenderToFile(razorPages.GetView("/Pages/Posts/Index.cshtml").View, - new Pages.Posts.IndexModel { Static = true }, $"{renderTo}/index.html"); -} -``` - -### Prerendering Pages Task - -Next we need to come up with a solution to run this from the command-line. -[App Tasks](https://docs.servicestack.net/app-tasks) is ideal for this which lets you run one-off tasks within the full context of your App -but without the overhead of maintaining a separate .exe with duplicated App configuration & logic, instead we can run the .NET App to -run the specified Tasks then exit before launching its HTTP Server. - -To do this we'll register this task with the **prerender** AppTask name: - -```csharp -AppTasks.Register("prerender", args => blogPosts.LoadPosts("_blog/posts", renderTo: "blog")); -``` - -Which we can run now from the command-line with: - -```bash -$ dotnet run --AppTasks=prerender -``` - -To make it more discoverable, this is also registered as an npm script in `package.json`: - -```json -{ - "scripts": { - "prerender": "dotnet run --AppTasks=prerender" - } -} -``` - -That can now be run to prerender this blog to `/wwwroot/blog` with: - -```bash -$ npm run prerender -``` - -### Prerendering at Deployment - -To ensure this is always run at deployment it's also added as an MS Build task in -[MusicTypeChat.csproj](https://github.com/NetCoreTemplates/vue-mjs/blob/prerender/MusicTypeChat/MusicTypeChat.csproj): - -```xml - - - - - - - -``` - -Configured to run when the .NET App is published in the GitHub Actions deployment task in -[/.github/workflows/release.yml](https://github.com/NetCoreTemplates/vue-mjs/blob/prerender/.github/workflows/release.yml): - -```yaml - # Publish .NET Project - - name: Publish dotnet project - working-directory: ./MusicTypeChat - run: | - dotnet publish -c Release /p:APP_TASKS=prerender -``` - -Where it's able to control which App Tasks are run at deployment. - -### Pretty URLs for static .html pages - -A nicety we can add to serving static `.html` pages is giving them [Pretty URLs](https://en.wikipedia.org/wiki/Clean_URL) -by registering the Plugin: - -```csharp -Plugins.Add(new CleanUrlsFeature()); -``` - -Which allows prerendered pages to be accessed with and without its file extension: - - - [/blog/prerendering](/blog/prerendering) - - [/blog/prerendering.html](/blog/prerendering.html) - -### \ No newline at end of file diff --git a/MusicTypeChat/wwwroot/_blog/posts/2023-01-11_rider.md b/MusicTypeChat/wwwroot/_blog/posts/2023-01-11_rider.md deleted file mode 100644 index 02ceae0..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2023-01-11_rider.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Develop using JetBrains Rider -summary: Setting up & exploring development workflow in Rider -tags: c#,dev -splash: https://images.unsplash.com/photo-1472289065668-ce650ac443d2?crop=entropy&fit=crop&h=1000&w=2000 -author: Gayle Smith ---- - - - - -[JetBrains Rider](https://www.jetbrains.com/rider/) is our recommended IDE for any C# + JavaScript development as it -offers a great development UX for both, including excellent support -for TypeScript and popular JavaScript Framework SPA assets like [Vue SFC's](https://v3.vuejs.org/guide/single-file-component.html). - -#### Setup Rider IDE - -As Rider already understands and provides excellent HTML/JS/TypeScript support you'll be immediately productive out-of-the-box, -we can further improve the development experience for Vue.js Apps by adding an empty **vue** dependency to **package.json**: - -```json -{ - "devDependencies": { - "vue": "" - } -} -``` - -As this is just a heuristic Rider looks for to enable its Vue support, installing the dependency itself isn't used or required. - -Other than that the only plugin we recommend adding is: - - - - Tailwind CSS Plugin - - -Which provides provides intelli-sense support for [Tailwind CSS](https://tailwindcss.com). - -### Start both dotnet and Tailwind - -The only additional development workflow requirement to use tailwind is to start it running in the background -which can be done from a new Terminal: - -```bash -$ npm run ui:dev -``` - -We find `dotnet watch` offers the most productive iterative development workflow for .NET which refreshes on save -which works great with Tailwind which rewrites your `app.css` on save. - -How you want to run them is largely a matter of preference, our personal preference is to run the **dev** and **ui:dev** -npm scripts in your **package.json**: - -![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/scripts/dotnet-tailwind.png) - - -### Rider's Task Runner - -Where they will appear in Rider's useful task runner widget where you'll be able to easily, stop and rerun all project tasks: - -![](https://github.com/ServiceStack/docs/raw/master/docs/images/spa/rider-run-widget.png) - -### Running from the terminal - -These GUI tasks are just managing running CLI commands behind-the-scenes, which if you prefer you can use JetBrains -excellent multi-terminal support to run `$ dotnet watch` and `$ npm run ui:dev` from separate or split Terminal windows. - -### Deploying to Production - -This template also includes the necessary GitHub Actions to deploy this Apps production static assets to GitHub Pages CDN, -for more info, checkout [GitHub Actions Deployments](deploy). - -### Get Started - -If you're new to Vue 3 a good place to start is [Vue 3 Composition API](https://vuejs.org/api/composition-api-setup.html). diff --git a/MusicTypeChat/wwwroot/_blog/posts/2023-01-20_javascript.md b/MusicTypeChat/wwwroot/_blog/posts/2023-01-20_javascript.md deleted file mode 100644 index 7eb05c2..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2023-01-20_javascript.md +++ /dev/null @@ -1,612 +0,0 @@ ---- -title: Simple, Modern JavaScript -summary: Learn about JS Modules, Vue 3 and available rich UI Components -tags: js,dev -splash: https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000 -author: Brandon Foley ---- - - - - - - -JavaScript has progressed significantly in recent times where many of the tooling & language enhancements -that we used to rely on external tools for is now available in modern browsers alleviating the need for -complex tooling and npm dependencies that have historically plagued modern web development. - -The good news is that the complex npm tooling that was previously considered mandatory in modern JavaScript App -development can be considered optional as we can now utilize modern browser features like -[async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), -[JavaScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), -[dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import), -[import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) -and [modern language features](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide) for a -sophisticated development workflow without the need for any npm build tools. - -### Bringing Simplicity Back - -This template focuses on simplicity and eschews many aspects that has complicated modern JavaScript development, -specifically: - - - No npm node_modules or build tools - - No client side routing - - No heavy client state - -Effectively abandoning the traditional SPA approach in lieu of a simpler [MPA](https://docs.astro.build/en/concepts/mpa-vs-spa/) -development model using Razor Pages for Server Rendered content with any interactive UIs progressively enhanced with JavaScript. - -#### Freedom to use any JS library - -Avoiding the SPA route ends up affording more flexibility on which JS libraries each page can use as without heavy bundled JS -blobs of all JS used in the entire App, it's free to only load the required JS each page needs to best implement its -required functionality, which can be any JS library, preferably utilizing ESM builds that can be referenced from a -[JavaScript Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), taking advantage of the module system -native to modern browsers able to efficiently download the declarative matrix of dependencies each script needs. - -### Best libraries for progressive Multi Page Apps - -By default this template includes a collection of libraries we believe offers the best modern development experience in Progressive -MPA Web Apps, specifically: - -#### [Tailwind CLI](https://tailwindcss.com/docs/installation) -Tailwind enables a responsive, utility-first CSS framework for creating maintainable CSS at scale without the need for any CSS -preprocessors like Sass, which is configured to run from an npx script to avoid needing any node_module dependencies. - -#### [Vue 3](https://vuejs.org/guide/introduction.html) -Vue is a popular Progressive JavaScript Framework that makes it easy to create interactive Reactive Components whose -[Composition API](https://vuejs.org/api/composition-api-setup.html) offers a nice development model without requiring any -pre-processors like JSX. - -Where creating a component is as simple as: - -```js -const Hello = { - template: `Hello, {{name}}!`, - props: { name:String } -} -``` -
- -Or a simple reactive example: - -```js -import { ref } from "vue" - -const Counter = { - template: `Counter {{count}}`, - setup() { - let count = ref(1) - return { count } - } -} -``` - -
- -These components can be mounted using the standard [Vue 3 mount](https://vuejs.org/api/application.html#app-mount) API, but to -make it easier we've added additional APIs for declaratively mounting components to pages using the `data-component` and `data-props` -attributes, especially useful for including Vue components in Markdown content like this, e.g: - -```html -
-``` - -Alternatively they can be programatically added using the custom `mount` method in `api.mjs`: - -```js -import { mount } from "/mjs/api.mjs" -mount('#counter', Counter) -``` - -Both methods create components with access to all your Shared Components and any 3rd Party Plugins which -we can preview in this example that uses **@servicestack/vue**'s -[PrimaryButton](https://docs.servicestack.net/vue/gallery/navigation#primarybutton) -and [ModalDialog](https://docs.servicestack.net/vue/gallery/modals): - - -```js -const Plugin = { - template:`
- Open Modal - -
Hello @servicestack/vue!
-
-
`, - setup() { - const show = ref(false) - return { show } - } -} -``` - -
- -### @servicestack/vue -[@servicestack/vue](https://github.com/ServiceStack/servicestack-vue) is our growing Vue 3 Tailwind component library with a number of rich Tailwind components useful -in .NET Web Apps, including Input Components with auto form validation binding which is used by all HTML forms in this template. - -
- -### @servicestack/client -[@servicestack/client](https://docs.servicestack.net/javascript-client) is our generic JS/TypeScript client library -which enables a terse, typed API for using your App's typed DTOs from the built-in -[JavaScript ES6 Classes](https://docs.servicestack.net/javascript-add-servicestack-reference) support to enable an effortless -end-to-end Typed development model for calling your APIs **without any build steps**, e.g: - -```html - -
- - -``` - -For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with: - -```bash -$ npm run dtos -``` - -That can be referenced instead to unlock your IDE's static analysis type-checking and intelli-sense benefits during development: - -```js -import { Hello } from '/js/dtos.mjs' -client.api(new Hello({ name })) -``` - -You'll typically use all these libraries in your **API-enabled** components as seen in the -[HelloApi.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MusicTypeChat/wwwroot/mjs/components/HelloApi.mjs) -component on the home page which calls the [Hello](/ui/Hello) API on each key press: - -```js -import { ref } from "vue" -import { useClient } from "@servicestack/vue" -import { Hello } from "../dtos.mjs" - -export default { - template:/*html*/`
- -
{{ result }}
-
`, - props:['value'], - setup(props) { - let name = ref(props.value) - let result = ref('') - let client = useClient() - - async function update() { - let api = await client.api(new Hello({ name })) - if (api.succeeded) { - result.value = api.response.result - } - } - update() - - return { name, update, result } - } -} -``` - -Which we can also mount below: - -
- -We'll also go through and explain other features used in this component: - -#### `/*html*/` - -Although not needed in [Rider](rider) (which can automatically infer HTML in strings), the `/*html*/` type hint can be used -to instruct tooling like the [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) -VS Code extension to provide syntax highlighting and an enhanced authoring experience for HTML content in string literals. - -### useClient - -[useClient()](https://docs.servicestack.net/vue/use-client) provides managed APIs around the `JsonServiceClient` -instance registered in Vue App's with: - -```js -let client = JsonApiClient.create() -app.provide('client', client) -``` - -Which maintains contextual information around your API calls like **loading** and **error** states, used by `@servicestack/vue` components to -enable its auto validation binding. Other functionality in this provider include: - -```js -let { - api, apiVoid, apiForm, apiFormVoid, // Managed Typed ServiceClient APIs - loading, error, // Maintains 'loading' and 'error' states - setError, addFieldError, // Add custom errors in client - unRefs // Returns a dto with all Refs unwrapped -} = useClient() -``` - -Typically you would need to unwrap `ref` values when calling APIs, i.e: - -```js -let client = JsonApiClient.create() -let api = await client.api(new Hello({ name:name.value })) -``` - -#### useClient - api - -This is unnecessary in useClient `api*` methods which automatically unwraps ref values, allowing for the more pleasant API call: - -```js -let api = await client.api(new Hello({ name })) -``` - -#### useClient - unRefs - -But as DTOs are typed, passing reference values will report a type annotation warning in IDEs with type-checking enabled, -which can be resolved by explicitly unwrapping DTO ref values with `unRefs`: - -```js -let api = await client.api(new Hello(unRefs({ name }))) -``` - -#### useClient - setError - -`setError` can be used to populate client-side validation errors which the -[SignUp.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MusicTypeChat/wwwroot/Pages/SignUp.mjs) -component uses to report an invalid submissions when passwords don't match: - -```js -const { api, setError } = useClient() -async function onSubmit() { - if (password.value !== confirmPassword.value) { - setError({ fieldName:'confirmPassword', message:'Passwords do not match' }) - return - } - //... -} -``` - -### Form Validation - -All `@servicestack/vue` Input Components support contextual validation binding that's typically populated from API -[Error Response DTOs](https://docs.servicestack.net/error-handling) but can also be populated from client-side validation -as done above. - -#### Explicit Error Handling - -This populated `ResponseStatus` DTO can either be manually passed into each component's **status** property as done in [/TodoMvc](/TodoMvc): - -```html - -``` - -Where if you try adding an empty Todo the `CreateTodo` API will fail and populate its `store.error` reactive property with the -APIs Error Response DTO which the `` component checks to display any field validation errors adjacent to the HTML Input -with matching `id` fields: - -```js -let store = { - /** @type {Todo[]} */ - todos: [], - newTodo:'', - error:null, - async addTodo() { - this.todos.push(new Todo({ text:this.newTodo })) - let api = await client.api(new CreateTodo({ text:this.newTodo })) - if (api.succeeded) - this.newTodo = '' - else - this.error = api.error - }, - //... -} -``` - -#### Implicit Error Handling - -More often you'll want to take advantage of the implicit validation support in `useClient()` which makes its state available to child -components, alleviating the need to explicitly pass it in each component as seen in razor-tailwind's -[Contacts.mjs](https://github.com/NetCoreTemplates/razor-tailwind/blob/main/MusicTypeChat/wwwroot/Pages/Contacts.mjs) `Edit` component for its -[/Contacts](https://vue-mjs.web-templates.io/Contacts) page which doesn't do any manual error handling: - -```js -const Edit = { - template:/*html*/` -
- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
`, - props:['contact'], - emits:['done'], - setup(props, { emit }) { - const client = useClient() - const request = ref(new UpdateContact(props.contact)) - const colorOptions = propertyOptions(getProperty('UpdateContact','Color')) - - async function submit() { - const api = await client.api(request.value) - if (api.succeeded) close() - } - - async function onDelete () { - const api = await client.apiVoid(new DeleteContact({ id:props.id })) - if (api.succeeded) close() - } - - const close = () => emit('done') - - return { request, enumOptions, colorOptions, submit, onDelete, close } - } -} -``` - -Effectively making form validation binding a transparent detail where all `@servicestack/vue` -Input Components are able to automatically apply contextual validation errors next to the fields they apply to: - -![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/scripts/edit-contact-validation.png) - -### AutoForm Components - -We can elevate our productivity even further with -[Auto Form Components](https://docs.servicestack.net/vue/gallery/autoform) that can automatically generate an -instant API-enabled form with validation binding by just specifying the Request DTO you want to create the form of, e.g: - -```html - -``` - -
- -The AutoForm components are powered by your [App Metadata](https://docs.servicestack.net/vue/use-appmetadata) which allows creating -highly customized UIs from [declarative C# attributes](https://docs.servicestack.net/locode/declarative) whose customizations are -reused across all ServiceStack Auto UIs, including: - - - [API Explorer](https://docs.servicestack.net/api-explorer) - - [Locode](https://docs.servicestack.net/locode/) - - [Blazor Tailwind Components](https://docs.servicestack.net/templates-blazor-components) - -### Form Input Components - -In addition to including Tailwind versions of the standard [HTML Form Inputs](https://docs.servicestack.net/vue/gallery/form-inputs) controls to create beautiful Tailwind Forms, -it also contains a variety of integrated high-level components: - -- [FileInput](https://docs.servicestack.net/vue/gallery/fileinput) -- [TagInput](https://docs.servicestack.net/vue/gallery/taginput) -- [Autocomplete](https://docs.servicestack.net/vue/gallery/autocomplete) - -### useAuth - -Your Vue.js code can access Authenticated Users using [useAuth()](https://docs.servicestack.net/vue/use-auth) -which can also be populated without the overhead of an Ajax request by embedding the response of the built-in -[Authenticate API](/ui/Authenticate?tab=details) inside `_Layout.cshtml` with: - -```html - -``` - -Where it enables access to the below [useAuth()](https://docs.servicestack.net/vue/use-auth) utils for inspecting the -current authenticated user: - -```js -const { - signIn, // Sign In the currently Authenticated User - signOut, // Sign Out currently Authenticated User - user, // Access Authenticated User info in a reactive Ref - isAuthenticated, // Check if the current user is Authenticated in a reactive Ref - hasRole, // Check if the Authenticated User has a specific role - hasPermission, // Check if the Authenticated User has a specific permission - isAdmin // Check if the Authenticated User has the Admin role -} = useAuth() -``` - -This is used in [Bookings.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MusicTypeChat/wwwroot/Pages/Bookings.mjs) -to control whether the `` component should enable its delete functionality: - -```js -export default { - template/*html*/:` - - `, - setup(props) { - const { hasRole } = useAuth() - const canDelete = computed(() => hasRole('Manager')) - return { canDelete } - } -} -``` - -#### [JSDoc](https://jsdoc.app) - -We get great value from using [TypeScript](https://www.typescriptlang.org) to maintain our libraries typed code bases, however it -does mandate using an external tool to convert it to valid JS before it can be run, something the new Razor Vue.js templates expressly avoids. - -Instead it adds JSDoc type annotations to code where it adds value, which at the cost of slightly more verbose syntax enables much of the -same static analysis and intelli-sense benefits of TypeScript, but without needing any tools to convert it to valid JavaScript, e.g: - -```js -/** @param {KeyboardEvent} e */ -function validateSafeName(e) { - if (e.key.match(/[\W]+/g)) { - e.preventDefault() - return false - } -} -``` - -#### TypeScript Language Service - -Whilst the code-base doesn't use TypeScript syntax in its code base directly, it still benefits from TypeScript's language services -in IDEs for the included libraries from the TypeScript definitions included in `/lib/typings`, downloaded in -[postinstall.js](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MusicTypeChat/postinstall.js) after **npm install**. - -### Import Maps - -[Import Maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) is a useful browser feature that allows -specifying optimal names for modules, that can be used to map package names to the implementation it should use, e.g: - -```csharp -@Html.StaticImportMap(new() { - ["vue"] = "/lib/mjs/vue.mjs", - ["@servicestack/client"] = "/lib/mjs/servicestack-client.mjs", - ["@servicestack/vue"] = "/lib/mjs/servicestack-vue.mjs", -}) -``` - -Where they can be freely maintained in one place without needing to update any source code references. -This allows source code to be able to import from the package name instead of its physical location: - -```js -import { ref } from "vue" -import { useClient } from "@servicestack/vue" -import { JsonApiClient, $1, on } from "@servicestack/client" -``` - -It's a great solution for specifying using local unminified debug builds during **Development**, and more optimal CDN hosted -production builds when running in **Production**, alleviating the need to rely on complex build tools to perform this code transformation for us: - -```csharp -@Html.ImportMap(new() -{ - ["vue"] = ("/lib/mjs/vue.mjs", "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"), - ["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "https://unpkg.com/@servicestack/client@2/dist/servicestack-client.min.mjs"), - ["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "https://unpkg.com/@servicestack/vue@3/dist/servicestack-vue.min.mjs") -}) -``` - -Note: Specifying exact versions of each dependency improves initial load times by eliminating latency from redirects. - -Or if you don't want your Web App to reference any external dependencies, have the ImportMap reference local minified production builds instead: - -```csharp -@Html.ImportMap(new() -{ - ["vue"] = ("/lib/mjs/vue.mjs", "/lib/mjs/vue.min.mjs"), - ["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "/lib/mjs/servicestack-client.min.mjs"), - ["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "/lib/mjs/servicestack-vue.min.mjs") -}) -``` - -#### Polyfill for Safari - -Unfortunately Safari is the last modern browser to [support import maps](https://caniuse.com/import-maps) which is only now in -Technical Preview. Luckily this feature can be polyfilled with the [ES Module Shims](https://github.com/guybedford/es-module-shims) -that's configured in this template: - -```html -@if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) -{ - -} -``` - -### Fast Component Loading - -SPAs are notorious for being slow to load due to needing to download large blobs of JavaScript bundles that it needs to initialize -with their JS framework to mount their App component before it starts fetching the data from the server it needs to render its components. - -A complex solution to this problem is to server render the initial HTML content then re-render it again on the client after the page loads. -A simpler solution is to avoid unnecessary ajax calls by embedding the JSON data the component needs in the page that loads it, which is what -[/TodoMvc](/TodoMvc) does to load its initial list of todos using the [Service Gateway](https://docs.servicestack.net/service-gateway) -to invoke APIs in process and embed its JSON response with: - -```html - - -``` - -Where `ApiResultsAsJsonAsync` is a simplified helper that uses the `Gateway` to call your API and returns its unencoded JSON response: - -```csharp -(await Gateway.ApiAsync(new QueryTodos())).Response?.Results.AsRawJson(); -``` - -The result of which should render the List of Todos instantly when the page loads since it doesn't need to perform any additional Ajax requests -after the component is loaded. - -### Fast Page Loading - -We can get SPA-like page loading performance using htmx's [Boosting](https://htmx.org/docs/#boosting) feature which avoids full page reloads -by converting all anchor tags to use Ajax to load page content into the page body, improving perceived performance from needing to reload -scripts and CSS in ``. - -This is used in [Header.cshtml](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MusicTypeChat/Pages/Shared/Header.cshtml) to **boost** all -main navigation links: - -```html - -``` - -htmx has lots of useful [real world examples](https://htmx.org/examples/) that can be activated with declarative attributes, -another feature this template uses is the [class-tools](https://htmx.org/extensions/class-tools/) extension to hide elements from -appearing until after the page is loaded: - -```html -
- -``` - -Which is used to reduce UI yankiness from showing server rendered content before JS components have loaded. - -### @servicestack/vue Library - -[@servicestack/vue](https://docs.servicestack.net/vue/) is our cornerstone library for enabling a highly productive -Vue.js development model across our [Vue Tailwind Project templates](https://docs.servicestack.net/templates-vue) which -we'll continue to significantly invest in to unlock even greater productivity benefits in all Vue Tailwind Apps. - -In addition to a variety of high-productive components, it also contains a core library of functionality -underpinning the Vue Components that most Web Apps should also find useful: - -
\ No newline at end of file diff --git a/MusicTypeChat/wwwroot/_blog/posts/2023-01-21_start.md b/MusicTypeChat/wwwroot/_blog/posts/2023-01-21_start.md deleted file mode 100644 index 5b87b7e..0000000 --- a/MusicTypeChat/wwwroot/_blog/posts/2023-01-21_start.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Getting Started -summary: Found out how to use these template features -tags: js,dev,tailwind -splash: https://images.unsplash.com/photo-1533090161767-e6ffed986c88?crop=entropy&fit=crop&h=1000&w=2000 -author: Gayle Smith ---- - -### Setup - -If project wasn't created with [x new](https://docs.servicestack.net/dotnet-new), ensure postinstall tasks are run with: - -```bash -$ npm install -``` - -### Tailwind Configuration - -This template is configured with a stand-alone [Tailwind CSS CLI](https://tailwindcss.com/docs/installation) -installation with a modified **tailwind.input.css** that includes [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) -and [@tailwindcss/aspect-ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio) plugins so that no **node_modules** dependencies are needed. - -The [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin) plugin css is contained in `css/typography.css` which -applies a beautiful default style to unstyled HTML, ideal for Markdown content like this. - -### Running Tailwind during development - -Run tailwind in a new terminal during development to auto update your **app.css**: - -```bash -$ npm run ui:dev -``` - -For an optimal development experience run it together with `dotnet watch` to preview changes on each save. - -Or if using JetBrains Rider, **ui:dev** can be run directly from Rider in **package.json**: - -![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/servicestack-reference/scripts-tailwind.png) - -### Using JsonServiceClient in Web Pages - -Easiest way to call APIs is to use [@servicestack/client](https://docs.servicestack.net/javascript-client) with -the built-in [/types/mjs](/types/mjs) which returns your APIs annotated typed JS DTOs that can be used immediately -(i.e. without any build steps): - -```html - -
-``` - -```html - -``` - -For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with: - -```bash -$ npm run dtos -``` - -Where it will enable IDE static analysis when calling Typed APIs from JavaScript: - -```js -import { Hello } from '/mjs/dtos.mjs' -client.api(new Hello({ name })) -``` diff --git a/MusicTypeChat/wwwroot/img/authors/author1.svg b/MusicTypeChat/wwwroot/img/authors/author1.svg deleted file mode 100644 index f980887..0000000 --- a/MusicTypeChat/wwwroot/img/authors/author1.svg +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MusicTypeChat/wwwroot/img/authors/author2.svg b/MusicTypeChat/wwwroot/img/authors/author2.svg deleted file mode 100644 index 1638f32..0000000 --- a/MusicTypeChat/wwwroot/img/authors/author2.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MusicTypeChat/wwwroot/img/authors/author3.svg b/MusicTypeChat/wwwroot/img/authors/author3.svg deleted file mode 100644 index efec71e..0000000 --- a/MusicTypeChat/wwwroot/img/authors/author3.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MusicTypeChat/wwwroot/mjs/app.mjs b/MusicTypeChat/wwwroot/mjs/app.mjs index 9bb506c..4947c69 100644 --- a/MusicTypeChat/wwwroot/mjs/app.mjs +++ b/MusicTypeChat/wwwroot/mjs/app.mjs @@ -1,11 +1,6 @@ import { createApp, reactive, ref, computed } from "vue" import { JsonApiClient, $1, $$ } from "@servicestack/client" import ServiceStackVue from "@servicestack/vue" -import HelloApi from "./components/HelloApi.mjs" -import SrcLink from "./components/SrcLink.mjs" -import AudioRecorderComponent from "./components/MusicAudio.mjs"; -import VueComponentGallery from "./components/VueComponentGallery.mjs" -import VueComponentLibrary from "./components/VueComponentLibrary.mjs" let client = null, Apps = [] let AppData = { @@ -13,41 +8,8 @@ let AppData = { } export { client, Apps } -/** Simple inline component examples */ -const Hello = { - template: `Hello, {{name}}!`, - props: { name:String } -} -const Counter = { - template: `Counter {{count}}`, - setup() { - let count = ref(1) - return { count } - } -} -const Plugin = { - template:`
- Open Modal - -
Hello @servicestack/vue!
-
-
`, - setup() { - const show = ref(false) - return { show } - } -} - /** Shared Components */ const Components = { - HelloApi, - SrcLink, - Hello, - Counter, - Plugin, - VueComponentGallery, - VueComponentLibrary, - AudioRecorderComponent } const alreadyMounted = el => el.__vue_app__ diff --git a/MusicTypeChat/wwwroot/mjs/components/GettingStarted.mjs b/MusicTypeChat/wwwroot/mjs/components/GettingStarted.mjs deleted file mode 100644 index a250813..0000000 --- a/MusicTypeChat/wwwroot/mjs/components/GettingStarted.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import { ref } from "vue" -import ShellCommand from "./ShellCommand.mjs" - -export default { - components: { - ShellCommand, - }, - template:/*html*/` -
-

Create New Project

- - - - dotnet tool install -g x - x new {{template}} {{project}} - -

In /MusicTypeChat, Run Tailwind

- npm run ui:dev - -

Run .NET Project (New Terminal)

- dotnet watch - -
- -
-
`, - props: { template:String }, - setup(props) { - const project = ref('ProjectName') - /** @param path {string} - * @returns {string} */ - const resolvePath = (path) => navigator.userAgent.indexOf("Win") >= 0 ? path.replace(/\//g,'\\') : path - const uiPath = () => resolvePath(`ui`) - const apiPath = () => resolvePath(`api/${project.value}`) - - /** @param e {KeyboardEvent} */ - function validateSafeName(e) { - if (e.key.match(/[\W]+/g)) { - e.preventDefault() - return false - } - } - return { project, uiPath, apiPath, validateSafeName } - } -} \ No newline at end of file diff --git a/MusicTypeChat/wwwroot/mjs/components/HelloApi.mjs b/MusicTypeChat/wwwroot/mjs/components/HelloApi.mjs deleted file mode 100644 index 251a95a..0000000 --- a/MusicTypeChat/wwwroot/mjs/components/HelloApi.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { ref } from "vue" -import { useClient } from "@servicestack/vue" -import { Hello } from "../dtos.mjs" - -export default { - template:/*html*/`
- -
{{ result }}
-
`, - props:['value'], - setup(props) { - let name = ref(props.value) - let result = ref('') - let client = useClient() - - async function update() { - let api = await client.api(new Hello({ name })) - if (api.succeeded) { - result.value = api.response.result - } - } - update() - - return { name, update, result } - } -} diff --git a/MusicTypeChat/wwwroot/mjs/components/ShellCommand.mjs b/MusicTypeChat/wwwroot/mjs/components/ShellCommand.mjs deleted file mode 100644 index eda5d81..0000000 --- a/MusicTypeChat/wwwroot/mjs/components/ShellCommand.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { ref } from "vue" -import { map } from "@servicestack/client" - -export default { - template:`
-
- - sh -
-
-
- - {{ successText }} -
-
-
`, - setup(props) { - let successText = ref('') - /** @param {MouseEvent} e */ - function copy(e) { - let $el = document.createElement("input") - let $lbl = e.target.parentElement.querySelector('label') - $el.setAttribute("value", $lbl.innerText) - document.body.appendChild($el) - $el.select() - document.execCommand("copy") - document.body.removeChild($el) - if (typeof window.getSelection == "function") { - const range = document.createRange() - range.selectNodeContents($lbl) - map(window.getSelection(), sel => { - sel.removeAllRanges() - sel.addRange(range) - }) - } - successText.value = 'copied' - setTimeout(() => successText.value = '', 3000) - } - return { successText, copy } - } -} diff --git a/MusicTypeChat/wwwroot/mjs/components/SrcLink.mjs b/MusicTypeChat/wwwroot/mjs/components/SrcLink.mjs deleted file mode 100644 index f3fe2e3..0000000 --- a/MusicTypeChat/wwwroot/mjs/components/SrcLink.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { lastRightPart } from "@servicestack/client" - -export default { - template:/*html*/` - file icon{{ fileName }} - - - - - - {{ fileName }} - `, - props: ['href','iconSrc'], - setup(props) { - const fileName = lastRightPart(props.href, '/') - return { fileName } - } -} diff --git a/MusicTypeChat/wwwroot/mjs/components/VueComponentGallery.mjs b/MusicTypeChat/wwwroot/mjs/components/VueComponentGallery.mjs deleted file mode 100644 index 93f47a6..0000000 --- a/MusicTypeChat/wwwroot/mjs/components/VueComponentGallery.mjs +++ /dev/null @@ -1,69 +0,0 @@ -export default { - template:/*html*/` -
-
-
- - - -
-
- -
-

- Component Gallery -

-

- Explore Vue Tailwind Component Gallery -

-
-
-
- - - Instant customizable UIs for calling AutoQuery CRUD APIs - - - DataGrid Component Examples for rendering tabular data - - - Render Auto Form UIs from a Request DTO class - - - Tailwind UI Input Components - - - Modal Dialogs and Slide Overs - - - Breadcrumbs and Link navigation components - - - Tailwind Alert and Notification components - - - HTML Value Formatters - - -
-
-
- `, - setup() { - const svg = (viewbox,body) => - `${body}` - - const Icons = { - AutoQueryGrid: svg("0 0 28 28", ""), - DataGrid: svg("0 0 16 16", ""), - AutoForms: svg("0 0 1024 1024", ""), - FormInputs: svg("0 0 36 36", ""), - Modals: svg("0 0 32 32", ""), - Navigation: svg("0 0 12 12", ""), - Alerts: svg("0 0 16 16", ""), - Formats: svg("0 0 1024 1024", ""), - Code: svg("0 0 24 24", ""), - } - return { Icons } - } -} diff --git a/MusicTypeChat/wwwroot/mjs/components/VueComponentLibrary.mjs b/MusicTypeChat/wwwroot/mjs/components/VueComponentLibrary.mjs deleted file mode 100644 index c53bbf8..0000000 --- a/MusicTypeChat/wwwroot/mjs/components/VueComponentLibrary.mjs +++ /dev/null @@ -1,47 +0,0 @@ -export default { - template:/*html*/` -
-
-

- Vue Library -

-
-
-
- - - Reflective utils for inspecting API AppMetadata - - - Utilize JSON Api Client features in Components - - - Inspect Authenticated Users Info, Roles & Permissions - - - Built-in Formats and formatting functions - - - File utils for resolving SVG icons, extensions and MIME types - - - Manage global configuration & defaults - - - General functionality and utils - - -
-
-
- `, - setup() { - const svg = (viewbox,body) => - `${body}` - - const Icons = { - Code: svg("0 0 24 24", ""), - } - return { Icons } - } -}