Лабораторная работа 1: каркас ASP.NET Core MVC (ТехноМир)

This commit is contained in:
Борисов Иван Сергеевич 2026-09-14 01:49:15 +05:00
commit de01dbf78e
80 changed files with 75563 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# файлы, которые не нужно коммитить в Git
# это стандартный .gitignore для ASP.NET Core проектов
# папки с результатами сборки
[Bb]in/
[Oo]bj/
# папки пакетов NuGet
*.nuget/
# файлы настроек IDE
.vs/
.idea/
*.suo
*.user
*.userosscache
*.sln.docstates
# логи и временные файлы
*.log
*.tmp
# файлы тестовой базы данных
*.db
*.db-journal

View File

@ -0,0 +1,20 @@
// подключаем базовые классы MVC
using Microsoft.AspNetCore.Mvc;
// подключаем модель ошибки
using TechnoWorld.Models;
// контроллер каталога — отвечает за страницу с карточками товаров
namespace TechnoWorld.Controllers;
public class CatalogController : Controller
{
// метод Index — страница каталога
// вызывается когда пользователь заходит на /Catalog или /Catalog/Index
public IActionResult Index()
{
// ставим заголовок страницы
ViewData["Title"] = "Каталог";
// рендерим представление Views/Catalog/Index.cshtml
return View();
}
}

View File

@ -0,0 +1,98 @@
// подключаем Activity — он нужен для Diagnosis-страницы (чтобы узнать ID запроса)
using System.Diagnostics;
// подключаем ASP.NET MVC — здесь Controller, IActionResult и другие классы
using Microsoft.AspNetCore.Mvc;
// подключаем наши модели — ErrorViewModel и ContactViewModel
using TechnoWorld.Models;
// пространство имён нашего проекта — все контроллеры лежат в папке Controllers
namespace TechnoWorld.Controllers;
// главный контроллер — отвечает за Most страниц: главную, о системе, контакты и ошибки
public class HomeController : Controller
{
// логгер — через него можно писать сообщения в консоль/логи для отладки
private readonly ILogger<HomeController> _logger;
// конструктор — сюда ASP.NET сам подставит логгер (внедрение зависимостей, dependency injection)
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
// метод Index — это главная страница сайта
// когда кто-то заходит на localhost:5000/ — вызывается именно этот метод
public IActionResult Index()
{
// передаём в представление, что заголовок страницы — "Главная"
ViewData["Title"] = "Главная";
// возвращаем представление (страницу Views/Home/Index.cshtml)
return View();
}
// метод About — страница "О системе"
public IActionResult About()
{
// ставим заголовок страницы
ViewData["Title"] = "О системе";
// рендерим представление Views/Home/About.cshtml
return View();
}
// метод Contacts — страница "Контакты" с формой обратной связи
[HttpGet] // эта страница открывается обычным GET-запросом (просто открыли ссылку)
public IActionResult Contacts()
{
// заголовок страницы
ViewData["Title"] = "Контакты";
// рендерим представление Views/Home/Contacts.cshtml
return View();
}
// перегрузка Contacts для POST — когда пользователь отправил форму
// атрибут [HttpPost] говорит: этот метод обрабатывает POST-запросы
[HttpPost]
// принимаем модель формы — ContactViewModel (там имя, телефон, сообщение)
public IActionResult Contacts(ContactViewModel model)
{
// проверяем, заполнил ли пользователь все обязательные поля
if (ModelState.IsValid)
{
// если форма заполнена нормально — сохраняем сообщение в TempData
// TempData — это временная переменная, которая живёт один редирект
// потом покажем пользователю "Ваше сообщение принято"
TempData["ContactSuccess"] = "Ваше сообщение принято! Мы свяжемся с вами в ближайшее время.";
// перенаправляем обратно на страницу Contacts (PRG-паттерн — Post/Redirect/Get)
// это нужно, чтобы при обновлении страницы форма не отправилась заново
return RedirectToAction(nameof(Contacts));
}
// если валидация не прошла — показываем форму снова с ошибками
ViewData["Title"] = "Контакты";
return View(model);
}
// метод Error — страница ошибок (404, 500 и т.д.)
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
// отключаем кеширование этой страницы — чтобы браузер не запоминал ошибку
public IActionResult Error(int? statusCode = null)
{
// создаём модель ошибки и передаём туда ID текущего запроса (чтобы можно было найти в логах)
var model = new ErrorViewModel
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier,
// передаём код ошибки (404, 500 и т.д.) — он приходит из UseStatusCodePagesWithReExecute
StatusCode = statusCode
};
// передаём заголовок в зависимости от кода ошибки
if (statusCode == 404)
ViewData["Title"] = "Страница не найдена";
else
ViewData["Title"] = "Ошибка";
// рендерим представление Views/Shared/Error.cshtml
return View(model);
}
}

View File

@ -0,0 +1,20 @@
// пространство имён моделей — все модели лежат в папке Models
namespace TechnoWorld.Models;
// модель для формы обратной связи
// когда пользователь заполняет форму на странице "Контакты" — данные попадают сюда
public class ContactViewModel
{
// имя пользователя — обязательное поле
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Введите ваше имя")]
public string Name { get; set; } = string.Empty;
// телефон — тоже обязательный, добавил атрибут телефона для валидации на клиенте
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Введите телефон")]
[System.ComponentModel.DataAnnotations.Phone(ErrorMessage = "Некорректный номер телефона")]
public string Phone { get; set; } = string.Empty;
// сообщение — обязательное поле
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Введите сообщение")]
public string Message { get; set; } = string.Empty;
}

16
Models/ErrorViewModel.cs Normal file
View File

@ -0,0 +1,16 @@
// пространство имён моделей
namespace TechnoWorld.Models;
// расширяем ErrorViewModel — добавляем код ошибки (404, 500 и т.д.)
// этот класс наследуется от базового ErrorViewModel, который уже есть в шаблоне
public class ErrorViewModel
{
// ID запроса — нужен для отладки, чтобы найти конкретный запрос в логах
public string? RequestId { get; set; }
// свойство — показывать ли ID запроса на странице (если не пустой, то показываем)
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
// код HTTP-ошибки (404, 500 и т.д.) — передаётся из контроллера
public int? StatusCode { get; set; }
}

