diff --git a/playground/Components/Home/CodeBlock.razor b/playground/Components/Home/CodeBlock.razor index 66d51a6e..7a8217e7 100644 --- a/playground/Components/Home/CodeBlock.razor +++ b/playground/Components/Home/CodeBlock.razor @@ -7,7 +7,7 @@ ● @Title } -
+
@((MarkupString)HighlightedCode)
diff --git a/playground/Components/Home/IntroductionSection.razor b/playground/Components/Home/IntroductionSection.razor new file mode 100644 index 00000000..ce644c33 --- /dev/null +++ b/playground/Components/Home/IntroductionSection.razor @@ -0,0 +1,505 @@ +@implements IAsyncDisposable + +
+
+ Query-Based DTO Generation +

+ Unlike traditional generators that create queries from class definitions,
+ Linqraft generates DTOs from your queries. Watch how it works: +

+ + +
+
+ @for (int i = 1; i < steps.Length; i++) + { + var stepIndex = i; + // Tab is green if: step is completed (stepIndex <= currentStep) + var isCompleted = stepIndex <= currentStep; + // Tab is cyan if: this is the target step AND we're currently typing + var isCurrent = stepIndex == targetStep && isTyping; + // Tab is gray if: step is not completed and not current + var isPending = stepIndex > currentStep && stepIndex != targetStep; + +
+ + + @if (isCurrent && isTyping) + { + +
+
+
+ } +
+ } +
+
+ +
+ +
+
+

Your Query

+
+
+ + @if (isTyping) + { +
+ +
+ } +
+
+ + +
+
+
+ +

Generated Code

