fullstack-classic

SKILL.md

Classic Fullstack Integration Patterns

Form Handling

Server-Side Form Processing

// Controller
public class ContactController : Controller
{
    private readonly IContactService _contactService;
    private readonly ILogger<ContactController> _logger;

    public ContactController(
        IContactService contactService,
        ILogger<ContactController> logger)
    {
        _contactService = contactService;
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Index()
    {
        return View(new ContactFormModel());
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Index(ContactFormModel model, CancellationToken ct)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        try
        {
            await _contactService.ProcessContactAsync(model, ct);
            TempData["SuccessMessage"] = "Thank you for your message!";
            return RedirectToAction(nameof(Index));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process contact form");
            ModelState.AddModelError("", "An error occurred. Please try again.");
            return View(model);
        }
    }
}

Razor Form

@model ContactFormModel

@if (TempData["SuccessMessage"] != null)
{
    <div class="alert alert-success">
        @TempData["SuccessMessage"]
    </div>
}

<form asp-action="Index" method="post">
    @Html.AntiForgeryToken()
    
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" type="email" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
    
    <div class="form-group">
        <label asp-for="Message"></label>
        <textarea asp-for="Message" class="form-control" rows="5"></textarea>
        <span asp-validation-for="Message" class="text-danger"></span>
    </div>
    
    <button type="submit" class="btn btn-primary">Send Message</button>
</form>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

jQuery AJAX Integration

AJAX Form Submission

// JavaScript
$(document).ready(function() {
    $('#contact-form').on('submit', function(e) {
        e.preventDefault();
        
        var $form = $(this);
        var $submitBtn = $form.find('button[type="submit"]');
        var $result = $('#form-result');
        
        // Disable button and show loading state
        $submitBtn.prop('disabled', true).text('Sending...');
        $result.empty();
        
        $.ajax({
            url: $form.attr('action'),
            type: 'POST',
            data: $form.serialize(),
            success: function(response) {
                if (response.success) {
                    $result.html('<div class="alert alert-success">' + response.message + '</div>');
                    $form[0].reset();
                } else {
                    showValidationErrors(response.errors);
                }
            },
            error: function(xhr, status, error) {
                $result.html('<div class="alert alert-danger">An error occurred. Please try again.</div>');
                console.error('Form submission failed:', error);
            },
            complete: function() {
                $submitBtn.prop('disabled', false).text('Send Message');
            }
        });
    });
    
    function showValidationErrors(errors) {
        // Clear previous errors
        $('.field-validation-error').text('');
        $('.input-validation-error').removeClass('input-validation-error');
        
        // Show new errors
        $.each(errors, function(field, messages) {
            var $field = $('[name="' + field + '"]');
            $field.addClass('input-validation-error');
            $field.siblings('.field-validation-error').text(messages.join(', '));
        });
    }
});

AJAX Controller Action

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SubmitAjax(ContactFormModel model, CancellationToken ct)
{
    if (!ModelState.IsValid)
    {
        var errors = ModelState
            .Where(x => x.Value.Errors.Count > 0)
            .ToDictionary(
                kvp => kvp.Key,
                kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
            );
            
        return Json(new { success = false, errors });
    }

    try
    {
        await _contactService.ProcessContactAsync(model, ct);
        return Json(new { success = true, message = "Thank you for your message!" });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to process contact form");
        return Json(new { 
            success = false, 
            errors = new { General = new[] { "An error occurred. Please try again." } }
        });
    }
}

Anti-Forgery Token Handling

Include Token in AJAX Requests

// Setup for all AJAX requests
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (settings.type === 'POST' || settings.type === 'PUT' || settings.type === 'DELETE') {
            var token = $('input[name="__RequestVerificationToken"]').val();
            if (token) {
                xhr.setRequestHeader('RequestVerificationToken', token);
            }
        }
    }
});

// Or include in data for form-encoded requests
$.ajax({
    url: '/api/items',
    type: 'POST',
    data: {
        __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val(),
        name: 'New Item'
    }
});

Token in Layout

@* Add to _Layout.cshtml for global availability *@
<form id="__AjaxAntiForgeryForm" action="#" method="post">
    @Html.AntiForgeryToken()
