💄 Optimize hosted page index

This commit is contained in:
2025-11-22 14:16:40 +08:00
parent f4505d2ecc
commit ef02265ccd
8 changed files with 484 additions and 181 deletions

View File

@@ -304,60 +304,6 @@
About the site
</h2>
<div class="flex flex-col md:flex-row gap-6">
<div class="flex flex-col gap-4 flex-1">
<div class="card bg-base-100 border">
<div class="card-body">
<h3 class="card-title"><span class="mdi mdi-home"></span> Info</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-label"></span>
Name
</span>
<span class="text-right">@Model.Site.Name</span>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-tag"></span>
Slug
</span>
<span class="text-right font-mono">@Model.Site.Slug</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-4 flex-1">
@if (!string.IsNullOrEmpty(Model.Site.Description))
{
<div class="flex-1">
<div class="card bg-base-100 border">
<div class="card-body">
<h3 class="card-title">
<span class="mdi mdi-information"></span> Description
</h3>
<div class="prose prose-sm max-w-none">
@Model.Site.Description
</div>
</div>
</div>
</div>
}
<div class="flex-1">
<div class="flex flex-col p-5 opacity-80 text-sm">
<p>Proudly powered by the Solar Network Pages</p>
<p>Hosted on the Solar Network</p>
<p class="mt-2">Network powered by Cloudflare</p>
<p>Therefore, if the site is down, 99% is Cloudflare's fault</p>
</div>
</div>
</div>
</div>
<partial name="Shared/_SiteInfo" model="Model.Site" />
</div>
}

View File

@@ -1,16 +1,134 @@
@page
@page
@model IndexModel
@{
Layout = "_LayoutContained";
ViewData["Title"] = "Solar Network Pages";
var pageTitle = "Home";
var pageDescription = "Welcome to the Solar Network Pages.";
var ogType = "website";
string? ogImageUrl = null;
var canonicalUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}";
var siteName = Model.Site?.Name ?? "Solar Network";
if (Model.UserAccount != null)
{
pageTitle = $"{Model.UserAccount.Nick ?? Model.UserAccount.Name}'s Page";
pageDescription = !string.IsNullOrWhiteSpace(Model.UserAccount.Profile?.Bio) ? Model.UserAccount.Profile.Bio : $"Profile of {Model.UserAccount.Nick ?? Model.UserAccount.Name} on {siteName}";
ogType = "profile";
ogImageUrl = Model.UserBackgroundUrl;
if (!string.IsNullOrEmpty(ogImageUrl) && !ogImageUrl.StartsWith("http"))
ogImageUrl = $"{Request.Scheme}://{Request.Host}{ogImageUrl}";
}
else if (Model.Site != null)
{
pageTitle = Model.Site.Name;
if (!string.IsNullOrWhiteSpace(Model.Site.Description))
{
pageDescription = Model.Site.Description;
}
ogType = "website";
ogImageUrl = null;
}
if (!string.IsNullOrWhiteSpace(pageDescription) && pageDescription.Length > 160)
{
pageDescription = pageDescription.Substring(0, 157) + "...";
}
ViewData["Title"] = $"{pageTitle} - {siteName}";
}
<div class="h-full flex justify-center items-center">
<div class="text-center max-w-96">
<img src="~/favicon.png" width="80" height="80" alt="Logo" class="mb-1 mx-auto"/>
<h1 class="text-2xl">Hello World 👋</h1>
<p>Here are the Solar Network Pages</p>
<p class="text-sm opacity-80 mt-1">为什么这个首页长这样呢?因为羊不知道该放什么,所以如果你有想法欢迎跟羊讲!</p>
@section Head {
<meta name="description" content="@pageDescription"/>
<link rel="canonical" href="@canonicalUrl"/>
<meta property="og:title" content="@pageTitle"/>
<meta property="og:description" content="@pageDescription"/>
<meta property="og:type" content="@ogType"/>
<meta property="og:url" content="@canonicalUrl"/>
@if (!string.IsNullOrEmpty(ogImageUrl))
{
<meta property="og:image" content="@ogImageUrl"/>
}
<meta property="og:site_name" content="@siteName"/>
<meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="@pageTitle"/>
<meta name="twitter:description" content="@pageDescription"/>
@if (!string.IsNullOrEmpty(ogImageUrl))
{
<meta name="twitter:image" content="@ogImageUrl"/>
}
}
<div class="container mx-auto px-8 py-8">
@if (Model.FeaturedPosts.Any())
{
<h2 class="text-xl flex gap-2 mb-5 px-3">
<span class="mdi mdi-star"></span> Featured Posts
</h2>
<div class="carousel carousel-center w-full max-h-120 p-4 space-x-4 rounded-box mb-8 shadow-lg bg-base-300">
@for (int i = 0; i < Model.FeaturedPosts.Count; i++)
{
var post = Model.FeaturedPosts[i];
<div id="item@(i + 1)" class="carousel-item card card-side lg:card-side w-full bg-base-100 shadow-xl">
<figure class="max-w-3/5">
@if (post.Attachments.Any(a => a.MimeType!.StartsWith("image")))
{
var imageAttachment = post.Attachments.First(a => a.MimeType!.StartsWith("image"));
<img src="/drive/files/@imageAttachment.Id" alt="@imageAttachment.Name" class="h-full object-cover"/>
}
else
{
<img src="https://placehold.co/600x400?text=No+Image" alt="No Image" class="h-full object-cover"/>
}
</figure>
<div class="card-body">
<h2 class="card-title">
<a asp-page="/Posts/Details" asp-route-slug="@(post.Slug ?? post.Id.ToString())">@post.Title</a>
</h2>
@if (!string.IsNullOrWhiteSpace(post.Description))
{
<p class="text-sm">@post.Description</p>
}
<div class="card-actions justify-end">
<a asp-page="/Posts/Details" asp-route-slug="@(post.Slug ?? post.Id.ToString())" class="btn btn-primary btn-sm">Read More</a>
</div>
</div>
</div>
}
</div>
@if (Model.FeaturedPosts.Count > 1)
{
<div class="flex justify-center w-full py-2 gap-2 mb-8">
@for (var idx = 0; idx < Model.FeaturedPosts.Count; idx++)
{
<a href="#item@(idx + 1)" class="btn btn-xs">@(idx + 1)</a>
}
</div>
}
}
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
@if (Model.UserAccount != null)
{
<div>
<h2 class="text-xl flex gap-2 mb-5 px-3">
<span class="mdi mdi-account-circle"></span> About me
</h2>
<partial name="Shared/_UserInfo" model="Model"/>
</div>
}
@if (Model.Site != null)
{
<div>
<h2 class="text-xl flex gap-2 mb-5 px-3">
<span class="mdi mdi-sitemap"></span> About the Site
</h2>
<partial name="Shared/_SiteInfo" model="Model.Site"/>
</div>
}
</div>
</div>
</div>

