Add project

This commit is contained in:
aloslider
2026-07-06 09:32:44 +03:00
parent 2ff2bb67f2
commit 1af22c39db
29 changed files with 1048 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc;
namespace Web.Controllers;
public class HomeController : Controller
{
public IActionResult Index() =>
RedirectToAction("Create", "OrdersPage");
}
+38
View File
@@ -0,0 +1,38 @@
using Web.Models;
using Microsoft.AspNetCore.Mvc;
using Web.Services;
namespace Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class OrdersController(IOrdersService os) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IEnumerable<Order>>> GetOrders()
{
Order[] orders = await os.GetOrdersAsync();
return Ok(orders);
}
[HttpGet("{id}")]
public async Task<ActionResult<Order>> GetOrder(Guid id)
{
Order? order = await os.GetOrderAsync(id);
if (order is null)
return NotFound();
return Ok(order);
}
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder(CreateOrderDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
Order order = await os.CreateOrderAsync(dto);
return Ok(order);
}
}
@@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Mvc;
using Web.Models;
using Web.Services;
namespace Web.Controllers;
public class OrdersPageController(IOrdersService ordersService) : Controller
{
[HttpGet]
public IActionResult Create() =>
View(new CreateOrderViewModel());
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateOrderViewModel model)
{
if (!ModelState.IsValid)
return View(model);
CreateOrderDto dto = new()
{
SenderCity = model.SenderCity,
SenderAddress = model.SenderAddress,
RecipientCity = model.RecipientCity,
RecipientAddress = model.RecipientAddress,
CargoWeight = model.CargoWeight,
PickupDate = new DateTimeOffset(model.PickupDate.Date, TimeSpan.Zero)
};
Order order = await ordersService.CreateOrderAsync(dto);
return RedirectToAction(nameof(Details), new { id = order.OrderNumber });
}
[HttpGet]
public async Task<IActionResult> List()
{
Order[] orders = await ordersService.GetOrdersAsync();
return View(orders);
}
[HttpGet]
public async Task<IActionResult> Details(Guid id)
{
Order? order = await ordersService.GetOrderAsync(id);
if (order is null)
return NotFound();
return View(order);
}
}
+45
View File
@@ -0,0 +1,45 @@
using Web.Models;
using Microsoft.EntityFrameworkCore;
namespace Web.Data;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Order> Orders { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Order>(entity =>
{
entity.HasKey(e => e.OrderNumber);
entity.Property(e => e.SenderCity)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.SenderAddress)
.IsRequired()
.HasMaxLength(200);
entity.Property(e => e.RecipientCity)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.RecipientAddress)
.IsRequired()
.HasMaxLength(200);
entity.Property(e => e.CargoWeight)
.IsRequired()
.HasPrecision(10, 2);
entity.Property(e => e.PickupDate)
.IsRequired();
entity.Property(e => e.CreatedAt)
.IsRequired();
});
}
}
+11
View File
@@ -0,0 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["Web.csproj", "."]
RUN dotnet restore "Web.csproj"
COPY . .
RUN dotnet publish "Web.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Web.dll"]
@@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Web.Data;
using Web.Services;
namespace Web.Extensions;
internal static class IServiceCollectionExtensions
{
public static IServiceCollection AddInternalServices(this IServiceCollection services)
{
services.AddScoped<IOrdersService, OrdersService>();
return services;
}
public static IServiceCollection AddDbAccess(this IServiceCollection services, IConfiguration configuration)
{
string connectionString = configuration.GetConnectionString("DbContext")
?? throw new InvalidOperationException("DB connection string is not set");
services.AddDbContext<AppDbContext>(opts => opts.UseNpgsql(connectionString));
return services;
}
}
+71
View File
@@ -0,0 +1,71 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Web.Data;
#nullable disable
namespace Web.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260706073156_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Web.Models.Order", b =>
{
b.Property<Guid>("OrderNumber")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CargoWeight")
.HasPrecision(10, 2)
.HasColumnType("numeric(10,2)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("PickupDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("RecipientAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("RecipientCity")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SenderAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("SenderCity")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("OrderNumber");
b.ToTable("Orders");
});
#pragma warning restore 612, 618
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Web.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
OrderNumber = table.Column<Guid>(type: "uuid", nullable: false),
SenderCity = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
SenderAddress = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
RecipientCity = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
RecipientAddress = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
CargoWeight = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
PickupDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.OrderNumber);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Orders");
}
}
}
@@ -0,0 +1,68 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Web.Data;
#nullable disable
namespace Web.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Web.Models.Order", b =>
{
b.Property<Guid>("OrderNumber")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("CargoWeight")
.HasPrecision(10, 2)
.HasColumnType("numeric(10,2)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("PickupDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("RecipientAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("RecipientCity")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("SenderAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("SenderCity")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.HasKey("OrderNumber");
b.ToTable("Orders");
});
#pragma warning restore 612, 618
}
}
}
+29
View File
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
namespace Web.Models;
public class CreateOrderDto
{
[Required]
[MaxLength(100)]
public string SenderCity { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string SenderAddress { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
public string RecipientCity { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string RecipientAddress { get; set; } = string.Empty;
[Required]
[Range(0.1, 100, ErrorMessage = "Weight must be between 0.1 and 100 kg")]
public decimal CargoWeight { get; set; }
[Required]
public DateTimeOffset PickupDate { get; set; }
}
+36
View File
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;
namespace Web.Models;
public class CreateOrderViewModel
{
[Required(ErrorMessage = "Укажите город отправителя")]
[MaxLength(100)]
[Display(Name = "Город отправителя")]
public string SenderCity { get; set; } = string.Empty;
[Required(ErrorMessage = "Укажите адрес отправителя")]
[MaxLength(200)]
[Display(Name = "Адрес отправителя")]
public string SenderAddress { get; set; } = string.Empty;
[Required(ErrorMessage = "Укажите город получателя")]
[MaxLength(100)]
[Display(Name = "Город получателя")]
public string RecipientCity { get; set; } = string.Empty;
[Required(ErrorMessage = "Укажите адрес получателя")]
[MaxLength(200)]
[Display(Name = "Адрес получателя")]
public string RecipientAddress { get; set; } = string.Empty;
[Required(ErrorMessage = "Укажите вес груза")]
[Range(0.1, 100, ErrorMessage = "Вес должен быть от 0,1 до 100 кг")]
[Display(Name = "Вес груза, кг")]
public decimal CargoWeight { get; set; }
[Required(ErrorMessage = "Укажите дату забора груза")]
[DataType(DataType.Date)]
[Display(Name = "Дата забора груза")]
public DateTime PickupDate { get; set; } = DateTime.Today;
}
+33
View File
@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
namespace Web.Models;
public class Order
{
public Guid OrderNumber { get; set; } = Guid.Empty;
[Required]
[MaxLength(100)]
public string SenderCity { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string SenderAddress { get; set; } = string.Empty;
[Required]
[MaxLength(100)]
public string RecipientCity { get; set; } = string.Empty;
[Required]
[MaxLength(200)]
public string RecipientAddress { get; set; } = string.Empty;
[Required]
[Range(0.1, 100)]
public decimal CargoWeight { get; set; }
[Required]
public DateTimeOffset PickupDate { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
+36
View File
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Web.Data;
using Web.Extensions;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
services.AddControllersWithViews();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddDbAccess(configuration);
services.AddInternalServices();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
dbContext.Database.Migrate();
}
app.UseRouting();
app.UseStaticFiles();
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapControllers();
app.Run();
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5280",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+50
View File
@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Web.Data;
using Web.Models;
namespace Web.Services;
public interface IOrdersService
{
Task<Order?> GetOrderAsync(Guid id);
Task<Order[]> GetOrdersAsync();
Task<Order> CreateOrderAsync(CreateOrderDto dto);
}
internal class OrdersService(AppDbContext dbCtx) : IOrdersService
{
public async Task<Order?> GetOrderAsync(Guid id)
{
Order? order = await dbCtx.Orders.FindAsync(id);
return order;
}
public async Task<Order[]> GetOrdersAsync()
{
Order[] orders =
await dbCtx.Orders
.OrderByDescending(o => o.CreatedAt)
.ToArrayAsync();
return orders;
}
public async Task<Order> CreateOrderAsync(CreateOrderDto dto)
{
Order order = new()
{
OrderNumber = Guid.NewGuid(),
SenderCity = dto.SenderCity,
SenderAddress = dto.SenderAddress,
RecipientCity = dto.RecipientCity,
RecipientAddress = dto.RecipientAddress,
CargoWeight = dto.CargoWeight,
PickupDate = dto.PickupDate,
CreatedAt = DateTime.UtcNow
};
EntityEntry<Order> created = dbCtx.Orders.Add(order);
await dbCtx.SaveChangesAsync();
return created.Entity;
}
}
+53
View File
@@ -0,0 +1,53 @@
@model CreateOrderViewModel
@{
ViewData["Title"] = "Новый заказ";
}
<section class="card">
<h1>Форма создания заказа</h1>
<form asp-action="Create" method="post" class="form" novalidate>
@Html.AntiForgeryToken()
<div class="form__grid">
<div class="field">
<label asp-for="SenderCity"></label>
<input asp-for="SenderCity" />
<span asp-validation-for="SenderCity" class="field__error"></span>
</div>
<div class="field">
<label asp-for="SenderAddress"></label>
<input asp-for="SenderAddress" />
<span asp-validation-for="SenderAddress" class="field__error"></span>
</div>
<div class="field">
<label asp-for="RecipientCity"></label>
<input asp-for="RecipientCity" />
<span asp-validation-for="RecipientCity" class="field__error"></span>
</div>
<div class="field">
<label asp-for="RecipientAddress"></label>
<input asp-for="RecipientAddress" />
<span asp-validation-for="RecipientAddress" class="field__error"></span>
</div>
<div class="field">
<label asp-for="CargoWeight"></label>
<input asp-for="CargoWeight" type="number" min="0.1" max="100" step="0.1" />
<span asp-validation-for="CargoWeight" class="field__error"></span>
</div>
<div class="field">
<label asp-for="PickupDate"></label>
<input asp-for="PickupDate" type="date" />
<span asp-validation-for="PickupDate" class="field__error"></span>
</div>
</div>
<div class="form__actions">
<button type="submit" class="button">Создать заказ</button>
</div>
</form>
</section>
+49
View File
@@ -0,0 +1,49 @@
@model Order
@{
ViewData["Title"] = "Просмотр заказа";
}
<section class="card">
<div class="page-header">
<div>
<h1>Просмотр заказа</h1>
<p class="hint">Режим только для чтения</p>
</div>
<a class="button button--secondary" href="@Url.Action("List", "OrdersPage")">К списку</a>
</div>
<dl class="details">
<div class="details__item">
<dt>Номер заказа</dt>
<dd>@Model.OrderNumber</dd>
</div>
<div class="details__item">
<dt>Город отправителя</dt>
<dd>@Model.SenderCity</dd>
</div>
<div class="details__item">
<dt>Адрес отправителя</dt>
<dd>@Model.SenderAddress</dd>
</div>
<div class="details__item">
<dt>Город получателя</dt>
<dd>@Model.RecipientCity</dd>
</div>
<div class="details__item">
<dt>Адрес получателя</dt>
<dd>@Model.RecipientAddress</dd>
</div>
<div class="details__item">
<dt>Вес груза</dt>
<dd>@Model.CargoWeight.ToString("0.##") кг</dd>
</div>
<div class="details__item">
<dt>Дата забора груза</dt>
<dd>@Model.PickupDate.ToString("dd.MM.yyyy")</dd>
</div>
<div class="details__item">
<dt>Дата создания</dt>
<dd>@Model.CreatedAt.ToString("dd.MM.yyyy HH:mm")</dd>
</div>
</dl>
</section>
+57
View File
@@ -0,0 +1,57 @@
@model Order[]
@{
ViewData["Title"] = "Список заказов";
}
<section class="card">
<div class="page-header">
<div>
<h1>Список заказов</h1>
<p class="hint">Нажмите на номер заказа, чтобы открыть подробности</p>
</div>
<a class="button button--secondary" href="@Url.Action("Create", "OrdersPage")">Создать заказ</a>
</div>
@if (Model.Length == 0)
{
<p class="empty">Заказов пока нет.</p>
}
else
{
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Номер заказа</th>
<th>Отправитель</th>
<th>Получатель</th>
<th>Вес</th>
<th>Дата забора</th>
</tr>
</thead>
<tbody>
@foreach (Order order in Model)
{
<tr>
<td>
<a href="@Url.Action("Details", "OrdersPage", new { id = order.OrderNumber })">
@order.OrderNumber
</a>
</td>
<td>
<strong>@order.SenderCity</strong>
<span>@order.SenderAddress</span>
</td>
<td>
<strong>@order.RecipientCity</strong>
<span>@order.RecipientAddress</span>
</td>
<td>@order.CargoWeight.ToString("0.##") кг</td>
<td>@order.PickupDate.ToString("dd.MM.yyyy")</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] — Versta test</title>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
<header class="header">
<div class="container header__inner">
<a class="brand" href="/">Versta test</a>
<nav class="nav">
<a href="@Url.Action("Create", "OrdersPage")">Новый заказ</a>
<a href="@Url.Action("List", "OrdersPage")">Список заказов</a>
<a href="/swagger">API</a>
</nav>
</div>
</header>
<main class="container main">
@RenderBody()
</main>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
@using Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+3
View File
@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}
+24
View File
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableOpenApiSourceGeneration>false</EnableOpenApiSourceGeneration>
<OpenApiGenerateXmlComments>false</OpenApiGenerateXmlComments>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
</ItemGroup>
</Project>
+12
View File
@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DbContext": "Host=localhost;Port=5432;Database=versta-db;Username=versta-user;Password=versta"
},
"AllowedHosts": "*"
}
+198
View File
@@ -0,0 +1,198 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.5;
color: #1a1a1a;
background: #f5f5f5;
}
a {
color: #1d4ed8;
}
.container {
width: min(960px, calc(100% - 2rem));
margin: 0 auto;
}
.header {
background: #fff;
border-bottom: 1px solid #e5e7eb;
}
.header__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 0;
}
.brand {
font-weight: 700;
color: inherit;
text-decoration: none;
}
.nav {
display: flex;
gap: 1rem;
}
.nav a {
color: #374151;
text-decoration: none;
}
.nav a:hover {
color: #111827;
}
.main {
padding: 2rem 0;
}
.card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1.5rem;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
h1 {
margin: 0 0 0.25rem;
font-size: 1.5rem;
}
.hint,
.empty {
margin: 0;
color: #6b7280;
}
.form__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.field label {
font-weight: 600;
}
.field input {
padding: 0.6rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
}
.field input:focus {
outline: 2px solid #93c5fd;
border-color: #60a5fa;
}
.field__error {
color: #b91c1c;
font-size: 0.875rem;
}
.form__actions {
margin-top: 1.25rem;
}
.button {
display: inline-block;
padding: 0.65rem 1rem;
border: none;
border-radius: 6px;
background: #1d4ed8;
color: #fff;
text-decoration: none;
cursor: pointer;
}
.button--secondary {
background: #e5e7eb;
color: #111827;
}
.table-wrap {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
border-bottom: 1px solid #e5e7eb;
text-align: left;
vertical-align: top;
}
.table td strong {
display: block;
}
.table td span {
display: block;
color: #6b7280;
font-size: 0.9rem;
}
.details {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin: 0;
}
.details__item {
padding: 0.75rem;
background: #f9fafb;
border-radius: 6px;
}
.details dt {
margin: 0 0 0.25rem;
font-size: 0.8rem;
color: #6b7280;
}
.details dd {
margin: 0;
font-weight: 600;
word-break: break-word;
}
@media (max-width: 700px) {
.form__grid,
.details,
.page-header,
.header__inner {
grid-template-columns: 1fr;
flex-direction: column;
align-items: stretch;
}
}
+8
View File
@@ -0,0 +1,8 @@
document.querySelectorAll('form[novalidate]').forEach((form) => {
form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
event.preventDefault();
form.reportValidity();
}
});
});