</form>
// Get token from hidden form
function getAntiForgeryToken() {
    return $('#__AjaxAntiForgeryForm input[name="__RequestVerificationToken"]').val();
}

Loading Content Dynamically

Partial View Loading

// Controller
[HttpGet]
public async Task<IActionResult> LoadProducts(
    int page = 1, 
    string category = null,
    CancellationToken ct = default)
{
    var products = await _productService.GetPagedAsync(page, 12, category, ct);
    return PartialView("_ProductGrid", products);
}

[HttpGet]
public async Task<IActionResult> ProductDetails(Guid id, CancellationToken ct)
{
    var product = await _productService.GetByIdAsync(id, ct);
    if (product == null)
    {
        return NotFound();
    }
    return PartialView("_ProductDetails", product);
}
// Load more products
$('#load-more').on('click', function() {
    var $btn = $(this);
    var page = parseInt($btn.data('page')) + 1;
    var category = $btn.data('category');
    
    $btn.prop('disabled', true).text('Loading...');
    
    $.get('/Products/LoadProducts', { page: page, category: category })
        .done(function(html) {
            $('#product-grid').append(html);
            $btn.data('page', page);
        })
        .fail(function() {
            alert('Failed to load products');
        })
        .always(function() {
            $btn.prop('disabled', false).text('Load More');
        });
});

// Load product details in modal
$(document).on('click', '[data-product-details]', function(e) {
    e.preventDefault();
    var productId = $(this).data('product-details');
    
    $.get('/Products/ProductDetails/' + productId)
        .done(function(html) {
            $('#modal-content').html(html);
            $('#product-modal').modal('show');
        })
        .fail(function() {
            alert('Failed to load product details');
        });
});

Search with Debounce

JavaScript

var MYAPP = MYAPP || {};

MYAPP.search = (function($) {
    var debounceTimer;
    var $input;
    var $results;
    var minChars = 3;
    var debounceDelay = 300;
    
    function init() {
        $input = $('#search-input');
        $results = $('#search-results');
        
        $input.on('keyup', function() {
            var query = $(this).val().trim();
            
            clearTimeout(debounceTimer);
            
            if (query.length < minChars) {
                $results.empty().hide();
                return;
            }
            
            debounceTimer = setTimeout(function() {
                performSearch(query);
            }, debounceDelay);
        });
        
        // Close results when clicking outside
        $(document).on('click', function(e) {
            if (!$(e.target).closest('.search-container').length) {
                $results.hide();
            }
        });
    }
    
    function performSearch(query) {
        $results.html('<div class="search-loading">Searching...</div>').show();
        
        $.get('/Search/Results', { q: query })
            .done(function(html) {
                $results.html(html).show();
            })
            .fail(function() {
                $results.html('<div class="search-error">Search failed</div>');
            });
    }
    
    return { init: init };
})(jQuery);

$(document).ready(function() {
    MYAPP.search.init();
});

Controller

[HttpGet]
public async Task<IActionResult> Results(string q, CancellationToken ct)
{
    if (string.IsNullOrWhiteSpace(q) || q.Length < 3)
    {
        return PartialView("_NoResults");
    }

    var results = await _searchService.SearchAsync(q, maxResults: 10, ct);
    return PartialView("_SearchResults", results);
}

Pagination

Controller

[HttpGet]
public async Task<IActionResult> Index(int page = 1, CancellationToken ct = default)
{
    const int pageSize = 12;
    var result = await _productService.GetPagedAsync(page, pageSize, ct);
    
    ViewBag.CurrentPage = page;
    ViewBag.TotalPages = result.TotalPages;
    ViewBag.HasPrevious = page > 1;
    ViewBag.HasNext = page < result.TotalPages;
    
    return View(result.Items);
}

Razor Partial

@* _Pagination.cshtml *@
@{
    var currentPage = (int)ViewBag.CurrentPage;
    var totalPages = (int)ViewBag.TotalPages;
    var hasPrevious = (bool)ViewBag.HasPrevious;
    var hasNext = (bool)ViewBag.HasNext;
}