View File

@@ -1,10 +1,109 @@
using Markdig;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Zone.Publication;
using Microsoft.AspNetCore.Mvc.RazorPages;
using NodaTime;
namespace DysonNetwork.Zone.Pages;
public class IndexModel : PageModel
public class IndexModel(PostService.PostServiceClient postClient, RemotePublisherService rps, RemoteAccountService ras) : PageModel
{
public void OnGet()
public SnPublicationSite? Site { get; set; }
public SnPublisher? Publisher { get; set; }
public Account? UserAccount { get; set; }
public List<SnPost> FeaturedPosts { get; set; } = [];
public string? UserPictureUrl => UserAccount?.Profile?.Picture?.Id != null
? $"/drive/files/{UserAccount.Profile.Picture.Id}"
: null;
public string? UserBackgroundUrl => UserAccount?.Profile?.Background?.Id != null
? $"/drive/files/{UserAccount.Profile.Background.Id}?original=true"
: null;
public async Task OnGetAsync()
{
Site = HttpContext.Items[PublicationSiteMiddleware.SiteContextKey] as SnPublicationSite;
if (Site != null)
{
// Fetch Publisher Information
Publisher = await rps.GetPublisher(id: Site!.PublisherId.ToString());
// Fetch User Account Information if available
UserAccount = await ras.GetAccount(Site.AccountId);
// Fetch Featured Posts (e.g., top 5 by views)
var request = new ListPostsRequest
{
OrderBy = "views", // Assuming 'views' is a valid order-by option for popularity
OrderDesc = true,
PageSize = 5,
PublisherId = Site!.PublisherId.ToString()
};
var response = await postClient.ListPostsAsync(request);
if (response?.Posts != null)
{
FeaturedPosts = response.Posts.Select(SnPost.FromProtoValue).ToList();
// Convert the markdown content to HTML
foreach (var post in FeaturedPosts.Where(post => !string.IsNullOrEmpty(post.Content)))
post.Content = Markdown.ToHtml(post.Content!);
}
}
}
public int CalculateAge(Instant birthday)
{
var birthDate = birthday.ToDateTimeOffset();
var today = DateTimeOffset.Now;
var age = today.Year - birthDate.Year;
if (birthDate > today.AddYears(-age)) age--;
return age;
}
public string GetOffsetUtcString(string targetTimeZone)
{
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZone);
var offset = tz.GetUtcOffset(DateTimeOffset.Now);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
return $"{Math.Abs(offset.Hours):D2}:{Math.Abs(offset.Minutes):D2}";
}
catch
{
return "00:00";
}
}
public string GetCurrentTimeInTimeZone(string targetTimeZone)
{
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZone);
var now = TimeZoneInfo.ConvertTime(DateTimeOffset.Now, tz);
return now.ToString("t", System.Globalization.CultureInfo.InvariantCulture);
}
catch
{
return DateTime.Now.ToString("t", System.Globalization.CultureInfo.InvariantCulture);
}
}
public (string Name, string Color) GetPerkInfo(string identifier)
{
return identifier switch
{
"solian.stellar.primary" => ("Stellar", "#2196f3"),
"solian.stellar.nova" => ("Nova", "#39c5bb"),
"solian.stellar.supernova" => ("Supernova", "#ffc109"),
_ => ("Unknown", "#2196f3")
};
}
}

