1
0
This commit is contained in:
Debug_pro
2026-06-07 18:19:53 +03:00
commit fb13e5a20a
33 changed files with 2382 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
<Window x:Class="SimpleWiki.Views.ArticleEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Редактор статьи"
Width="1320"
Height="860"
WindowStartupLocation="CenterOwner"
Background="{StaticResource BaseBrush}">
<Border Background="{StaticResource BaseBrush}" Padding="20">
<Grid Background="{StaticResource BaseBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="420" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Style="{StaticResource CardBorder}">
<ScrollViewer VerticalScrollBarVisibility="Hidden">
<StackPanel>
<TextBlock Style="{StaticResource PageTitle}" Text="Редактор статьи" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="Markdown поддерживается." />
<TextBlock Text="Заголовок" />
<TextBox x:Name="TitleTextBox" MaxLength="150" TextChanged="AnyInput_TextChanged" />
<TextBlock Text="Краткое описание" />
<TextBox x:Name="SummaryTextBox" MaxLength="400" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Height="120" TextChanged="AnyInput_TextChanged" />
<TextBlock Text="Markdown" />
<TextBox x:Name="MarkdownTextBox"
MaxLength="20000"
AcceptsReturn="True"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden"
TextWrapping="Wrap"
Height="420"
Background="{StaticResource CrustBrush}"
TextChanged="AnyInput_TextChanged" />
<Button Style="{StaticResource PrimaryButton}" Content="Сохранить" Click="SaveButton_Click" />
<Button Style="{StaticResource SecondaryButton}" Content="Отмена" Click="CancelButton_Click" />
</StackPanel>
</ScrollViewer>
</Border>
<Border Grid.Column="1" Background="{StaticResource BaseBrush}">
<GridSplitter Width="8" HorizontalAlignment="Center" VerticalAlignment="Stretch" Margin="4,0" />
</Border>
<Border Grid.Column="2" Style="{StaticResource CardBorder}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource SectionTitle}" Text="Предпросмотр" />
<Border Grid.Row="1" Margin="0,16,0,0" Background="{StaticResource CrustBrush}" BorderBrush="{StaticResource Surface1Brush}" BorderThickness="1" CornerRadius="12" ClipToBounds="True">
<Grid Background="{StaticResource CrustBrush}">
<WebBrowser x:Name="PreviewBrowser" Margin="0" />
</Grid>
</Border>
</Grid>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,147 @@
using System.Windows;
using System.Windows.Threading;
using Microsoft.EntityFrameworkCore;
using SimpleWiki.Data;
using SimpleWiki.Models;
using SimpleWiki.Services;
namespace SimpleWiki.Views;
public partial class ArticleEditorWindow : Window
{
private readonly User _currentUser;
private readonly int? _articleId;
private bool _isPreviewBrowserReady;
private string _pendingPreviewHtml = string.Empty;
public ArticleEditorWindow(User currentUser, int? articleId = null)
{
InitializeComponent();
_currentUser = currentUser;
_articleId = articleId;
_pendingPreviewHtml = MarkdownService.ToHtmlDocument("Предпросмотр", "# Предпросмотр\n\nПодготовка предпросмотра...");
Loaded += ArticleEditorWindow_Loaded;
ContentRendered += ArticleEditorWindow_ContentRendered;
}
private void ArticleEditorWindow_ContentRendered(object? sender, EventArgs e)
{
_isPreviewBrowserReady = true;
Dispatcher.BeginInvoke(RenderPreviewIfReady, DispatcherPriority.ApplicationIdle);
}
private void ArticleEditorWindow_Loaded(object sender, RoutedEventArgs e)
{
if (_articleId.HasValue)
{
using var db = new AppDbContext();
var article = db.Articles.AsNoTracking().FirstOrDefault(x => x.Id == _articleId.Value);
if (article is not null)
{
TitleTextBox.Text = article.Title;
SummaryTextBox.Text = article.Summary;
MarkdownTextBox.Text = article.MarkdownContent;
}
}
UpdatePreview();
}
private void AnyInput_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
UpdatePreview();
}
private void UpdatePreview()
{
var title = string.IsNullOrWhiteSpace(TitleTextBox.Text) ? "Новая статья" : TitleTextBox.Text.Trim();
var content = string.IsNullOrWhiteSpace(MarkdownTextBox.Text)
? "# Предпросмотр\n\nЗдесь появится отрендеренный markdown."
: MarkdownTextBox.Text;
_pendingPreviewHtml = MarkdownService.ToHtmlDocument(title, content);
RenderPreviewIfReady();
}
private void RenderPreviewIfReady()
{
if (!_isPreviewBrowserReady)
{
return;
}
PreviewBrowser.NavigateToString(_pendingPreviewHtml);
}
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
var titleError = InputValidator.ValidateArticleTitle(TitleTextBox.Text);
var summaryError = InputValidator.ValidateArticleSummary(SummaryTextBox.Text);
var markdownError = InputValidator.ValidateArticleMarkdown(MarkdownTextBox.Text);
var errors = InputValidator.JoinErrors(titleError, summaryError, markdownError);
if (!string.IsNullOrWhiteSpace(errors))
{
MessageBox.Show(errors, "Ошибка валидации", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
using var db = new AppDbContext();
var slug = SlugHelper.Generate(TitleTextBox.Text);
if (_articleId.HasValue)
{
var article = db.Articles.FirstOrDefault(x => x.Id == _articleId.Value);
if (article is null)
{
MessageBox.Show("Статья не найдена.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var duplicate = db.Articles.AsNoTracking().Any(x => x.Slug == slug && x.Id != article.Id);
if (duplicate)
{
MessageBox.Show("Статья с таким заголовком уже существует.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
article.Title = TitleTextBox.Text.Trim();
article.Summary = SummaryTextBox.Text.Trim();
article.MarkdownContent = MarkdownTextBox.Text.Trim();
article.Slug = slug;
article.UpdatedAt = DateTime.UtcNow;
}
else
{
var duplicate = db.Articles.AsNoTracking().Any(x => x.Slug == slug);
if (duplicate)
{
MessageBox.Show("Статья с таким заголовком уже существует.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var article = new Article
{
Title = TitleTextBox.Text.Trim(),
Summary = SummaryTextBox.Text.Trim(),
MarkdownContent = MarkdownTextBox.Text.Trim(),
Slug = slug,
AuthorId = _currentUser.Id,
CreatedAt = DateTime.UtcNow
};
db.Articles.Add(article);
}
db.SaveChanges();
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}

View File

@@ -0,0 +1,35 @@
<Window x:Class="SimpleWiki.Views.ForgotPasswordWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
PreviewTextInput="Window_PreviewTextInput"
Title="Simple Wiki - Восстановление доступа"
Width="560"
Height="760"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BaseBrush}">
<Border Background="{StaticResource BaseBrush}" Padding="28">
<Grid Background="{StaticResource BaseBrush}">
<Border Style="{StaticResource CardBorder}" Padding="28">
<StackPanel>
<TextBlock Style="{StaticResource PageTitle}" Text="Восстановление пароля" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="SMTP нужен." />
<TextBlock Text="Email" />
<TextBox x:Name="EmailTextBox" MaxLength="256" />
<Button Style="{StaticResource PrimaryButton}" Content="Отправить код" Click="SendCodeButton_Click" />
<TextBlock Margin="0,18,0,0" Text="Код подтверждения" />
<TextBox x:Name="CodeTextBox" MaxLength="6" />
<Button Style="{StaticResource SecondaryButton}" Content="Проверить код" Click="VerifyCodeButton_Click" />
<TextBlock Margin="0,18,0,0" Text="Новый пароль" />
<PasswordBox x:Name="NewPasswordBox" MaxLength="64" />
<Button Style="{StaticResource PrimaryButton}" Content="Сменить пароль" Click="ChangePasswordButton_Click" />
<Button Style="{StaticResource SecondaryButton}" Content="Назад ко входу" Click="BackButton_Click" />
</StackPanel>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,116 @@
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Input;
using Microsoft.EntityFrameworkCore;
using SimpleWiki.Data;
using SimpleWiki.Services;
namespace SimpleWiki.Views;
public partial class ForgotPasswordWindow : Window
{
private string? _sentCode;
private bool _isCodeVerified;
public ForgotPasswordWindow()
{
InitializeComponent();
}
private void SendCodeButton_Click(object sender, RoutedEventArgs e)
{
var emailError = InputValidator.ValidateEmail(EmailTextBox.Text);
if (!string.IsNullOrWhiteSpace(emailError))
{
MessageBox.Show(emailError, "Ошибка валидации", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
using var db = new AppDbContext();
var email = EmailTextBox.Text.Trim().ToLowerInvariant();
var exists = db.Users.AsNoTracking().Any(x => x.Email.ToLower() == email);
if (!exists)
{
MessageBox.Show("Пользователь с таким email не найден.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
try
{
_sentCode = EmailService.SendPasswordResetCode(email);
_isCodeVerified = false;
MessageBox.Show("Код отправлен на email.", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка отправки", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void VerifyCodeButton_Click(object sender, RoutedEventArgs e)
{
var codeError = InputValidator.ValidateResetCode(CodeTextBox.Text);
if (!string.IsNullOrWhiteSpace(codeError))
{
MessageBox.Show(codeError, "Ошибка валидации", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
_isCodeVerified = !string.IsNullOrWhiteSpace(_sentCode) && CodeTextBox.Text.Trim() == _sentCode;
if (_isCodeVerified)
{
MessageBox.Show("Код подтверждён.", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show("Неверный код.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ChangePasswordButton_Click(object sender, RoutedEventArgs e)
{
if (!_isCodeVerified)
{
MessageBox.Show("Сначала подтвердите код из письма.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var passwordError = InputValidator.ValidatePassword(NewPasswordBox.Password);
if (!string.IsNullOrWhiteSpace(passwordError))
{
MessageBox.Show(passwordError, "Ошибка валидации", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
using var db = new AppDbContext();
var email = EmailTextBox.Text.Trim().ToLowerInvariant();
var user = db.Users.FirstOrDefault(x => x.Email.ToLower() == email);
if (user is null)
{
MessageBox.Show("Пользователь не найден.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
user.PasswordHash = PasswordHasher.HashPassword(NewPasswordBox.Password);
db.SaveChanges();
MessageBox.Show("Пароль изменён.", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
new LoginWindow().Show();
Close();
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
new LoginWindow().Show();
Close();
}
private void Window_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (CodeTextBox.IsKeyboardFocusWithin)
{
e.Handled = !Regex.IsMatch(e.Text, "^[0-9]+$");
}
}
}

39
Views/LoginWindow.xaml Normal file
View File

@@ -0,0 +1,39 @@
<Window x:Class="SimpleWiki.Views.LoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Simple Wiki - Вход"
Width="520"
Height="640"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BaseBrush}">
<Border Background="{StaticResource BaseBrush}" Padding="28">
<Grid Background="{StaticResource BaseBrush}">
<Border Style="{StaticResource CardBorder}" Padding="28">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Style="{StaticResource PageTitle}" Text="Simple Wiki" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="Вход по email." />
</StackPanel>
<StackPanel Grid.Row="1" VerticalAlignment="Center">
<TextBlock Text="Email" />
<TextBox x:Name="EmailTextBox" MaxLength="256" />
<TextBlock Text="Пароль" />
<PasswordBox x:Name="PasswordBox" MaxLength="64" />
<Button Style="{StaticResource PrimaryButton}" Content="Войти" Click="LoginButton_Click" />
<Button Style="{StaticResource SecondaryButton}" Content="Создать аккаунт" Click="OpenRegisterButton_Click" />
<Button Style="{StaticResource SecondaryButton}" Content="Забыли пароль?" Click="OpenForgotPasswordButton_Click" />
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</Window>

53
Views/LoginWindow.xaml.cs Normal file
View File

@@ -0,0 +1,53 @@
using System.Windows;
using Microsoft.EntityFrameworkCore;
using SimpleWiki.Data;
using SimpleWiki.Services;
namespace SimpleWiki.Views;
public partial class LoginWindow : Window
{
public LoginWindow()
{
InitializeComponent();
}
private void LoginButton_Click(object sender, RoutedEventArgs e)
{
var emailError = InputValidator.ValidateEmail(EmailTextBox.Text);
var passwordError = string.IsNullOrWhiteSpace(PasswordBox.Password) ? "Пароль обязателен." : null;
var errors = InputValidator.JoinErrors(emailError, passwordError);
if (!string.IsNullOrWhiteSpace(errors))
{
MessageBox.Show(errors, "Ошибка валидации", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
using var db = new AppDbContext();
var email = EmailTextBox.Text.Trim().ToLowerInvariant();
var user = db.Users.AsNoTracking().FirstOrDefault(x => x.Email.ToLower() == email);
if (user is null || !PasswordHasher.VerifyPassword(PasswordBox.Password, user.PasswordHash))
{
MessageBox.Show("Неверный email или пароль.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
SessionService.CurrentUser = user;
new MainWindow(user).Show();
Close();
}
private void OpenRegisterButton_Click(object sender, RoutedEventArgs e)
{
new RegisterWindow().Show();
Close();
}
private void OpenForgotPasswordButton_Click(object sender, RoutedEventArgs e)
{
new ForgotPasswordWindow().Show();
Close();
}
}

93
Views/MainWindow.xaml Normal file
View File

@@ -0,0 +1,93 @@
<Window x:Class="SimpleWiki.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:SimpleWiki.Models"
Title="Simple Wiki"
Width="1440"
Height="900"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BaseBrush}">
<Border Background="{StaticResource BaseBrush}" Padding="20">
<Grid Background="{StaticResource BaseBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="320" />
<ColumnDefinition Width="380" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,16,0">
<Border Style="{StaticResource CardBorder}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Профиль" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="Это и есть персональная информация без хеша пароля. Бумажная методичка была бы довольна." />
<TextBlock x:Name="FullNameTextBlock" FontSize="18" FontWeight="SemiBold" Margin="0,12,0,6" />
<TextBlock x:Name="EmailTextBlock" Foreground="{StaticResource SubtextBrush}" />
<TextBlock x:Name="RoleTextBlock" Foreground="{StaticResource BlueBrush}" Margin="0,6,0,0" />
<TextBlock x:Name="CreatedAtTextBlock" Foreground="{StaticResource SubtextBrush}" Margin="0,6,0,0" />
<Button Style="{StaticResource SecondaryButton}" Content="Выйти" Click="LogoutButton_Click" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorder}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Поиск" />
<TextBox x:Name="SearchTextBox" MaxLength="150" TextChanged="SearchTextBox_TextChanged" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="Фильтр по заголовку и краткому описанию." />
</StackPanel>
</Border>
</StackPanel>
<Border Grid.Column="1" Style="{StaticResource CardBorder}" Margin="0,0,16,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Статьи" />
<StackPanel x:Name="AdminToolbar" Orientation="Horizontal" Margin="0,0,0,12">
<Button Width="100" Style="{StaticResource PrimaryButton}" Content="Добавить" Click="AddArticleButton_Click" />
<Button Width="100" Margin="10,6,0,0" Style="{StaticResource SecondaryButton}" Content="Изменить" Click="EditArticleButton_Click" />
<Button Width="100" Margin="10,6,0,0" Style="{StaticResource DangerButton}" Content="Удалить" Click="DeleteArticleButton_Click" />
</StackPanel>
</StackPanel>
<ListBox Grid.Row="1" x:Name="ArticlesListBox" SelectionChanged="ArticlesListBox_SelectionChanged" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:Article}">
<StackPanel>
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" FontSize="16" TextWrapping="Wrap" />
<TextBlock Text="{Binding Summary}" Foreground="{StaticResource SubtextBrush}" Margin="0,6,0,6" TextWrapping="Wrap" />
<TextBlock Foreground="{StaticResource BlueBrush}"
Text="{Binding Author.FullName, StringFormat=Автор: {0}}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<Border Grid.Column="2" Style="{StaticResource CardBorder}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock x:Name="ArticleTitleTextBlock" Style="{StaticResource PageTitle}" Text="Выберите статью" />
<TextBlock x:Name="ArticleSummaryTextBlock" Foreground="{StaticResource SubtextBrush}" TextWrapping="Wrap" Margin="0,0,0,8" />
<TextBlock x:Name="ArticleMetaTextBlock" Foreground="{StaticResource BlueBrush}" />
</StackPanel>
<Border Grid.Row="1" Margin="0,16,0,0" Background="{StaticResource CrustBrush}" BorderBrush="{StaticResource Surface1Brush}" BorderThickness="1" CornerRadius="12" ClipToBounds="True">
<Grid Background="{StaticResource CrustBrush}">
<WebBrowser x:Name="ArticleBrowser" Margin="0" />
</Grid>
</Border>
</Grid>
</Border>
</Grid>
</Border>
</Window>

173
Views/MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,173 @@
using System.Windows;
using System.Windows.Threading;
using Microsoft.EntityFrameworkCore;
using SimpleWiki.Data;
using SimpleWiki.Models;
using SimpleWiki.Services;
namespace SimpleWiki.Views;
public partial class MainWindow : Window
{
private readonly User _currentUser;
private bool _isArticleBrowserReady;
private string _pendingArticleHtml = string.Empty;
public MainWindow(User currentUser)
{
InitializeComponent();
_currentUser = currentUser;
_pendingArticleHtml = MarkdownService.ToHtmlDocument("Simple Wiki", "## Загрузка...");
Loaded += MainWindow_Loaded;
ContentRendered += MainWindow_ContentRendered;
}
private void MainWindow_ContentRendered(object? sender, EventArgs e)
{
_isArticleBrowserReady = true;
Dispatcher.BeginInvoke(RenderArticleIfReady, DispatcherPriority.ApplicationIdle);
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
FullNameTextBlock.Text = _currentUser.FullName;
EmailTextBlock.Text = _currentUser.Email;
RoleTextBlock.Text = _currentUser.Role == Roles.Admin ? "Роль: администратор" : "Роль: пользователь";
CreatedAtTextBlock.Text = $"Дата регистрации: {_currentUser.CreatedAt.ToLocalTime():dd.MM.yyyy HH:mm}";
var isAdmin = _currentUser.Role == Roles.Admin;
AdminToolbar.Visibility = isAdmin ? Visibility.Visible : Visibility.Collapsed;
LoadArticles();
}
private void LoadArticles(string? query = null)
{
using var db = new AppDbContext();
var items = db.Articles
.Include(x => x.Author)
.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(query))
{
var term = query.Trim().ToLower();
items = items.Where(x => x.Title.ToLower().Contains(term) || x.Summary.ToLower().Contains(term));
}
var articles = items.ToList();
ArticlesListBox.ItemsSource = articles;
if (articles.Count > 0)
{
ArticlesListBox.SelectedIndex = 0;
}
else
{
ArticleTitleTextBlock.Text = "Статей пока нет";
ArticleSummaryTextBlock.Text = "Бебебе";
ArticleMetaTextBlock.Text = string.Empty;
_pendingArticleHtml = MarkdownService.ToHtmlDocument("Empty", "## Пусто\n\nПока нечего читать.");
RenderArticleIfReady();
}
}
private void ArticlesListBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (ArticlesListBox.SelectedItem is not Article selectedArticle)
{
return;
}
using var db = new AppDbContext();
var article = db.Articles
.Include(x => x.Author)
.FirstOrDefault(x => x.Id == selectedArticle.Id);
if (article is null)
{
return;
}
ArticleTitleTextBlock.Text = article.Title;
ArticleSummaryTextBlock.Text = article.Summary;
ArticleMetaTextBlock.Text = $"Автор: {article.Author?.FullName} | Создано: {article.CreatedAt.ToLocalTime():dd.MM.yyyy HH:mm}" +
(article.UpdatedAt.HasValue ? $" | Обновлено: {article.UpdatedAt.Value.ToLocalTime():dd.MM.yyyy HH:mm}" : string.Empty);
_pendingArticleHtml = MarkdownService.ToHtmlDocument(article.Title, article.MarkdownContent);
RenderArticleIfReady();
}
private void RenderArticleIfReady()
{
if (!_isArticleBrowserReady)
{
return;
}
ArticleBrowser.NavigateToString(_pendingArticleHtml);
}
private void SearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
LoadArticles(SearchTextBox.Text);
}
private void AddArticleButton_Click(object sender, RoutedEventArgs e)
{
var editor = new ArticleEditorWindow(_currentUser);
if (editor.ShowDialog() == true)
{
LoadArticles(SearchTextBox.Text);
}
}
private void EditArticleButton_Click(object sender, RoutedEventArgs e)
{
if (ArticlesListBox.SelectedItem is not Article article)
{
MessageBox.Show("Сначала выберите статью.", "Внимание", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var editor = new ArticleEditorWindow(_currentUser, article.Id);
if (editor.ShowDialog() == true)
{
LoadArticles(SearchTextBox.Text);
}
}
private void DeleteArticleButton_Click(object sender, RoutedEventArgs e)
{
if (ArticlesListBox.SelectedItem is not Article article)
{
MessageBox.Show("Сначала выберите статью.", "Внимание", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var result = MessageBox.Show($"Удалить статью '{article.Title}'?", "Подтверждение", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
{
return;
}
using var db = new AppDbContext();
var entity = db.Articles.FirstOrDefault(x => x.Id == article.Id);
if (entity is null)
{
return;
}
db.Articles.Remove(entity);
db.SaveChanges();
LoadArticles(SearchTextBox.Text);
}
private void LogoutButton_Click(object sender, RoutedEventArgs e)
{
SessionService.CurrentUser = null;
new LoginWindow().Show();
Close();
}
}

37
Views/RegisterWindow.xaml Normal file
View File

@@ -0,0 +1,37 @@
<Window x:Class="SimpleWiki.Views.RegisterWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Simple Wiki - Регистрация"
Width="560"
Height="720"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BaseBrush}">
<Border Background="{StaticResource BaseBrush}" Padding="28">
<Grid Background="{StaticResource BaseBrush}">
<Border Style="{StaticResource CardBorder}" Padding="28">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Style="{StaticResource PageTitle}" Text="Создать аккаунт" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="Строгая валидация включена. Машина сомневается в тебе заранее." />
<TextBlock Text="ФИО" />
<TextBox x:Name="FullNameTextBox" MaxLength="100" />
<TextBlock Text="Email" />
<TextBox x:Name="EmailTextBox" MaxLength="256" />
<TextBlock Text="Пароль" />
<PasswordBox x:Name="PasswordBox" MaxLength="64" />
<TextBlock Style="{StaticResource CaptionTextBlock}" Text="8-64 символа, строчная, заглавная, цифра и спецсимвол." />
<CheckBox x:Name="IsAdminCheckBox" Content="Администратор" />
<Button Style="{StaticResource PrimaryButton}" Content="Зарегистрироваться" Click="RegisterButton_Click" />
<Button Style="{StaticResource SecondaryButton}" Content="Назад ко входу" Click="BackButton_Click" />
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,61 @@
using System.Windows;
using Microsoft.EntityFrameworkCore;
using SimpleWiki.Data;
using SimpleWiki.Models;
using SimpleWiki.Services;
namespace SimpleWiki.Views;
public partial class RegisterWindow : Window
{
public RegisterWindow()
{
InitializeComponent();
}
private void RegisterButton_Click(object sender, RoutedEventArgs e)
{
var fullNameError = InputValidator.ValidateFullName(FullNameTextBox.Text);
var emailError = InputValidator.ValidateEmail(EmailTextBox.Text);
var passwordError = InputValidator.ValidatePassword(PasswordBox.Password);
var errors = InputValidator.JoinErrors(fullNameError, emailError, passwordError);
if (!string.IsNullOrWhiteSpace(errors))
{
MessageBox.Show(errors, "Ошибка валидации", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
using var db = new AppDbContext();
var email = EmailTextBox.Text.Trim().ToLowerInvariant();
var exists = db.Users.AsNoTracking().Any(x => x.Email.ToLower() == email);
if (exists)
{
MessageBox.Show("Пользователь с таким email уже существует.", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var user = new User
{
FullName = FullNameTextBox.Text.Trim(),
Email = email,
PasswordHash = PasswordHasher.HashPassword(PasswordBox.Password),
Role = IsAdminCheckBox.IsChecked == true ? Roles.Admin : Roles.User,
CreatedAt = DateTime.UtcNow
};
db.Users.Add(user);
db.SaveChanges();
MessageBox.Show("Аккаунт создан. Теперь можно войти.", "Успех", MessageBoxButton.OK, MessageBoxImage.Information);
new LoginWindow().Show();
Close();
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
new LoginWindow().Show();
Close();
}
}