@if (totalPages > 1)
{
    <nav aria-label="Page navigation">
        <ul class="pagination">
            <li class="page-item @(!hasPrevious ? "disabled" : "")">
                <a class="page-link" 
                   asp-action="Index" 
                   asp-route-page="@(currentPage - 1)"
                   aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>
            
            @for (int i = 1; i <= totalPages; i++)
            {
                <li class="page-item @(i == currentPage ? "active" : "")">
                    <a class="page-link" asp-action="Index" asp-route-page="@i">@i</a>
                </li>
            }
            
            <li class="page-item @(!hasNext ? "disabled" : "")">
                <a class="page-link" 
                   asp-action="Index" 
                   asp-route-page="@(currentPage + 1)"
                   aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        </ul>
    </nav>
}

AJAX Pagination

$(document).on('click', '.pagination a', function(e) {
    e.preventDefault();
    
    var url = $(this).attr('href');
    
    $.get(url)
        .done(function(html) {
            $('#content-container').html(html);
            // Update browser URL without reload
            history.pushState(null, '', url);
        })
        .fail(function() {
            alert('Failed to load page');
        });
});

// Handle browser back/forward
$(window).on('popstate', function() {
    $.get(location.href)
        .done(function(html) {
            $('#content-container').html(html);
        });
});

File Upload

Razor Form

<form asp-action="Upload" method="post" enctype="multipart/form-data">
    @Html.AntiForgeryToken()
    
    <div class="form-group">
        <label for="file">Select file</label>
        <input type="file" name="file" id="file" class="form-control-file" accept=".jpg,.png,.pdf" />
        <small class="form-text text-muted">Max size: 5MB. Allowed: JPG, PNG, PDF</small>
    </div>
    
    <button type="submit" class="btn btn-primary">Upload</button>
</form>

Controller

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload(IFormFile file, CancellationToken ct)
{
    if (file == null || file.Length == 0)
    {
        ModelState.AddModelError("file", "Please select a file");
        return View();
    }

    if (file.Length > 5 * 1024 * 1024) // 5MB
    {
        ModelState.AddModelError("file", "File size cannot exceed 5MB");
        return View();
    }

    var allowedExtensions = new[] { ".jpg", ".png", ".pdf" };
    var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
    if (!allowedExtensions.Contains(extension))
    {
        ModelState.AddModelError("file", "Invalid file type");
        return View();
    }

    var fileName = $"{Guid.NewGuid()}{extension}";
    var filePath = Path.Combine(_uploadPath, fileName);

    using (var stream = new FileStream(filePath, FileMode.Create))
    {
        await file.CopyToAsync(stream, ct);
    }

    TempData["SuccessMessage"] = "File uploaded successfully";
    return RedirectToAction(nameof(Index));
}

AJAX File Upload

$('#upload-form').on('submit', function(e) {
    e.preventDefault();
    
    var formData = new FormData(this);
    var $progress = $('#upload-progress');
    var $progressBar = $progress.find('.progress-bar');
    
    $progress.show();
    
    $.ajax({
        url: $(this).attr('action'),
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        xhr: function() {
            var xhr = new window.XMLHttpRequest();
            xhr.upload.addEventListener('progress', function(e) {
                if (e.lengthComputable) {
                    var percent = Math.round((e.loaded / e.total) * 100);
                    $progressBar.css('width', percent + '%').text(percent + '%');
                }
            });
            return xhr;
        },
        success: function(response) {
            if (response.success) {
                showSuccess('File uploaded successfully');
                $('#file-input').val('');
            } else {
                showError(response.message);
            }
        },
        error: function() {
            showError('Upload failed');
        },
        complete: function() {
            setTimeout(function() {
                $progress.hide();
                $progressBar.css('width', '0%').text('');
            }, 1000);
        }
    });
});

Error Handling

Global AJAX Error Handler

$(document).ajaxError(function(event, xhr, settings, error) {
    if (xhr.status === 401) {
        // Redirect to login
        window.location.href = '/Account/Login?returnUrl=' + encodeURIComponent(window.location.pathname);
        return;
    }
    
    if (xhr.status === 403) {
        showError('You do not have permission to perform this action');
        return;
    }
    
    if (xhr.status === 404) {
        showError('The requested resource was not found');
        return;
    }
    
    if (xhr.status >= 500) {
        showError('A server error occurred. Please try again later.');
        return;
    }
    
    console.error('AJAX error:', settings.url, error);
});

Display Errors from Server