View File

@@ -58,92 +58,7 @@
<div class="space-y-8">
@foreach (var post in Model.Posts)
{
<div class="card bg-base-200 shadow-md">
<div class="card-body flex flex-col gap-5">
<div>
@if (!string.IsNullOrWhiteSpace(post.Title) || !string.IsNullOrWhiteSpace(post.Description))
{
<section class="mb-1">
@if (!string.IsNullOrWhiteSpace(post.Title))
{
<h2 class="text-lg font-bold">
<a asp-page="/Posts/Details"
asp-route-slug="@(post.Slug ?? post.Id.ToString())">@post.Title</a>
</h2>
}
@if (!string.IsNullOrWhiteSpace(post.Description))
{
<p>@post.Description</p>
}
</section>
}
<p>@Html.Raw(post.Content)</p>
</div>
@if (post.Attachments.Any())
{
<div class="flex flex-col gap-4">
@foreach (var attachment in post.Attachments)
{
<div>
@if (attachment.MimeType!.StartsWith("image"))
{
<figure>
<img src="/drive/files/@attachment.Id" alt="@attachment.Name"
class="max-w-full h-auto rounded-lg"/>
</figure>
}
else if (attachment.MimeType!.StartsWith("video"))
{
<video controls src="/drive/files/@attachment.Id"
class="max-w-full h-auto rounded-lg"></video>
}
else if (attachment.MimeType!.StartsWith("audio"))
{
<audio controls src="/drive/files/@attachment.Id"
class="max-w-full h-auto rounded-lg"></audio>
}
else
{
<a href="/drive/files/@attachment.Id" target="_blank"
class="text-blue-500 hover:underline">@attachment.Name</a>
}
</div>
}
</div>
}
@if (post.Categories.Any() || post.Tags.Any())
{
<div>
@foreach (var category in post.Categories)
{
<span class="badge badge-primary">
<span class="mdi mdi-shape"></span>
@(!string.IsNullOrEmpty(category.Name) ? category.Name : category.Slug)
</span>
}
@foreach (var tag in post.Tags)
{
<span class="badge badge-info">
<span class="mdi mdi-tag"></span>
@(!string.IsNullOrEmpty(tag.Name) ? tag.Name : tag.Slug)
</span>
}
</div>
}
<div class="card-actions justify-end items-center">
<div class="text-sm text-base-content/60">
Posted on @post.CreatedAt.ToDateTimeOffset().ToString("yyyy-MM-dd")
</div>
<a asp-page="/Posts/Details" asp-route-slug="@(post.Slug ?? post.Id.ToString())"
class="btn btn-sm btn-ghost btn-circle">
<span class="mdi mdi-arrow-right text-lg"></span>
</a>
</div>
</div>
</div>
<partial name="Shared/_PostItem" model="post" />
}
</div>
}
@@ -175,35 +90,7 @@
@if (Model.Publisher != null)
{
<div class="col-span-3 md:col-span-1">
<div class="card bg-base-200 shadow-md">
@if (Model.Publisher!.Background != null)
{
<figure class="relative">
<img
src="/drive/files/@Model.Publisher!.Background.Id"
alt="Background"/>
</figure>
}
<div class="card-body">
<div class="flex gap-4 card-title">
<div class="avatar">
<div class="w-16 rounded-full">
<img
src="@(Model.Publisher.Picture is null ? defaultAvatar : "/drive/files/" + Model.Publisher.Picture!.Id)"
alt="Avatar"/>
</div>
</div>
<h2 class="flex-1 flex flex-col">
@Model.Publisher.Nick
<span class="text-xs">@@@Model.Publisher.Name</span>
</h2>
</div>
@if (!string.IsNullOrWhiteSpace(Model.Publisher.Bio))
{
<p>@Model.Publisher.Bio</p>
}
</div>
</div>
<partial name="Shared/_PublisherInfo" model="Model.Publisher" />
</div>
}
</div>