+
+ +
+ + +
+
+
+ +
+
+
+
+
+ +@code { + private int currentStep = 0; + private int targetStep = 0; + private int activeTab = 0; + private bool isTyping = false; + private bool isGeneratedUpdating = false; + private string displayedCode = ""; + private string previousCode = ""; + private string currentBlockText = ""; + private int currentCharIndex = 0; + private string highlightedSection = ""; + private System.Timers.Timer? typewriterTimer; + private CancellationTokenSource? _cts; + + // Step definitions based on README comments + private readonly CodeStep[] steps = [ + new CodeStep + { + Description = "Initial setup - empty structure", + CodeToAdd = "", + BaseCode = @"var orders = await dbContext.Orders + .SelectExpr(o => new + { + }) + .ToListAsync();", + DtoCode = @"// DTO class will be auto-generated", + SelectCode = @"// Select expression will be auto-generated" + }, + new CodeStep + { + Description = "can use inferred member names", + CodeToAdd = @" o.Id,", + BaseCode = "", + DtoCode = @"public partial class OrderDto +{ + public required int Id { get; set; } +}", + SelectCode = @".Select(o => new OrderDto +{ + Id = o.Id, +})" + }, + new CodeStep + { + Description = "null-propagation supported", + CodeToAdd = @" CustomerName = o.Customer?.Name,", + BaseCode = "", + DtoCode = @"public partial class OrderDto +{ + public required int Id { get; set; } + public required string? CustomerName { get; set; } +}", + SelectCode = @".Select(o => new OrderDto +{ + Id = o.Id, + CustomerName = o.Customer != null + ? o.Customer.Name : null, +})" + }, + new CodeStep + { + Description = "also works for nested objects", + CodeToAdd = @" CustomerCountry = o.Customer?.Address?.Country?.Name, + CustomerCity = o.Customer?.Address?.City?.Name,", + BaseCode = "", + DtoCode = @"public partial class OrderDto +{ + public required int Id { get; set; } + public required string? CustomerName { get; set; } + public required string? CustomerCountry { get; set; } + public required string? CustomerCity { get; set; } +}", + SelectCode = @".Select(o => new OrderDto +{ + Id = o.Id, + CustomerName = o.Customer != null + ? o.Customer.Name : null, + CustomerCountry = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.Country != null + ? o.Customer.Address.Country.Name : null, + CustomerCity = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.City != null + ? o.Customer.Address.City.Name : null, +})" + }, + new CodeStep + { + Description = "you can use anonymous types inside", + CodeToAdd = @" CustomerInfo = new + { + Email = o.Customer?.EmailAddress, + Phone = o.Customer?.PhoneNumber, + },", + BaseCode = "", + DtoCode = @"public partial class OrderDto +{ + public required int Id { get; set; } + public required string? CustomerName { get; set; } + public required string? CustomerCountry { get; set; } + public required string? CustomerCity { get; set; } + public required CustomerInfoDto? CustomerInfo { get; set; } +} + +public partial class CustomerInfoDto +{ + public required string? Email { get; set; } + public required string? Phone { get; set; } +}", + SelectCode = @".Select(o => new OrderDto +{ + Id = o.Id, + CustomerName = o.Customer != null + ? o.Customer.Name : null, + CustomerCountry = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.Country != null + ? o.Customer.Address.Country.Name : null, + CustomerCity = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.City != null + ? o.Customer.Address.City.Name : null, + CustomerInfo = new CustomerInfoDto + { + Email = o.Customer != null + ? o.Customer.EmailAddress : null, + Phone = o.Customer != null + ? o.Customer.PhoneNumber : null, + } +})" + }, + new CodeStep + { + Description = "calculated fields? no problem!", + CodeToAdd = @" LatestOrderDate = o.OrderItems.Max(oi => oi.OrderDate), + TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice),", + BaseCode = "", + DtoCode = @"public partial class OrderDto +{ + public required int Id { get; set; } + public required string? CustomerName { get; set; } + public required string? CustomerCountry { get; set; } + public required string? CustomerCity { get; set; } + public required CustomerInfoDto? CustomerInfo { get; set; } + public required DateTime LatestOrderDate { get; set; } + public required decimal TotalAmount { get; set; } +} + +public partial class CustomerInfoDto +{ + public required string? Email { get; set; } + public required string? Phone { get; set; } +}", + SelectCode = @".Select(o => new OrderDto +{ + Id = o.Id, + CustomerName = o.Customer != null + ? o.Customer.Name : null, + CustomerCountry = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.Country != null + ? o.Customer.Address.Country.Name : null, + CustomerCity = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.City != null + ? o.Customer.Address.City.Name : null, + CustomerInfo = new CustomerInfoDto + { + Email = o.Customer != null + ? o.Customer.EmailAddress : null, + Phone = o.Customer != null + ? o.Customer.PhoneNumber : null, + }, + LatestOrderDate = o.OrderItems.Max(oi => oi.OrderDate), + TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice), +})" + }, + new CodeStep + { + Description = "collections available", + CodeToAdd = @" Items = o.OrderItems.Select(oi => new + { + ProductName = oi.Product?.Name, + oi.Quantity + }),", + BaseCode = "", + DtoCode = @"public partial class OrderDto +{ + public required int Id { get; set; } + public required string? CustomerName { get; set; } + public required string? CustomerCountry { get; set; } + public required string? CustomerCity { get; set; } + public required CustomerInfoDto? CustomerInfo { get; set; } + public required DateTime LatestOrderDate { get; set; } + public required decimal TotalAmount { get; set; } + public required IEnumerable Items { get; set; } +} + +public partial class CustomerInfoDto +{ + public required string? Email { get; set; } + public required string? Phone { get; set; } +} + +public partial class ItemsDto +{ + public required string? ProductName { get; set; } + public required int Quantity { get; set; } +}", + SelectCode = @".Select(o => new OrderDto +{ + Id = o.Id, + CustomerName = o.Customer != null + ? o.Customer.Name : null, + CustomerCountry = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.Country != null + ? o.Customer.Address.Country.Name : null, + CustomerCity = o.Customer != null + && o.Customer.Address != null + && o.Customer.Address.City != null + ? o.Customer.Address.City.Name : null, + CustomerInfo = new CustomerInfoDto + { + Email = o.Customer != null + ? o.Customer.EmailAddress : null, + Phone = o.Customer != null + ? o.Customer.PhoneNumber : null, + }, + LatestOrderDate = o.OrderItems.Max(oi => oi.OrderDate), + TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice), + Items = o.OrderItems.Select(oi => new ItemsDto + { + ProductName = oi.Product != null + ? oi.Product.Name : null, + Quantity = oi.Quantity + }) +})" + } + ]; + + protected override async Task OnInitializedAsync() + { + _cts = new CancellationTokenSource(); + + // Initialize with Step 1 (showing o.Id) + currentStep = 1; + targetStep = 1; + BuildCodeUpToStep(1); + } + + private void BuildCodeUpToStep(int targetStep) + { + displayedCode = steps[0].BaseCode; + for (int i = 1; i <= targetStep && i < steps.Length; i++) + { + string codeToAdd = steps[i].CodeToAdd; + int insertPosition = displayedCode.LastIndexOf(" })"); + if (insertPosition > 0) + { + displayedCode = displayedCode.Insert(insertPosition, codeToAdd + "\n"); + } + } + } + + private async Task StartTypewriterForStep(int newTargetStep) + { + if (_cts?.Token.IsCancellationRequested == true) return; + + // Stop any existing animation + typewriterTimer?.Stop(); + + // Set target step immediately (this makes the tab turn cyan) + targetStep = newTargetStep; + + // Save previous code for highlighting + previousCode = displayedCode; + + // Determine what needs to be added + if (newTargetStep > currentStep) + { + // Adding new code blocks + isTyping = true; + currentCharIndex = 0; + + // Build the text to add (all blocks between currentStep and targetStep) + currentBlockText = ""; + for (int i = currentStep + 1; i <= newTargetStep && i < steps.Length; i++) + { + currentBlockText += steps[i].CodeToAdd + "\n"; + } + + highlightedSection = currentBlockText; + await InvokeAsync(StateHasChanged); + + // Start typewriter animation + typewriterTimer = new System.Timers.Timer(8); // 8ms per character (faster) + typewriterTimer.Elapsed += async (sender, e) => + { + try + { + await TypeNextCharacter(); + } + catch (ObjectDisposedException) { } + }; + typewriterTimer.AutoReset = true; + typewriterTimer.Start(); + } + else if (newTargetStep < currentStep) + { + // Removing code blocks - instant update + BuildCodeUpToStep(newTargetStep); + currentStep = newTargetStep; + targetStep = newTargetStep; + highlightedSection = ""; + await InvokeAsync(StateHasChanged); + } + } + + private async Task TypeNextCharacter() + { + if (_cts?.Token.IsCancellationRequested == true) return; + + if (currentCharIndex >= currentBlockText.Length) + { + // Typing complete + typewriterTimer?.Stop(); + isTyping = false; + + // Update currentStep immediately (this makes tabs turn green and updates right side) + currentStep = targetStep; + await InvokeAsync(StateHasChanged); + + // Wait a moment before clearing highlight + await Task.Delay(1000); + highlightedSection = ""; + await InvokeAsync(StateHasChanged); + return; + } + + // Add next character + char nextChar = currentBlockText[currentCharIndex]; + currentCharIndex++; + + // Insert the character before the closing braces + int insertPosition = displayedCode.LastIndexOf(" })"); + if (insertPosition > 0) + { + displayedCode = displayedCode.Insert(insertPosition, nextChar.ToString()); + } + + await InvokeAsync(StateHasChanged); + } + + private double GetProgressPercentage() + { + if (string.IsNullOrEmpty(currentBlockText) || currentBlockText.Length == 0) + return 0; + + return Math.Min(100, (double)currentCharIndex / currentBlockText.Length * 100); + } + + private async void JumpToStep(int newTargetStep) + { + if (newTargetStep == currentStep && !isTyping) return; + + // Start typewriter animation for the new step + await StartTypewriterForStep(newTargetStep); + } + + private void SetActiveTab(int tab) + { + activeTab = tab; + StateHasChanged(); + } + + private string GetCurrentTabTitle() + { + return activeTab == 0 ? "Auto-Generated DTO" : "Generated Select Expression"; + } + + private string GetCurrentGeneratedCode() + { + if (currentStep >= steps.Length) return steps[^1].GetCodeForTab(activeTab); + return steps[currentStep].GetCodeForTab(activeTab); + } + + public async ValueTask DisposeAsync() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + + if (typewriterTimer != null) + { + typewriterTimer.Stop(); + typewriterTimer.Dispose(); + typewriterTimer = null; + } + } + + private class CodeStep + { + public required string Description { get; set; } + public required string CodeToAdd { get; set; } + public required string BaseCode { get; set; } + public required string DtoCode { get; set; } + public required string SelectCode { get; set; } + + public string GetCodeForTab(int tab) + { + return tab == 0 ? DtoCode : SelectCode; + } + } +} diff --git a/playground/Components/Home/SectionTitle.razor b/playground/Components/Home/SectionTitle.razor index 97dc0234..34c4bc73 100644 --- a/playground/Components/Home/SectionTitle.razor +++ b/playground/Components/Home/SectionTitle.razor @@ -1,4 +1,4 @@ -

@ChildContent

+

@ChildContent

@code { [Parameter] diff --git a/playground/Components/Home/WhyLinqraftSection.razor b/playground/Components/Home/WhyLinqraftSection.razor index 1400423b..d7316f2f 100644 --- a/playground/Components/Home/WhyLinqraftSection.razor +++ b/playground/Components/Home/WhyLinqraftSection.razor @@ -82,7 +82,7 @@
  • - No manual DTO definitions - auto-generated from anonymous types + No manual DTO definitions - auto-generated from query shape
  • @@ -93,15 +93,6 @@ Zero dependencies - only a source generator, no runtime library needed
  • - -
    -

    See the comparison section below to see what it looks like in practice!

    -
    - - - -
    -
    diff --git a/playground/Pages/Home.razor b/playground/Pages/Home.razor index b59faef4..106da63b 100644 --- a/playground/Pages/Home.razor +++ b/playground/Pages/Home.razor @@ -4,11 +4,12 @@
    + - +