44
Program.cs Normal file
View File

@ -0,0 +1,44 @@
// подключаем пространство имён Microsoft.AspNetCore для базовых классов
using Microsoft.AspNetCore.Mvc;
// создаём билдер — через него регистрируем сервисы и настраиваем приложение
var builder = WebApplication.CreateBuilder(args);
// регистрируем сервисы для MVC — контроллеры, представления, tag helpers
builder.Services.AddControllersWithViews();
// собираем приложение из зарегистрированных сервисов
var app = builder.Build();
// проверяем, не в продакшене ли мы
if (!app.Environment.IsDevelopment())
{
// в продакшене перенаправляем ошибки на страницу Error
app.UseExceptionHandler("/Home/Error");
// HSTS — браузер будет ходить только по HTTPS (для безопасности)
app.UseHsts();
}
// редирект с HTTP на HTTPS — чтобы всё шло по защищённому каналу
app.UseHttpsRedirection();
// раздаём статические файлы из папки wwwroot (css, js, картинки и т.д.)
app.UseStaticFiles();
// включаем маршрутизацию — приложение умеет понимать URL-ы
app.UseRouting();
// пока авторизации нет, но middleware уже на месте (по заданию она будет в ЛР 6)
app.UseAuthorization();
// описание страницы 404 — если страница не найдена, покажем красивую ошибку
app.UseStatusCodePagesWithReExecute("/Home/Error", "?statusCode={0}");
// задаём маршрут по умолчанию: /{controller}/{action}/{id?}
// то есть localhost:5000/Home/Index → HomeController → метод Index
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// запускаем приложение — оно начинает слушать HTTP-запросы
app.Run();

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:2153",
"sslPort": 44385
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5218",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7287;http://localhost:5218",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# ТехноМир — Лабораторная работа №1
## Информация об авторе
- **ФИО:** Борисов Иван Сергеевич
- **Группа:** РПО 23/1
- **Год:** 2026
## Описание проекта
Интернет-магазин техники «ТехноМир» — веб-приложение на ASP.NET Core MVC.
Создано в рамках лабораторной работы №1 по дисциплине "Разработка программного обеспечения".
### Реализованные страницы
| Страница | Описание |
|----------------|------------------------------------------------------------------|
| **Главная** | Баннер, описание сервиса, 3 преимущества, кнопка «Перейти в каталог» |
| **Каталог** | 4 карточки товаров-заглушки (ноутбук, смартфон, планшет, наушники) |
| **О системе** | Описание сервиса, роли пользователей (Гость, Покупатель, Админ) |
| **Контакты** | Адрес, телефон, форма обратной связи с валидацией |
| **404** | Красивая страница ошибки с ссылкой на главную |
### Технологии
- ASP.NET Core 8.0 (MVC)
- Bootstrap 5.3 (CDN)
- Кастомная CSS-тема с переключением светлой/тёмной темы
- Tag Helpers
- TempData для сообщений
- Адаптивная вёрстка (1200 px и 375 px)
## Как запустить
1. Установить [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
2. Открыть терминал в папке проекта
3. Выполнить команду:
```bash
dotnet run
```
4. Открыть в браузере: `https://localhost:5000` или `http://localhost:5000`
## Структура проекта
```
TechnoWorld/
├── Controllers/
│ ├── HomeController.cs — главная, о системе, контакты, ошибки
│ └── CatalogController.cs — каталог товаров
├── Models/
│ ├── ErrorViewModel.cs — модель ошибки
│ └── ContactViewModel.cs — модель формы обратной связи
├── Views/
│ ├── Home/
│ │ ├── Index.cshtml — главная страница
│ │ ├── About.cshtml — о системе
│ │ └── Contacts.cshtml — контакты с формой
│ ├── Catalog/
│ │ └── Index.cshtml — каталог товаров
│ └── Shared/
│ ├── _Layout.cshtml — общий макет (навбар + футер)
│ └── Error.cshtml — страница ошибки 404
├── wwwroot/
│ ├── css/site.css — кастомные стили + тёмная тема
│ └── js/site.js — скрипт переключения темы
├── Program.cs — точка входа приложения
└── TechnoWorld.csproj — файл проекта
```

9
TechnoWorld.csproj Normal file
View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

100
Views/Catalog/Index.cshtml Normal file
View File

@ -0,0 +1,100 @@
@{
ViewData["Title"] = "Каталог";
}
@* заголовок страницы *@
<h1 class="mb-4">Каталог товаров</h1>
@* описание — по заданию: 4 карточки-заглушки *@
<p class="text-muted mb-4">
Выберите товар для подробной информации. Пока что это заглушки —
позже карточки будут заполняться данными из базы.
</p>
@* строка с четырьмя карточками *@
@* g-4 — отступы между колонками 1.5rem *@
<div class="row g-4">
@* === КАРТОЧКА 1: НОУТБУК === *@
@* col-lg-3 — на больших экранах 4 карточки в ряд (3/12 = 25%) *@
@* col-md-6 — на средних — 2 в ряд *@
@* col-12 — на маленьких — 1 в ряд *@
<div class="col-lg-3 col-md-6 col-12">
<div class="card h-100 shadow-sm product-card">
@* заглушка для картинки — вместо реального фото просто серый блок *@
<div class="card-img-top placeholder-img d-flex align-items-center justify-content-center">
<span class="fs-1">&#128187;</span>
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title fw-bold">Ноутбук ProBook 15</h5>
<p class="card-text text-muted small flex-grow-1">
15.6", Intel Core i5, 8 ГБ RAM, 512 ГБ SSD
</p>
<div class="d-flex justify-content-between align-items-center mt-2">
@* цена *@
<span class="fw-bold fs-5 text-primary">45 990 &#8381;</span>
@* кнопка "Подробнее" — заглушка *@
<a href="#" class="btn btn-outline-primary btn-sm">Подробнее</a>
</div>
</div>
</div>
</div>
@* === КАРТОЧКА 2: СМАРТФОН === *@
<div class="col-lg-3 col-md-6 col-12">
<div class="card h-100 shadow-sm product-card">
<div class="card-img-top placeholder-img d-flex align-items-center justify-content-center">
<span class="fs-1">&#128241;</span>
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title fw-bold">Смартфон Nova X</h5>
<p class="card-text text-muted small flex-grow-1">
6.5", AMOLED, 128 ГБ, 48 Мп камера
</p>
<div class="d-flex justify-content-between align-items-center mt-2">
<span class="fw-bold fs-5 text-primary">29 990 &#8381;</span>
<a href="#" class="btn btn-outline-primary btn-sm">Подробнее</a>
</div>
</div>
</div>
</div>
@* === КАРТОЧКА 3: ПЛАНШЕТ === *@
<div class="col-lg-3 col-md-6 col-12">
<div class="card h-100 shadow-sm product-card">
<div class="card-img-top placeholder-img d-flex align-items-center justify-content-center">
<span class="fs-1">&#128246;</span>
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title fw-bold">Планшет Tab Lite</h5>
<p class="card-text text-muted small flex-grow-1">
10.1", 64 ГБ, Wi-Fi, идеален для учёбы
</p>
<div class="d-flex justify-content-between align-items-center mt-2">
<span class="fw-bold fs-5 text-primary">18 490 &#8381;</span>
<a href="#" class="btn btn-outline-primary btn-sm">Подробнее</a>
</div>
</div>
</div>
</div>
@* === КАРТОЧКА 4: НАУШНИКИ === *@
<div class="col-lg-3 col-md-6 col-12">
<div class="card h-100 shadow-sm product-card">
<div class="card-img-top placeholder-img d-flex align-items-center justify-content-center">
<span class="fs-1">&#127911;</span>
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title fw-bold">Наушники SoundMax</h5>
<p class="card-text text-muted small flex-grow-1">
Накладные, Bluetooth 5.0, шумоподавление, 40 ч
</p>
<div class="d-flex justify-content-between align-items-center mt-2">
<span class="fw-bold fs-5 text-primary">3 990 &#8381;</span>
<a href="#" class="btn btn-outline-primary btn-sm">Подробнее</a>
</div>
</div>
</div>
</div>
</div>

61
Views/Home/About.cshtml Normal file
View File

@ -0,0 +1,61 @@
@{
ViewData["Title"] = "О системе";
}
@* заголовок страницы *@
<h1 class="mb-4">О системе «ТехноМир»</h1>
@* описание для кого сервис *@
<section class="mb-4">
<h3>Для кого этот сервис?</h3>
<p>
«ТехноМир» — это интернет-магазин компьютерной и мобильной техники.
Сервис предназначен для всех, кто хочет купить качественную технику
по доступным ценам: от обычных покупателей до компаний, которые закупают
технику для сотрудников.
</p>
</section>
@* описание ролей *@
<section class="mb-4">
<h3>Роли пользователей</h3>
@* таблица с ролями — сделана через Bootstrap table *@
<table class="table table-bordered table-hover">
@* шапка таблицы *@
<thead class="table-primary">
<tr>
<th>Роль</th>
<th>Описание</th>
</tr>
</thead>
<tbody>
@* строка — Гость *@
<tr>
<td><strong>Гость</strong></td>
<td>Пользователь без аккаунта. Может смотреть каталог и главную страницу.</td>
</tr>
@* строка — Покупатель *@
<tr>
<td><strong>Покупатель</strong></td>
<td>Зарегистрированный пользователь. Может добавлять товары в корзину и оформлять заказы.</td>
</tr>
@* строка — Администратор *@
<tr>
<td><strong>Администратор</strong></td>
<td>Управляет каталогом: добавляет, редактирует и удаляет товары. Управляет заказами.</td>
</tr>
</tbody>
</table>
</section>
@* дополнительная информация *@
<section>
<h3>Что планируется</h3>
<ul>
<li>Каталог товаров с фильтрацией и сортировкой (уже готово в базовой форме)</li>
<li>Корзина и оформление заказов</li>
<li>Личный кабинет покупателя</li>
<li>Панель администратора для управления товарами</li>
<li>Система авторизации и аутентификации (ЛР 6)</li>
</ul>
</section>

View File

@ -0,0 +1,73 @@
@* подключаем модель ContactViewModel — она нужна для Tag Helpers (asp-for) *@
@model ContactViewModel
@{
ViewData["Title"] = "Контакты";
}
@* заголовок страницы *@
<h1 class="mb-4">Контакты</h1>
@* === КОНТАКТНАЯ ИНФОРМАЦИЯ (адрес, телефон) === *@
<section class="mb-4">
<div class="row">
@* левая колонка — адрес и телефон *@
<div class="col-md-6 mb-3">
<h3>Наш адрес</h3>
<p>г. Москва, ул. Примерная, д. 42, офис 101</p>
<h3>Телефон</h3>
<p>+7 (999) 123-45-67</p>
<h3>Электронная почта</h3>
<p>info@technomir.ru</p>
</div>
@* правая колонка — форма обратной связи *@
<div class="col-md-6">
<h3>Напишите нам</h3>
@* проверяем, есть ли сообщение об успехе в TempData *@
@* TempData доступен только один раз — после чтения он удаляется *@
@if (TempData["ContactSuccess"] != null)
{
@* показываем зелёное уведомление об успехе *@
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["ContactSuccess"]
@* кнопка закрытия уведомления (крестик) *@
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Закрыть"></button>
</div>
}
@* форма обратной связи *@
@* asp-action="Contacts" — при отправке данные уйдут на метод Contacts (POST) контроллера Home *@
@* method="post" — данные отправляются POST-запросом (не в URL, а в теле запроса) *@
<form asp-action="Contacts" method="post">
@* поле "Имя" *@
<div class="mb-3">
@* label with asp-for — Tag Helper, который генерирует <label for="Name"> *@
<label asp-for="Name" class="form-label">Ваше имя</label>
@* input with asp-for — генерирует <input id="Name" name="Name" ...> *@
@* class="form-control" — стиль Bootstrap для полей ввода *@
<input asp-for="Name" class="form-control" placeholder="Иван Иванов" />
@* span для отображения ошибки валидации (если имя не заполнено) *@
<span asp-validation-for="Name" class="text-danger"></span>
</div>
@* поле "Телефон" *@
<div class="mb-3">
<label asp-for="Phone" class="form-label">Телефон</label>
<input asp-for="Phone" class="form-control" placeholder="+7 (999) 123-45-67" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
@* поле "Сообщение" *@
<div class="mb-3">
<label asp-for="Message" class="form-label">Сообщение</label>
@* textarea — многострочное поле ввода *@
<textarea asp-for="Message" class="form-control" rows="4" placeholder="Напишите ваше сообщение..."></textarea>
<span asp-validation-for="Message" class="text-danger"></span>
</div>
@* кнопка отправки формы *@
<button type="submit" class="btn btn-primary">Отправить сообщение</button>
</form>
</div>
</div>
</section>

75
Views/Home/Index.cshtml Normal file
View File

@ -0,0 +1,75 @@
@*告诉他布局使用 _Layout.cshtml*@
@{
ViewData["Title"] = "Главная";
}
@* === ГЛАВНЫЙ БАННЕР (JUMBOTRON) === *@
@* jumbotron — большой блок-баннер вверху страницы *@
<section class="hero-section text-center text-white py-5 mb-5 rounded-3">
<div class="container py-4">
@* главное заголовок сайта *@
<h1 class="display-4 fw-bold">Добро пожаловать в ТехноМир!</h1>
@* короткое описание — о чём этот сайт *@
<p class="lead mt-3 mb-4 mx-auto" style="max-width: 650px;">
Мы поможем вам найти технику по лучшим ценам.
Ноутбуки, смартфоны, планшеты и наушники — всё в одном месте.
</p>
@* кнопка-призыв к действию — перейти в каталог *@
@* asp-controller="Catalog" — ссылка на контроллер каталога *@
<a asp-controller="Catalog" asp-action="Index" class="btn btn-light btn-lg px-4">
Перейти в каталог &rarr;
</a>
</div>
</section>
@* === ТРИ ПРЕИМУЩЕСТВА === *@
@* по заданию: 3 преимущества сервиса *@
<section class="mb-5">
<h2 class="text-center mb-4">Почему выбирают нас?</h2>
@* row — строка Bootstrap, col-md-4 — три колонки по 4/12 = по трети экрана *@
@* на мобильных они встанут друг под друга (автоматически) *@
<div class="row g-4">
@* === ПРЕИМУЩЕСТВО 1 === *@
<div class="col-md-4">
<div class="card h-100 text-center border-0 shadow-sm advantage-card">
<div class="card-body py-4">
@* эмодзи-иконка *@
<div class="fs-1 mb-3">&#128293;</div>
@* заголовок карточки *@
<h5 class="card-title fw-bold">Низкие цены</h5>
@* описание *@
<p class="card-text text-muted">
Мы предлагаем лучшие цены на рынке.
Сравнивайте и убеждайтесь сами!
</p>
</div>
</div>
</div>
@* === ПРЕИМУЩЕСТВО 2 === *@
<div class="col-md-4">
<div class="card h-100 text-center border-0 shadow-sm advantage-card">
<div class="card-body py-4">
<div class="fs-1 mb-3">&#128666;</div>
<h5 class="card-title fw-bold">Быстрая доставка</h5>
<p class="card-text text-muted">
Доставим ваш заказ за 1-3 дня по всей России.
Бесплатно при заказе от 5000 рублей.
</p>
</div>
</div>
</div>
@* === ПРЕИМУЩЕСТВО 3 === *@
<div class="col-md-4">
<div class="card h-100 text-center border-0 shadow-sm advantage-card">
<div class="card-body py-4">
<div class="fs-1 mb-3">&#128736;</div>
<h5 class="card-title fw-bold">Гарантия и сервис</h5>
<p class="card-text text-muted">
На всю технику действует гарантия от 12 месяцев.
Официальный сервисный центр.
</p>
</div>
</div>
</div>
</div>
</section>

48
Views/Shared/Error.cshtml Normal file
View File

@ -0,0 +1,48 @@
@* подключаем модель ErrorViewModel — она нужна чтобы вывести ID ошибки *@
@model ErrorViewModel
@{
// ставим заголовок страницы — "Ошибка" (или "Страница не найдена", зависит от контроллера)
ViewData["Title"] = "Ошибка";
}
@* === СТРАНИЦА ОШИБКИ 404 / ДРУГИЕ ОШИБКИ === *@
<div class="text-center py-5">
@* большая эмодзи-иллюстрация *@
<div class="fs-1 mb-3">&#128533;</div>
@* проверяем, есть ли код ошибки и равен ли он 404 *@
@if (Model?.StatusCode == 404)
{
@* если 404 — показываем сообщение "Страница не найдена" *@
<h1 class="display-5 fw-bold mb-3">404 — Страница не найдена</h1>
<p class="lead text-muted mb-4">
К сожалению, такой страницы на нашем сайте нет.
Возможно, она была перемещена или удалена.
</p>
@* кнопка возврата на главную *@
<a asp-controller="Home" asp-action="Index" class="btn btn-primary btn-lg">
&larr; Вернуться на главную
</a>
}
else
{
@* для всех остальных ошибок (500 и т.д.) *@
<h1 class="display-5 fw-bold mb-3">Что-то пошло не так</h1>
<p class="lead text-muted mb-4">
Произошла ошибка при обработке вашего запроса.
Попробуйте вернуться на главную или повторить попытку позже.
</p>
<a asp-controller="Home" asp-action="Index" class="btn btn-primary btn-lg">
&larr; Вернуться на главную
</a>
}
@* показываем ID запроса — полезно при обращении в поддержку *@
@if (Model?.ShowRequestId == true)
{
<p class="text-muted mt-3 small">
ID запроса: <code>@Model.RequestId</code>
</p>
}
</div>

124
Views/Shared/_Layout.cshtml Normal file
View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="ru">
<head>
@* кодировка UTF-8 — чтобы русские буквы отображались корректно *@
<meta charset="utf-8" />
@* viewport — чтобы сайт нормально смотрелся на телефонах *@
@* width=device-width — ширина = ширина экрана, initial-scale=1.0 — без масштабирования *@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@* Open Graph — чтобы при ссылке в соцсети показывалось красивое превью *@
<meta property="og:title" content="ТехноМир — Интернет-магазин техники" />
<meta property="og:description" content="Лучшие цены на ноутбуки, смартфоны, планшеты и наушники" />
<meta property="og:type" content="website" />
@* заголовок страницы — берётся из ViewData["Title"], который задаётся в каждом представлении *@
<title>@ViewData["Title"] — ТехноМир</title>
@* подключаем Bootstrap 5 через CDN (Content Delivery Network) *@
@* это значит что CSS-файл подгружается с сервера Bootstrap, а не лежит у нас *@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YcnS/1p0KQLI8apkWKaIl2pCeM0oG6mDcK0h"
crossorigin="anonymous" />
@* подключаем наш кастомный CSS — он лежит в wwwroot/css/site.css *@
@* asp-append-version="true" — добавляет хеш к файлу, чтобы браузер перезагружал CSS при изменениях *@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
@* иконка сайта (favicon) — маленькая картинка во вкладке браузера *@
<link rel="icon" type="image/x-icon" href="~/favicon.ico" />
</head>
@* data-theme — атрибут для переключения темы (светлая/тёмная) *@
<body data-theme="light">
@* === ШАПКА САЙТА (HEADER) === *@
<header>
@* навигационная панель Bootstrap — сверху сайта *@
@* navbar-expand-sm — на маленьких экранах схлопывается в бургер *@
@* navbar-dark — тёмный стиль навбара (белый текст на тёмном фоне) *@
@* bg-primary — фон навбара — наш акцентный цвет *@
<nav class="navbar navbar-expand-sm navbar-dark bg-primary shadow-sm mb-3">
<div class="container">
@* логотип / название сайта — ссылка на главную *@
@* asp-controller="Home" asp-action="Index" — Tag Helper, который генерирует href="/" *@
<a class="navbar-brand fw-bold" asp-controller="Home" asp-action="Index">
ТехноМир
</a>
@* кнопка-бургер — появляется на маленьких экранах *@
@* data-bs-target="#mainNav" — ссылается на ID меню, которое нужно свернуть/развернуть *@
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#mainNav"
aria-controls="mainNav" aria-expanded="false" aria-label="Меню">
@* иконка-бургер (три полоски) *@
<span class="navbar-toggler-icon"></span>
</button>
@* само меню — сворачивается на маленьких экранах *@
<div class="collapse navbar-collapse" id="mainNav">
@* список ссылок навигации *@
<ul class="navbar-nav me-auto">
@* Главная *@
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="Index">Главная</a>
</li>
@* Каталог *@
<li class="nav-item">
<a class="nav-link" asp-controller="Catalog" asp-action="Index">Каталог</a>
</li>
@* О системе *@
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="About">О системе</a>
</li>
@* Контакты *@
<li class="nav-item">
<a class="nav-link" asp-controller="Home" asp-action="Contacts">Контакты</a>
</li>
</ul>
@* кнопка переключения темы — справа в навбаре *@
<button id="themeToggle" class="btn btn-outline-light btn-sm" title="Переключить тему">
@* по умолчанию показываем луну (переключиться на тёмную) *@
<span id="themeIcon">&#9790;</span>
</button>
</div>
</div>
</nav>
</header>
@* === ОСНОВНОЕ СОДЕРЖИМОЕ СТРАНИЦЫ === *@
@* контейнер — ограничивает ширину контента, чтобы на широком экране не растягивался *@
<div class="container">
@* main — основной блок, сюда подставляется содержимое текущей страницы *@
@* RenderBody() — вот сюда вставляется то, что написано в Index.cshtml, About.cshtml и т.д. *@
<main role="main" class="pb-4">
@RenderBody()
</main>
</div>
@* === ПОДВАЛ (FOOTER) === *@
@* footer — самый низ сайта *@
<footer class="footer mt-auto py-3 bg-dark text-light">
<div class="container text-center">
@* группа, ФИО и год — как просили в задании *@
<span class="text-muted">
РПО 23/1 &mdash; Борисов Иван Сергеевич &mdash; 2026
</span>
</div>
</footer>
@* === СКРИПТЫ === *@
@* подключаем Bootstrap JS-бандл (включает Popper.js — нужен для выпадающих меню и тултипов) *@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
@* наш скрипт — переключение темы *@
<script src="~/js/site.js" asp-append-version="true"></script>
@* рендерим дополнительные скрипты, если они нужны конкретной странице *@
@* required: false — секция необязательная, страницы без неё не ошибутся *@
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

View File

@ -0,0 +1,8 @@
@* подключаем пространства имён проекта — чтобы в представлениях не писать полное имя классов *@
@* например ErrorViewModel вместо TechnoWorld.Models.ErrorViewModel *@
@using TechnoWorld
@using TechnoWorld.Models
@* подключаем Tag Helpers — они нужны для asp-controller, asp-action, asp-for и других атрибутов *@
@* Tag Helpers — это специальные атрибуты, которые превращаются в HTML на сервере *@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

5
Views/_ViewStart.cshtml Normal file
View File

@ -0,0 +1,5 @@
@* этот файл говорит всем представлениям: используй макет _Layout *@
@* то есть каждый .cshtml автоматически обёрнётся в _Layout.cshtml *@
@{
Layout = "_Layout";
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

193
wwwroot/css/site.css Normal file
View File

@ -0,0 +1,193 @@
/* ============================================
КАСТОМНЫЙ CSS ДЛЯ ПРОЕКТА "ТЕХНОМИР"
Борисов Иван Сергеевич, РПО 23/1, 2026
============================================ */
/* === CSS-ПЕРЕМЕННЫЕ ДЛЯ ТЁМЫ === */
/* здесь задаём цвета для светлой и тёмной темы */
/* потом эти переменные используем в коде через var(--имя-переменной) */
:root {
/* акцентный цвет — синий */
--color-primary: #2563eb;
/* фон страницы — белый для светлой темы */
--bg-body: #f8f9fa;
/* фон карточек */
--bg-card: #ffffff;
/* цвет текста */
--text-color: #212529;
/* цвет мутного/второстепенного текста */
--text-muted: #6c757d;
/* тень карточек */
--shadow-card: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
/* тень карточки при наведении */
--shadow-card-hover: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
/* цвет бордера карточки */
--border-card: #dee2e6;
}
/* тёмная тема — меняем цвета при data-theme="dark" на body */
[data-theme="dark"] {
--bg-body: #1a1d23;
--bg-card: #2b2f38;
--text-color: #e4e6ea;
--text-muted: #adb5bd;
--shadow-card: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3);
--shadow-card-hover: 0 0.5rem 1rem rgba(0, 0, 0, 0.5);
--border-card: #444950;
}
/* === ОСНОВНЫЕ СТИЛИ === */
/* базовый размер шрифта — 14px на мобильных */
html {
font-size: 14px;
}
/* на экранах от 768px и шире — увеличиваем до 16px */
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
/* чтобы footer всегда был внизу даже на пустых страницах */
html {
position: relative;
min-height: 100%;
}
/* body с отступом снизу чтобы footer не налезал на контент */
body {
margin-bottom: 80px;
/* подставляем цвет фона из переменной темы */
background-color: var(--bg-body);
/* подставляем цвет текста */
color: var(--text-color);
/* плавный переход при смене темы — 0.3 секунды */
transition: background-color 0.3s ease, color 0.3s ease;
}
/* === СТИЛИ НАВБАРА === */
/* при тёмной теме навбар чуть темнее */
[data-theme="dark"] .navbar {
background-color: #1e2128 !important;
}
/* === ГЛАВНЫЙ БАННЕР (HERO) === */
/* секция-баннер на главной странице */
.hero-section {
/* градиентный фон — от акцентного цвета к фиолетовому */
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
/* скруглённые углы */
border-radius: 0.75rem !important;
}
/* === КАРТОЧКИ ПРЕИМУЩЕСТВ === */
/* общие стили для карточек с преимуществами */
.advantage-card {
/* меняем фон карточки на нашу переменную */
background-color: var(--bg-card);
/* меняем цвет текста */
color: var(--text-color);
/* плавный переход при смене темы */
transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
/* эффект при наведении мыши — карточка немного поднимается и тень увеличивается */
.advantage-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-card-hover);
}
/* === КАРТОЧКИ ТОВАРОВ В КАТАЛОГЕ === */
/* общие стили для карточки товара */
.product-card {
/* фон из переменной темы */
background-color: var(--bg-card);
/* цвет текста */
color: var(--text-color);
/* бордер */
border: 1px solid var(--border-card);
/* плавный переход для hover-эффекта */
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.3s ease;
}
/* при наведении — карточка поднимается и тень становится больше */
.product-card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-card-hover);
}
/* заглушка-картинка в карточке товара (серый фон с эмодзи) */
.placeholder-img {
/* высота заглушки */
height: 180px;
/* фон — светлый серый */
background-color: #e9ecef;
/* нижняя граница */
border-bottom: 1px solid #dee2e6;
}
/* в тёмной теме заглушка чуть темнее */
[data-theme="dark"] .placeholder-img {
background-color: #343a40;
border-bottom-color: #495057;
}
/* === ФОРМА КОНТАКТОВ === */
/* стилизация полей ввода в тёмной теме */
[data-theme="dark"] .form-control {
background-color: #343a40;
border-color: #495057;
color: #e4e6ea;
}
/* активное поле ввода — акцентная рамка */
[data-theme="dark"] .form-control:focus {
background-color: #3a3f49;
border-color: var(--color-primary);
color: #e4e6ea;
}
/* стилизация таблицы в тёмной теме */
[data-theme="dark"] .table {
color: #e4e6ea;
--bs-table-bg: transparent;
--bs-table-striped-bg: rgba(255, 255, 255, 0.03);
--bs-table-hover-bg: rgba(255, 255, 255, 0.05);
}
/* === FOOTER === */
/* footer привязан к низу страницы */
.footer {
/* скрепляем footer с низом */
position: absolute;
bottom: 0;
width: 100%;
/* высота строки = высота отступа body */
line-height: 60px;
}
/* === КНОПКА ПЕРЕКЛЮЧЕНИЯ ТЕМЫ === */
/* кнопка темы — небольшая, с иконкой */
#themeToggle {
/* размер шрифта */
font-size: 1.1rem;
/* убираем обводку при фокусе */
outline: none;
}
/* === УВЕДОМЛЕНИЕ ОБ УСПЕХЕ === */
/* стилизуем alert-success для тёмной темы */
[data-theme="dark"] .alert-success {
background-color: rgba(25, 135, 84, 0.2);
border-color: rgba(25, 135, 84, 0.4);
color: #75b798;
}