View File

@@ -0,0 +1,89 @@
@using DysonNetwork.Shared.Models
@model DysonNetwork.Shared.Models.SnPost
<div class="card bg-base-200 shadow-md">
<div class="card-body flex flex-col gap-5">
<div>
@if (!string.IsNullOrWhiteSpace(Model.Title) || !string.IsNullOrWhiteSpace(Model.Description))
{
<section class="mb-1">
@if (!string.IsNullOrWhiteSpace(Model.Title))
{
<h2 class="text-lg font-bold">
<a asp-page="/Posts/Details"
asp-route-slug="@(Model.Slug ?? Model.Id.ToString())">@Model.Title</a>
</h2>
}
@if (!string.IsNullOrWhiteSpace(Model.Description))
{
<p>@Model.Description</p>
}
</section>
}
<p>@Html.Raw(Model.Content)</p>
</div>
@if (Model.Attachments.Any())
{
<div class="flex flex-col gap-4">
@foreach (var attachment in Model.Attachments)
{
<div>
@if (attachment.MimeType!.StartsWith("image"))
{
<figure>
<img src="/drive/files/@attachment.Id" alt="@attachment.Name"
class="max-w-full h-auto rounded-lg"/>
</figure>
}
else if (attachment.MimeType!.StartsWith("video"))
{
<video controls src="/drive/files/@attachment.Id"
class="max-w-full h-auto rounded-lg"></video>
}
else if (attachment.MimeType!.StartsWith("audio"))
{
<audio controls src="/drive/files/@attachment.Id"
class="max-w-full h-auto rounded-lg"></audio>
}
else
{
<a href="/drive/files/@attachment.Id" target="_blank"
class="text-blue-500 hover:underline">@attachment.Name</a>
}
</div>
}
</div>
}
@if (Model.Categories.Any() || Model.Tags.Any())
{
<div>
@foreach (var category in Model.Categories)
{
<span class="badge badge-primary">
<span class="mdi mdi-shape"></span>
@(!string.IsNullOrEmpty(category.Name) ? category.Name : category.Slug)
</span>
}
@foreach (var tag in Model.Tags)
{
<span class="badge badge-info">
<span class="mdi mdi-tag"></span>
@(!string.IsNullOrEmpty(tag.Name) ? tag.Name : tag.Slug)
</span>
}
</div>
}
<div class="card-actions justify-end items-center">
<div class="text-sm text-base-content/60">
Posted on @Model.CreatedAt.ToDateTimeOffset().ToString("yyyy-MM-dd")
</div>
<a asp-page="/Posts/Details" asp-route-slug="@(Model.Slug ?? Model.Id.ToString())"
class="btn btn-sm btn-ghost btn-circle">
<span class="mdi mdi-arrow-right text-lg"></span>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
@using DysonNetwork.Shared.Models
@model DysonNetwork.Shared.Models.SnPublisher
@{
const string defaultAvatar = "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp";
}
<div class="card bg-base-200 shadow-md">
@if (Model.Background != null)
{
<figure class="relative">
<img
src="/drive/files/@Model.Background.Id"
alt="Background"/>
</figure>
}
<div class="card-body">
<div class="flex gap-4 card-title">
<div class="avatar">
<div class="w-16 rounded-full">
<img
src="@(Model.Picture is null ? defaultAvatar : "/drive/files/" + Model.Picture!.Id)"
alt="Avatar"/>
</div>
</div>
<h2 class="flex-1 flex flex-col">
@Model.Nick
<span class="text-xs">@@@Model.Name</span>
</h2>
</div>
@if (!string.IsNullOrWhiteSpace(Model.Bio))
{
<p>@Model.Bio</p>
}
</div>
</div>