// Controller returning JSON errors
[HttpPost]
public IActionResult Process(ProcessRequest request)
{
    try
    {
        // Process...
        return Json(new { success = true });
    }
    catch (ValidationException ex)
    {
        return BadRequest(new { 
            success = false, 
            errors = ex.Errors 
        });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Processing failed");
        return StatusCode(500, new { 
            success = false, 
            message = "An unexpected error occurred" 
        });
    }
}
$.ajax({
    url: '/api/process',
    type: 'POST',
    data: formData,
    success: function(response) {
        if (response.success) {
            showSuccess('Operation completed');
        }
    },
    error: function(xhr) {
        if (xhr.responseJSON) {
            if (xhr.responseJSON.errors) {
                displayFieldErrors(xhr.responseJSON.errors);
            } else if (xhr.responseJSON.message) {
                showError(xhr.responseJSON.message);
            }
        } else {
            showError('An error occurred');
        }
    }
});

Session and State Management

Server-Side Session

// Store in session
HttpContext.Session.SetString("UserPreference", "dark");
HttpContext.Session.SetInt32("CartCount", 5);

// Complex objects
HttpContext.Session.SetString("Cart", JsonSerializer.Serialize(cart));

// Retrieve from session
var preference = HttpContext.Session.GetString("UserPreference");
var cartCount = HttpContext.Session.GetInt32("CartCount");
var cart = JsonSerializer.Deserialize<Cart>(HttpContext.Session.GetString("Cart"));

Client-Side with Cookies

// Set cookie
function setCookie(name, value, days) {
    var expires = '';
    if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = '; expires=' + date.toUTCString();
    }
    document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/';
}

// Get cookie
function getCookie(name) {
    var nameEQ = name + '=';
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
        var cookie = cookies[i].trim();
        if (cookie.indexOf(nameEQ) === 0) {
            return decodeURIComponent(cookie.substring(nameEQ.length));
        }
    }
    return null;
}

// Delete cookie
function deleteCookie(name) {
    setCookie(name, '', -1);
}

Notification/Toast Messages

JavaScript Toast System

var MYAPP = MYAPP || {};

MYAPP.toast = (function($) {
    var $container;
    
    function init() {
        $container = $('<div id="toast-container"></div>').appendTo('body');
    }
    
    function show(message, type, duration) {
        type = type || 'info';
        duration = duration || 5000;
        
        var $toast = $('<div class="toast toast-' + type + '">' + 
            '<span class="toast-message">' + message + '</span>' +
            '<button class="toast-close">&times;</button>' +
            '</div>');
        
        $container.append($toast);
        
        setTimeout(function() {
            $toast.addClass('show');
        }, 10);
        
        var timer = setTimeout(function() {
            remove($toast);
        }, duration);
        
        $toast.find('.toast-close').on('click', function() {
            clearTimeout(timer);
            remove($toast);
        });
    }
    
    function remove($toast) {
        $toast.removeClass('show');
        setTimeout(function() {
            $toast.remove();
        }, 300);
    }
    
    function success(message) { show(message, 'success'); }
    function error(message) { show(message, 'error'); }
    function warning(message) { show(message, 'warning'); }
    function info(message) { show(message, 'info'); }
    
    return {
        init: init,
        show: show,
        success: success,
        error: error,
        warning: warning,
        info: info
    };
})(jQuery);

$(document).ready(function() {
    MYAPP.toast.init();
});

CSS for Toasts

#toast-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 9999;
}

.toast {
    min-width: 300px;
    padding: 15px 40px 15px 15px;
    margin-bottom: 10px;
    border-radius: 4px;
    opacity: 0;
    transform: translateX(100%);
    transition: all 0.3s ease;
    
    &.show {
        opacity: 1;
        transform: translateX(0);
    }
    
    &-success { background: #28a745; color: white; }
    &-error { background: #dc3545; color: white; }
    &-warning { background: #ffc107; color: #333; }
    &-info { background: #17a2b8; color: white; }
    
    &-close {
        position: absolute;
        top: 10px;
        right: 10px;
        background: none;
        border: none;
        color: inherit;
        font-size: 20px;
        cursor: pointer;
        opacity: 0.7;
        
        &:hover { opacity: 1; }
    }
}
Weekly Installs
3
GitHub Stars
1
First Seen
Mar 1, 2026
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3