187 lines
8.7 KiB
Plaintext
187 lines
8.7 KiB
Plaintext
@page "/"
|
|
@using Microsoft.EntityFrameworkCore
|
|
@using Microsoft.Extensions.Options
|
|
@using Wave.Data
|
|
@using Wave.Utilities
|
|
@using System.Globalization
|
|
|
|
@inject IOptions<Customization> Customizations
|
|
@inject IOptions<Features> Features
|
|
@inject NavigationManager Navigation
|
|
@inject IDbContextFactory<ApplicationDbContext> ContextFactory;
|
|
@inject IStringLocalizer<Home> Localizer
|
|
@inject IMessageDisplay Message
|
|
|
|
<HeadContent>
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:title" content="@Customizations.Value.AppName">
|
|
<meta property="og:site_name" content="@Customizations.Value.AppName">
|
|
<meta property="og:url" content="@Navigation.BaseUri">
|
|
@if (!string.IsNullOrWhiteSpace(Customizations.Value.LogoLink)) {
|
|
<meta property="og:image" content="@Customizations.Value.LogoLink">
|
|
} else {
|
|
<meta property="og:image" content="@Navigation.ToAbsoluteUri("/img/logo.png")">
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Customizations.Value.AppDescription)) {
|
|
<meta name="description" content="@Customizations.Value.AppDescription">
|
|
<meta property="og:description" content="@Customizations.Value.AppDescription">
|
|
}
|
|
@if (Features.Value.Rss) {
|
|
<link rel="alternate" type="application/rss+xml" title="RSS Feed on @Customizations.Value.AppName" href="/rss/rss.xml">
|
|
<link rel="alternate" type="application/atom+xml" title="Atom RSS Feed on @Customizations.Value.AppName" href="/rss/atom.xml">
|
|
}
|
|
|
|
@if (Page >= TotalPages) {
|
|
<meta name="robots" content="noindex">
|
|
} else if (Page > 0) {
|
|
<link rel="canonical" href="@(new UriBuilder(Navigation.BaseUri) { Scheme = "https", Query = "?page=" + Page, Port = -1 }.Uri.AbsoluteUri)" />
|
|
} else {
|
|
<link rel="canonical" href="@(new UriBuilder(Navigation.BaseUri) { Scheme = "https", Port = -1 }.Uri.AbsoluteUri)" />
|
|
}
|
|
</HeadContent>
|
|
|
|
<PageTitle>@(Customizations.Value.AppName ?? "Wave")</PageTitle>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 xl:grid-rows-4 gap-4">
|
|
<div class="sm:col-span-2 flex flex-col">
|
|
<h1 class="text-3xl lg:text-5xl font-light mb-3 text-primary">@Customizations.Value.AppName</h1>
|
|
<p class="flex-1 my-3">@Customizations.Value.AppDescription</p>
|
|
<section class="flex gap-2 justify-between sm:justify-start flex-wrap mb-3">
|
|
@if (Features.Value.Rss) {
|
|
<a class="btn btn-sm bg-orange-500 text-slate-50" title="RSS Feed on @Customizations.Value.AppName" href="/rss/rss.xml">
|
|
RSS
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M3.75 4.5a.75.75 0 0 1 .75-.75h.75c8.284 0 15 6.716 15 15v.75a.75.75 0 0 1-.75.75h-.75a.75.75 0 0 1-.75-.75v-.75C18 11.708 12.292 6 5.25 6H4.5a.75.75 0 0 1-.75-.75V4.5Zm0 6.75a.75.75 0 0 1 .75-.75h.75a8.25 8.25 0 0 1 8.25 8.25v.75a.75.75 0 0 1-.75.75H12a.75.75 0 0 1-.75-.75v-.75a6 6 0 0 0-6-6H4.5a.75.75 0 0 1-.75-.75v-.75Zm0 7.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" clip-rule="evenodd" />
|
|
</svg>
|
|
</a>
|
|
<a class="btn btn-sm bg-orange-500 text-slate-50" title="Atom RSS Feed on @Customizations.Value.AppName" href="/rss/atom.xml">
|
|
Atom RSS
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M3.75 4.5a.75.75 0 0 1 .75-.75h.75c8.284 0 15 6.716 15 15v.75a.75.75 0 0 1-.75.75h-.75a.75.75 0 0 1-.75-.75v-.75C18 11.708 12.292 6 5.25 6H4.5a.75.75 0 0 1-.75-.75V4.5Zm0 6.75a.75.75 0 0 1 .75-.75h.75a8.25 8.25 0 0 1 8.25 8.25v.75a.75.75 0 0 1-.75.75H12a.75.75 0 0 1-.75-.75v-.75a6 6 0 0 0-6-6H4.5a.75.75 0 0 1-.75-.75v-.75Zm0 7.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" clip-rule="evenodd" />
|
|
</svg>
|
|
</a>
|
|
}
|
|
@if (Features.Value.EmailSubscriptions) {
|
|
<a class="btn btn-sm btn-primary" title="E-Mail Newsletter" href="/Email/Subscribe">
|
|
E-Mail
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
|
|
<path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" />
|
|
</svg>
|
|
</a>
|
|
}
|
|
</section>
|
|
</div>
|
|
@if (Featured is {} featured) {
|
|
<div class="sm:col-span-2 row-span-2 aspect-square xl:order-first" style="padding: 0 4px 4px 0">
|
|
<ArticleLink Article="featured">
|
|
<article class="relative h-full bg-secondary text-secondary-content border-2 border-current shadow-[4px_4px_0_0_currentColor]">
|
|
<div class="absolute inset-8 overflow-hidden fade-away">
|
|
<div class="mb-3">
|
|
<h2 class="text-2xl lg:text-4xl line-clamp-2 hyphens-auto">
|
|
@featured.Title
|
|
</h2>
|
|
<p class="flex flex-wrap gap-2">
|
|
@foreach (var category in featured.Categories.OrderBy(c => c.Color)) {
|
|
<span class="badge badge-@CategoryUtilities.GetCssClassPostfixForColor(category.Color)">
|
|
@category.Name
|
|
</span>
|
|
}
|
|
</p>
|
|
</div>
|
|
<div class="hidden sm:block w-1/3 float-left mr-2">
|
|
<ProfilePictureComponent ProfileId="@featured.Author.Id" LoadAsync="false" Size="400" />
|
|
</div>
|
|
<p class="font-bold">@featured.Author.FullName</p>
|
|
<small class="text-sm">@featured.PublishDate.ToString("g")</small>
|
|
<p class="hyphens-auto text-justify" lang="@CultureInfo.CurrentCulture">
|
|
@((MarkupString) featured.BodyPlain[..Math.Min(1500, featured.BodyPlain.Length)]) ...
|
|
</p>
|
|
</div>
|
|
</article>
|
|
</ArticleLink>
|
|
</div>
|
|
} else {
|
|
<div class="sm:col-span-4">
|
|
<h2 class="text-2xl lg:text-4xl mb-6">@Localizer["NoArticles_Title"]</h2>
|
|
<p>@Localizer["NoArticles_Message"]</p>
|
|
</div>
|
|
}
|
|
|
|
<PageComponent LoadCallback="LoadArticles" Page="@Page">
|
|
<div class="aspect-square">
|
|
<ArticleTile Article="context" />
|
|
</div>
|
|
</PageComponent>
|
|
</div>
|
|
|
|
<div class="grid place-content-center my-3">
|
|
<div class="join">
|
|
@if (Page < 1) {
|
|
<button class="join-item btn" disabled title="@Localizer["Paging_Previous"]">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 1 1 1.06 1.06L9.31 12l6.97 6.97a.75.75 0 1 1-1.06 1.06l-7.5-7.5Z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
} else {
|
|
<a class="join-item btn" target="_top" href="@(Page < 2 ? "/" : $"/?page={Page - 1}")" title="@Localizer["Paging_Previous"]">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 1 1 1.06 1.06L9.31 12l6.97 6.97a.75.75 0 1 1-1.06 1.06l-7.5-7.5Z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</a>
|
|
}
|
|
<button class="join-item btn md:btn-wide no-animation">@Localizer["Paging_Page"] @(Page + 1)</button>
|
|
@if (Page >= TotalPages - 1) {
|
|
<button class="join-item btn" disabled title="@Localizer["Paging_Next"]">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M16.28 11.47a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 0 1-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 0 1 1.06-1.06l7.5 7.5Z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</button>
|
|
} else {
|
|
<a class="join-item btn" target="_top" href="/?page=@(Page + 1)" title="@Localizer["Paging_Next"]">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
|
|
<path fill-rule="evenodd" d="M16.28 11.47a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 0 1-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 0 1 1.06-1.06l7.5 7.5Z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
[CascadingParameter(Name = "TitlePrefix")]
|
|
private string TitlePrefix { get; set; } = default!;
|
|
[SupplyParameterFromQuery]
|
|
public int Page { get; set; } = 0;
|
|
|
|
private int TotalPages { get; set; } = 1;
|
|
|
|
private Article? Featured { get; set; }
|
|
|
|
protected override async Task OnInitializedAsync() {
|
|
try {
|
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
|
|
var query = context.Set<Article>()
|
|
.Include(a => a.Author).Include(a => a.Categories)
|
|
.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id);
|
|
|
|
Featured = await query.FirstOrDefaultAsync();
|
|
TotalPages = (int) Math.Max(Math.Ceiling((await query.CountAsync() - 1) / 10.0), 1);
|
|
} catch {
|
|
Message.ShowError(Localizer["Articles_Load_Error"]);
|
|
}
|
|
}
|
|
|
|
private async ValueTask<IEnumerable<Article>> LoadArticles(int page, int count) {
|
|
try {
|
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
return await context.Set<Article>()
|
|
.Include(a => a.Author).Include(a => a.Categories)
|
|
.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id)
|
|
.Skip(page + 1).Take(count).ToListAsync();
|
|
} catch {
|
|
Message.ShowError(Localizer["Articles_Load_Error"]);
|
|
return [];
|
|
}
|
|
}
|
|
} |