View File

@@ -0,0 +1,58 @@
@using DysonNetwork.Shared.Models
@model DysonNetwork.Shared.Models.SnPublicationSite
<div class="flex flex-col md:flex-row gap-6">
<div class="flex flex-col gap-4 flex-1">
<div class="card bg-base-100 border">
<div class="card-body">
<h3 class="card-title"><span class="mdi mdi-home"></span> Info</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-label"></span>
Name
</span>
<span class="text-right">@Model.Name</span>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-tag"></span>
Slug
</span>
<span class="text-right font-mono">@Model.Slug</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-4 flex-1">
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="flex-1">
<div class="card bg-base-100 border">
<div class="card-body">
<h3 class="card-title">
<span class="mdi mdi-information"></span> Description
</h3>
<div class="prose prose-sm max-w-none">
@Model.Description
</div>
</div>
</div>
</div>
}
<div class="flex-1">
<div class="flex flex-col p-5 opacity-80 text-sm">
<p>Proudly powered by the Solar Network Pages</p>
<p>Hosted on the Solar Network</p>
<p class="mt-2">Network powered by Cloudflare</p>
<p>Therefore, if the site is down, 99% is Cloudflare's fault</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,70 @@
@model DysonNetwork.Zone.Pages.IndexModel
@if (Model.UserAccount != null)
{
<div class="card bg-base-100 border">
<div class="card-body">
<div class="flex items-center gap-6 mb-8">
@if (!string.IsNullOrEmpty(Model.UserPictureUrl))
{
<img src="@Model.UserPictureUrl" class="w-20 h-20 rounded-full object-cover" alt="Avatar"/>
}
else
{
<div class="w-20 h-20 rounded-full bg-gray-300 flex items-center justify-center">
<span class="text-2xl">@(Model.UserAccount.Name != null ? Model.UserAccount.Name[0] : '?')</span>
</div>
}
<div>
<div class="text-2xl font-bold">
@(Model.UserAccount.Nick ?? Model.UserAccount.Name)
</div>
<div class="text-body-2 text-base-content/60">@@@Model.UserAccount.Name</div>
</div>
</div>
<div class="space-y-2">
@if (!string.IsNullOrEmpty(Model.UserAccount.Profile?.TimeZone))
{
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-map-clock"></span>
Time Zone
</span>
<span class="text-right">
@Model.GetCurrentTimeInTimeZone(Model.UserAccount.Profile.TimeZone)
<span class="text-base-content/50">·</span>
@Model.GetOffsetUtcString(Model.UserAccount.Profile.TimeZone)
<span class="text-base-content/50">·</span>
@Model.UserAccount.Profile.TimeZone
</span>
</div>
}
@if (!string.IsNullOrEmpty(Model.UserAccount.Profile?.Location))
{
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-map-marker"></span> Location
</span>
<span>@Model.UserAccount.Profile.Location</span>
</div>
}
@if (!string.IsNullOrEmpty(Model.UserAccount.Profile?.FirstName) || !string.IsNullOrEmpty(Model.UserAccount.Profile?.LastName))
{
<div class="flex justify-between">
<span class="flex gap-2">
<span class="mdi mdi-card-bulleted-outline"></span> Name
</span>
<span>
@string.Join(" ", new[]
{
Model.UserAccount.Profile.FirstName,
Model.UserAccount.Profile.MiddleName,
Model.UserAccount.Profile.LastName
}.Where(s => !string.IsNullOrEmpty(s)))
</span>
</div>
}
</div>
</div>
</div>
}