BIN
wwwroot/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

52
wwwroot/js/site.js Normal file
View File

@ -0,0 +1,52 @@
// ============================================
// СКРИПТ ПЕРЕКЛЮЧЕНИЯ ТЁМЫ (СВЕТЛАЯ / ТЁМНАЯ)
// Борисов Иван Сергеевич, РПО 23/1, 2026
// ============================================
// ждём пока вся страница загрузится
document.addEventListener('DOMContentLoaded', function () {
// находим кнопку переключения темы по её ID
var btn = document.getElementById('themeToggle');
// находим элемент body чтобы менять атрибут data-theme
var body = document.body;
// находим иконку внутри кнопки (луна/солнце)
var icon = document.getElementById('themeIcon');
// при загрузке страницы — проверяем в localStorage, какую тему выбрал пользователь раньше
// если там ничего нет — ставим "light" (светлая по умолчанию)
var savedTheme = localStorage.getItem('theme') || 'light';
// применяем сохранённую тему — ставим атрибут data-theme на body
body.setAttribute('data-theme', savedTheme);
// обновляем иконку: если тёмная тема — показываем солнце, если светлая — луну
updateIcon(savedTheme);
// вешаем обработчик клика на кнопку
btn.addEventListener('click', function () {
// читаем текущую тему из атрибута data-theme
var current = body.getAttribute('data-theme');
// переключаем: было light → станет dark, было dark → станет light
var next = current === 'light' ? 'dark' : 'light';
// ставим новую тему
body.setAttribute('data-theme', next);
// сохраняем выбор в localStorage — чтобы при обновлении страницы тема осталась
localStorage.setItem('theme', next);
// обновляем иконку
updateIcon(next);
});
// функция обновления иконки на кнопке
// если тема тёмная — показываем солнце (чтобы переключить на светлую)
// если тема светлая — показываем луну (чтобы переключить на тёмную)
function updateIcon(theme) {
if (theme === 'dark') {
// HTML-код солнца (☀)
icon.innerHTML = '&#9788;';
} else {
// HTML-код луны (☽)
icon.innerHTML = '&#9790;';
}
}
});

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,427 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,424 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5026
wwwroot/lib/bootstrap/dist/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) .NET Foundation and Contributors
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,435 @@
/**
* @license
* Unobtrusive validation support library for jQuery and jQuery Validate
* Copyright (c) .NET Foundation. All rights reserved.
* Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
* @version v4.0.0
*/
/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */
/*global document: false, jQuery: false */
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define("jquery.validate.unobtrusive", ['jquery-validation'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS-like environments that support module.exports
module.exports = factory(require('jquery-validation'));
} else {
// Browser global
jQuery.validator.unobtrusive = factory(jQuery);
}
}(function ($) {
var $jQval = $.validator,
adapters,
data_validation = "unobtrusiveValidation";
function setValidationValues(options, ruleName, value) {
options.rules[ruleName] = value;
if (options.message) {
options.messages[ruleName] = options.message;
}
}
function splitAndTrim(value) {
return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g);
}
function escapeAttributeValue(value) {
// As mentioned on http://api.jquery.com/category/selectors/
return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
}
function getModelPrefix(fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
}
function appendModelPrefix(value, prefix) {
if (value.indexOf("*.") === 0) {
value = value.replace("*.", prefix);
}
return value;
}
function onError(error, inputElement) { // 'this' is the form element
var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
container.removeClass("field-validation-valid").addClass("field-validation-error");
error.data("unobtrusiveContainer", container);
if (replace) {
container.empty();
error.removeClass("input-validation-error").appendTo(container);
}
else {
error.hide();
}
}
function onErrors(event, validator) { // 'this' is the form element
var container = $(this).find("[data-valmsg-summary=true]"),
list = container.find("ul");
if (list && list.length && validator.errorList.length) {
list.empty();
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");
$.each(validator.errorList, function () {
$("<li />").html(this.message).appendTo(list);
});
}
}
function onSuccess(error) { // 'this' is the form element
var container = error.data("unobtrusiveContainer");
if (container) {
var replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null;
container.addClass("field-validation-valid").removeClass("field-validation-error");
error.removeData("unobtrusiveContainer");
if (replace) {
container.empty();
}
}
}
function onReset(event) { // 'this' is the form element
var $form = $(this),
key = '__jquery_unobtrusive_validation_form_reset';
if ($form.data(key)) {
return;
}
// Set a flag that indicates we're currently resetting the form.
$form.data(key, true);
try {
$form.data("validator").resetForm();
} finally {
$form.removeData(key);
}
$form.find(".validation-summary-errors")
.addClass("validation-summary-valid")
.removeClass("validation-summary-errors");
$form.find(".field-validation-error")
.addClass("field-validation-valid")
.removeClass("field-validation-error")
.removeData("unobtrusiveContainer")
.find(">*") // If we were using valmsg-replace, get the underlying error
.removeData("unobtrusiveContainer");
}
function validationInfo(form) {
var $form = $(form),
result = $form.data(data_validation),
onResetProxy = $.proxy(onReset, form),
defaultOptions = $jQval.unobtrusive.options || {},
execInContext = function (name, args) {
var func = defaultOptions[name];
func && $.isFunction(func) && func.apply(form, args);
};
if (!result) {
result = {
options: { // options structure passed to jQuery Validate's validate() method
errorClass: defaultOptions.errorClass || "input-validation-error",
errorElement: defaultOptions.errorElement || "span",
errorPlacement: function () {
onError.apply(form, arguments);
execInContext("errorPlacement", arguments);
},
invalidHandler: function () {
onErrors.apply(form, arguments);
execInContext("invalidHandler", arguments);
},
messages: {},
rules: {},
success: function () {
onSuccess.apply(form, arguments);
execInContext("success", arguments);
}
},
attachValidation: function () {
$form
.off("reset." + data_validation, onResetProxy)
.on("reset." + data_validation, onResetProxy)
.validate(this.options);
},
validate: function () { // a validation function that is called by unobtrusive Ajax
$form.validate();
return $form.valid();
}
};
$form.data(data_validation, result);
}
return result;
}
$jQval.unobtrusive = {
adapters: [],
parseElement: function (element, skipAttach) {
/// <summary>
/// Parses a single HTML element for unobtrusive validation attributes.
/// </summary>
/// <param name="element" domElement="true">The HTML element to be parsed.</param>
/// <param name="skipAttach" type="Boolean">[Optional] true to skip attaching the
/// validation to the form. If parsing just this single element, you should specify true.
/// If parsing several elements, you should specify false, and manually attach the validation
/// to the form when you are finished. The default is false.</param>
var $element = $(element),
form = $element.parents("form")[0],
valInfo, rules, messages;
if (!form) { // Cannot do client-side validation without a form
return;
}
valInfo = validationInfo(form);
valInfo.options.rules[element.name] = rules = {};
valInfo.options.messages[element.name] = messages = {};
$.each(this.adapters, function () {
var prefix = "data-val-" + this.name,
message = $element.attr(prefix),
paramValues = {};
if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy)
prefix += "-";
$.each(this.params, function () {
paramValues[this] = $element.attr(prefix + this);
});
this.adapt({
element: element,
form: form,
message: message,
params: paramValues,
rules: rules,
messages: messages
});
}
});
$.extend(rules, { "__dummy__": true });
if (!skipAttach) {
valInfo.attachValidation();
}
},
parse: function (selector) {
/// <summary>
/// Parses all the HTML elements in the specified selector. It looks for input elements decorated
/// with the [data-val=true] attribute value and enables validation according to the data-val-*
/// attribute values.
/// </summary>
/// <param name="selector" type="String">Any valid jQuery selector.</param>
// $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one
// element with data-val=true
var $selector = $(selector),
$forms = $selector.parents()
.addBack()
.filter("form")
.add($selector.find("form"))
.has("[data-val=true]");
$selector.find("[data-val=true]").each(function () {
$jQval.unobtrusive.parseElement(this, true);
});
$forms.each(function () {
var info = validationInfo(this);
if (info) {
info.attachValidation();
}
});
}
};
adapters = $jQval.unobtrusive.adapters;
adapters.add = function (adapterName, params, fn) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="params" type="Array" optional="true">[Optional] An array of parameter names (strings) that will
/// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and
/// mmmm is the parameter name).</param>
/// <param name="fn" type="Function">The function to call, which adapts the values from the HTML
/// attributes into jQuery Validate rules and/or messages.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
if (!fn) { // Called with no params, just a function
fn = params;
params = [];
}
this.push({ name: adapterName, params: params, adapt: fn });
return this;
};
adapters.addBool = function (adapterName, ruleName) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation rule has no parameter values.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
/// of adapterName will be used instead.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, function (options) {
setValidationValues(options, ruleName || adapterName, true);
});
};
adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and
/// one for min-and-max). The HTML parameters are expected to be named -min and -max.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="minRuleName" type="String">The name of the jQuery Validate rule to be used when you only
/// have a minimum value.</param>
/// <param name="maxRuleName" type="String">The name of the jQuery Validate rule to be used when you only
/// have a maximum value.</param>
/// <param name="minMaxRuleName" type="String">The name of the jQuery Validate rule to be used when you
/// have both a minimum and maximum value.</param>
/// <param name="minAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
/// contains the minimum value. The default is "min".</param>
/// <param name="maxAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
/// contains the maximum value. The default is "max".</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) {
var min = options.params.min,
max = options.params.max;
if (min && max) {
setValidationValues(options, minMaxRuleName, [min, max]);
}
else if (min) {
setValidationValues(options, minRuleName, min);
}
else if (max) {
setValidationValues(options, maxRuleName, max);
}
});
};
adapters.addSingleVal = function (adapterName, attribute, ruleName) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation rule has a single value.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute(where nnnn is the adapter name).</param>
/// <param name="attribute" type="String">[Optional] The name of the HTML attribute that contains the value.
/// The default is "val".</param>
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
/// of adapterName will be used instead.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, [attribute || "val"], function (options) {
setValidationValues(options, ruleName || adapterName, options.params[attribute]);
});
};
$jQval.addMethod("__dummy__", function (value, element, params) {
return true;
});
$jQval.addMethod("regex", function (value, element, params) {
var match;
if (this.optional(element)) {
return true;
}
match = new RegExp(params).exec(value);
return (match && (match.index === 0) && (match[0].length === value.length));
});
$jQval.addMethod("nonalphamin", function (value, element, nonalphamin) {
var match;
if (nonalphamin) {
match = value.match(/\W/g);
match = match && match.length >= nonalphamin;
}
return match;
});
if ($jQval.methods.extension) {
adapters.addSingleVal("accept", "mimtype");
adapters.addSingleVal("extension", "extension");
} else {
// for backward compatibility, when the 'extension' validation method does not exist, such as with versions
// of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for
// validating the extension, and ignore mime-type validations as they are not supported.
adapters.addSingleVal("extension", "extension", "accept");
}
adapters.addSingleVal("regex", "pattern");
adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");
adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range");
adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength");
adapters.add("equalto", ["other"], function (options) {
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0];
setValidationValues(options, "equalTo", element);
});
adapters.add("required", function (options) {
// jQuery Validate equates "required" with "mandatory" for checkbox elements
if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") {
setValidationValues(options, "required", true);
}
});
adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
var value = {
url: options.params.url,
type: options.params.type || "GET",
data: {}
},
prefix = getModelPrefix(options.element.name);
$.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) {
var paramName = appendModelPrefix(fieldName, prefix);
value.data[paramName] = function () {
var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']");
// For checkboxes and radio buttons, only pick up values from checked fields.
if (field.is(":checkbox")) {
return field.filter(":checked").val() || field.filter(":hidden").val() || '';
}
else if (field.is(":radio")) {
return field.filter(":checked").val() || '';
}
return field.val();
};
});
setValidationValues(options, "remote", value);
});
adapters.add("password", ["min", "nonalphamin", "regex"], function (options) {
if (options.params.min) {
setValidationValues(options, "minlength", options.params.min);
}
if (options.params.nonalphamin) {
setValidationValues(options, "nonalphamin", options.params.nonalphamin);
}
if (options.params.regex) {
setValidationValues(options, "regex", options.params.regex);
}
});
adapters.add("fileextensions", ["extensions"], function (options) {
setValidationValues(options, "extension", options.params.extensions);
});
$(function () {
$jQval.unobtrusive.parse(document);
});
return $jQval.unobtrusive;
}));

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
=====================
Copyright Jörn Zaefferer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

10881
wwwroot/lib/jquery/dist/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
wwwroot/lib/jquery/dist/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long