Merge pull request #16503 from abpframework/Onur/blogging-module-enhancement

Blogging - Improvements to member page and post detail page
pull/16569/head
Alper Ebiçoğlu 2 years ago committed by GitHub
commit 3525da8adc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,226 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
{
/// <inheritdoc />
public partial class AddedBlogUserOptionals : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Biography",
table: "BlgUsers",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Company",
table: "BlgUsers",
type: "nvarchar(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Github",
table: "BlgUsers",
type: "nvarchar(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "JobTitle",
table: "BlgUsers",
type: "nvarchar(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Linkedin",
table: "BlgUsers",
type: "nvarchar(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Twitter",
table: "BlgUsers",
type: "nvarchar(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WebSite",
table: "BlgUsers",
type: "nvarchar(256)",
maxLength: 256,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "EntityVersion",
table: "AbpUsers",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "LastPasswordChangeTime",
table: "AbpUsers",
type: "datetimeoffset",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "ShouldChangePasswordOnNextLogin",
table: "AbpUsers",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "EntityVersion",
table: "AbpRoles",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "EntityVersion",
table: "AbpOrganizationUnits",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "AbpPermissionGroups",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpPermissionGroups", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AbpPermissions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
GroupName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
ParentName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
DisplayName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
IsEnabled = table.Column<bool>(type: "bit", nullable: false),
MultiTenancySide = table.Column<byte>(type: "tinyint", nullable: false),
Providers = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
StateCheckers = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpPermissions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AbpUserDelegations",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
SourceUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TargetUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
StartTime = table.Column<DateTime>(type: "datetime2", nullable: false),
EndTime = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUserDelegations", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_AbpPermissionGroups_Name",
table: "AbpPermissionGroups",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AbpPermissions_GroupName",
table: "AbpPermissions",
column: "GroupName");
migrationBuilder.CreateIndex(
name: "IX_AbpPermissions_Name",
table: "AbpPermissions",
column: "Name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AbpPermissionGroups");
migrationBuilder.DropTable(
name: "AbpPermissions");
migrationBuilder.DropTable(
name: "AbpUserDelegations");
migrationBuilder.DropColumn(
name: "Biography",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "Company",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "Github",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "JobTitle",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "Linkedin",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "Twitter",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "WebSite",
table: "BlgUsers");
migrationBuilder.DropColumn(
name: "EntityVersion",
table: "AbpUsers");
migrationBuilder.DropColumn(
name: "LastPasswordChangeTime",
table: "AbpUsers");
migrationBuilder.DropColumn(
name: "ShouldChangePasswordOnNextLogin",
table: "AbpUsers");
migrationBuilder.DropColumn(
name: "EntityVersion",
table: "AbpRoles");
migrationBuilder.DropColumn(
name: "EntityVersion",
table: "AbpOrganizationUnits");
}
}
}

@ -19,10 +19,10 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer)
.HasAnnotation("ProductVersion", "6.0.0")
.HasAnnotation("ProductVersion", "7.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Volo.Abp.BlobStoring.Database.DatabaseBlob", b =>
{
@ -183,6 +183,9 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<int>("EntityVersion")
.HasColumnType("int");
b.Property<string>("ExtraProperties")
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
@ -369,6 +372,9 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasDefaultValue(false)
.HasColumnName("EmailConfirmed");
b.Property<int>("EntityVersion")
.HasColumnType("int");
b.Property<string>("ExtraProperties")
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
@ -397,6 +403,9 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTimeOffset?>("LastPasswordChangeTime")
.HasColumnType("datetimeoffset");
b.Property<bool>("LockoutEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
@ -445,6 +454,9 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasColumnType("nvarchar(256)")
.HasColumnName("SecurityStamp");
b.Property<bool>("ShouldChangePasswordOnNextLogin")
.HasColumnType("bit");
b.Property<string>("Surname")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
@ -507,6 +519,33 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
b.ToTable("AbpUserClaims", (string)null);
});
modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("EndTime")
.HasColumnType("datetime2");
b.Property<Guid>("SourceUserId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("StartTime")
.HasColumnType("datetime2");
b.Property<Guid>("TargetUserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("AbpUserDelegations", (string)null);
});
modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b =>
{
b.Property<Guid>("UserId")
@ -647,6 +686,9 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasColumnType("nvarchar(128)")
.HasColumnName("DisplayName");
b.Property<int>("EntityVersion")
.HasColumnType("int");
b.Property<string>("ExtraProperties")
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
@ -708,6 +750,59 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
b.ToTable("AbpOrganizationUnitRoles", (string)null);
});
modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("ExtraProperties")
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<string>("GroupName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<byte>("MultiTenancySide")
.HasColumnType("tinyint");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ParentName")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Providers")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("StateCheckers")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("GroupName");
b.HasIndex("Name")
.IsUnique();
b.ToTable("AbpPermissions", (string)null);
});
modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b =>
{
b.Property<Guid>("Id")
@ -742,6 +837,34 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
b.ToTable("AbpPermissionGrants", (string)null);
});
modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("ExtraProperties")
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("AbpPermissionGroups", (string)null);
});
modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b =>
{
b.Property<Guid>("Id")
@ -1099,6 +1222,16 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Biography")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)")
.HasColumnName("Biography");
b.Property<string>("Company")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("Company");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasMaxLength(40)
@ -1121,10 +1254,25 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<string>("Github")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("Github");
b.Property<bool>("IsActive")
.HasColumnType("bit")
.HasColumnName("IsActive");
b.Property<string>("JobTitle")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)")
.HasColumnName("JobTitle");
b.Property<string>("Linkedin")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("Linkedin");
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
@ -1150,12 +1298,22 @@ namespace Volo.BloggingTestApp.EntityFrameworkCore.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Twitter")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("Twitter");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)")
.HasColumnName("UserName");
b.Property<string>("WebSite")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)")
.HasColumnName("WebSite");
b.HasKey("Id");
b.ToTable("BlgUsers", (string)null);

@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
using Volo.Blogging.Users;
namespace Volo.Blogging.Members;
public class CustomIdentityBlogUserUpdateDto
{
[StringLength(UserConsts.MaxNameLength)]
public string Name { get; set; }
[StringLength(UserConsts.MaxSurnameLength)]
public string Surname { get; set; }
[RegularExpression(@"^((?!\s).)*$", ErrorMessage= "PersonalSiteUrlValidationMessage")]
[StringLength(UserConsts.MaxWebSiteLength)]
public string WebSite { get; set; }
[RegularExpression(@"^((?!\s).)*$", ErrorMessage= "TwitterUserNameValidationMessage")]
[StringLength(UserConsts.MaxTwitterLength)]
public string Twitter { get; set; }
[RegularExpression(@"^((?!\s).)*$", ErrorMessage= "GitHubUserNameValidationMessage")]
[StringLength(UserConsts.MaxGithubLength)]
public string Github { get; set; }
[RegularExpression(@"^((?!\s).)*$", ErrorMessage= "LinkedinUrlValidationMessage")]
[StringLength(UserConsts.MaxLinkedinLength)]
public string Linkedin { get; set; }
[StringLength(UserConsts.MaxCompanyLength)]
public string Company { get; set; }
[StringLength(UserConsts.MaxJobTitleLength)]
public string JobTitle { get; set; }
[StringLength(UserConsts.MaxBiographyLength)]
public string Biography { get; set; }
}

@ -7,4 +7,6 @@ namespace Volo.Blogging.Members;
public interface IMemberAppService : IApplicationService
{
Task<BlogUserDto> FindAsync(string username);
Task UpdateUserProfileAsync(CustomIdentityBlogUserUpdateDto input);
}

@ -8,6 +8,10 @@ namespace Volo.Blogging.Posts
{
public Guid? TenantId { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
@ -17,7 +21,21 @@ namespace Volo.Blogging.Posts
public string PhoneNumber { get; set; }
public bool PhoneNumberConfirmed { get; set; }
public string WebSite { get; set; }
public string Twitter { get; set; }
public string Github { get; set; }
public string Linkedin { get; set; }
public string Company { get; set; }
public string JobTitle { get; set; }
public string Biography { get; set; }
public Dictionary<string, object> ExtraProperties { get; set; }
}
}

@ -23,5 +23,7 @@ namespace Volo.Blogging.Posts
Task<PostWithDetailsDto> UpdateAsync(Guid id, UpdatePostDto input);
Task<List<PostWithDetailsDto>> GetListByUserIdAsync(Guid userId);
Task<List<PostWithDetailsDto>> GetLatestBlogPostsAsync(Guid blogId, int count);
}
}

@ -26,4 +26,21 @@ public class MemberAppService : BloggingAppServiceBase, IMemberAppService
return ObjectMapper.Map<BlogUser, BlogUserDto>(user);
}
public async Task UpdateUserProfileAsync(CustomIdentityBlogUserUpdateDto input)
{
var user = await _userRepository.GetAsync(CurrentUser.Id.Value);
user.Name = input.Name;
user.Surname = input.Surname;
user.WebSite = input.WebSite;
user.Twitter = input.Twitter;
user.Github = input.Github;
user.Linkedin = input.Linkedin;
user.Company = input.Company;
user.JobTitle = input.JobTitle;
user.Biography = input.Biography;
await _userRepository.UpdateAsync(user);
}
}

@ -195,6 +195,36 @@ namespace Volo.Blogging.Posts
return ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts);
}
public async Task<List<PostWithDetailsDto>> GetLatestBlogPostsAsync(Guid blogId, int count)
{
var posts = await PostRepository.GetLatestBlogPostsAsync(blogId, count);
var userDictionary = new Dictionary<Guid, BlogUserDto>();
var postDtos = new List<PostWithDetailsDto>(ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts));
foreach (var postDto in postDtos)
{
if (!postDto.CreatorId.HasValue)
{
continue;
}
if (userDictionary.TryGetValue(postDto.CreatorId.Value, out var creatorUserDto))
{
postDto.Writer = creatorUserDto;
continue;
}
var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
if (creatorUser != null)
{
postDto.Writer = ObjectMapper.Map<BlogUser, BlogUserDto>(creatorUser);
userDictionary[creatorUser.Id] = postDto.Writer;
}
}
return new List<PostWithDetailsDto>(postDtos);
}
[Authorize(BloggingPermissions.Posts.Create)]
public async Task<PostWithDetailsDto> CreateAsync(CreatePostDto input)
{

@ -61,6 +61,18 @@
"FileUploadInfo": "Drag, drop, or paste a copied image.",
"PostDescriptionHint": "* Will be rendered in the article link preview, supports HTML",
"ReadMore": "Continue Reading",
"MemberNotPublishedPostYet": "No posts yet!"
"MemberNotPublishedPostYet": "No posts yet!",
"UpdateUserWebSiteInfo": "Example: https://johndoe.com",
"UpdateUserTwitterInfo": "Example: johndoe",
"UpdateUserGithubInfo": "Example: johndoe",
"UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...",
"UpdateUserCompanyInfo": "Example: Volosoft",
"UpdateUserJobTitleInfo": "Example: Software Developer",
"WebSite": "Web Site",
"UserName": "Username",
"FullURL": "Full URL",
"JobTitle": "Job Title",
"PersonalWebsite": "PERSONAL WEBSITE",
"EditProfile": "Edit Profile"
}
}

@ -0,0 +1,22 @@
namespace Volo.Blogging.Users;
public class UserConsts
{
public const int MaxNameLength = 64;
public const int MaxSurnameLength = 64;
public const int MaxBiographyLength = 1000;
public const int MaxWebSiteLength = 256;
public const int MaxTwitterLength = 128;
public const int MaxGithubLength = 256;
public const int MaxLinkedinLength = 256;
public const int MaxCompanyLength = 256;
public const int MaxJobTitleLength = 128;
}

@ -17,5 +17,7 @@ namespace Volo.Blogging.Posts
Task<List<Post>> GetOrderedList(Guid blogId,bool descending = false, CancellationToken cancellationToken = default);
Task<List<Post>> GetListByUserIdAsync(Guid userId, CancellationToken cancellationToken = default);
Task<List<Post>> GetLatestBlogPostsAsync(Guid blogId, int count, CancellationToken cancellationToken = default);
}
}

@ -1,4 +1,5 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Users;
@ -23,6 +24,27 @@ namespace Volo.Blogging.Users
public virtual string PhoneNumber { get; protected set; }
public virtual bool PhoneNumberConfirmed { get; protected set; }
[CanBeNull]
public virtual string WebSite { get; set; }
[CanBeNull]
public virtual string Twitter { get; set; }
[CanBeNull]
public virtual string Github { get; set; }
[CanBeNull]
public virtual string Linkedin { get; set; }
[CanBeNull]
public virtual string Company { get; set; }
[CanBeNull]
public virtual string JobTitle { get; set; }
[CanBeNull]
public virtual string Biography { get; set; }
protected BlogUser()
{

@ -26,8 +26,16 @@ namespace Volo.Blogging.EntityFrameworkCore
builder.Entity<BlogUser>(b =>
{
b.ToTable(AbpBloggingDbProperties.DbTablePrefix + "Users", AbpBloggingDbProperties.DbSchema);
b.ConfigureByConvention();
b.Property<string>(nameof(BlogUser.Biography)).HasMaxLength(UserConsts.MaxBiographyLength).HasColumnName(nameof(BlogUser.Biography));
b.Property<string>(nameof(BlogUser.WebSite)).HasMaxLength(UserConsts.MaxWebSiteLength).HasColumnName(nameof(BlogUser.WebSite));
b.Property<string>(nameof(BlogUser.Twitter)).HasMaxLength(UserConsts.MaxTwitterLength).HasColumnName(nameof(BlogUser.Twitter));
b.Property<string>(nameof(BlogUser.Github)).HasMaxLength(UserConsts.MaxGithubLength).HasColumnName(nameof(BlogUser.Github));
b.Property<string>(nameof(BlogUser.Linkedin)).HasMaxLength(UserConsts.MaxLinkedinLength).HasColumnName(nameof(BlogUser.Linkedin));
b.Property<string>(nameof(BlogUser.Company)).HasMaxLength(UserConsts.MaxCompanyLength).HasColumnName(nameof(BlogUser.Company));
b.Property<string>(nameof(BlogUser.JobTitle)).HasMaxLength(UserConsts.MaxJobTitleLength).HasColumnName(nameof(BlogUser.JobTitle));
b.ConfigureAbpUser();
b.ApplyObjectExtensionMappings();

@ -70,6 +70,15 @@ namespace Volo.Blogging.Posts
return await query.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<List<Post>> GetLatestBlogPostsAsync(Guid blogId, int count, CancellationToken cancellationToken = default)
{
var query = (await GetDbSetAsync()).Where(p => p.BlogId == blogId)
.OrderByDescending(p => p.CreationTime)
.Take(count);
return await query.ToListAsync(GetCancellationToken(cancellationToken));
}
public override async Task<IQueryable<Post>> WithDetailsAsync()
{
return (await GetQueryableAsync()).IncludeDetails();

@ -25,4 +25,12 @@ public partial class MembersClientProxy : ClientProxyBase<IMemberAppService>, IM
{ typeof(string), username }
});
}
public Task UpdateUserProfileAsync(CustomIdentityBlogUserUpdateDto input)
{
return RequestAsync(nameof(UpdateUserProfileAsync), new ClientProxyRequestTypeValue
{
{ typeof(CustomIdentityBlogUserUpdateDto), input }
});
}
}

@ -74,6 +74,15 @@ public partial class PostsClientProxy : ClientProxyBase<IPostAppService>, IPostA
});
}
public Task<List<PostWithDetailsDto>> GetLatestBlogPostsAsync(Guid blogId, int count)
{
return RequestAsync<List<PostWithDetailsDto>>(nameof(GetLatestBlogPostsAsync), new ClientProxyRequestTypeValue
{
{ typeof(Guid), blogId },
{ typeof(int), count }
});
}
public virtual async Task DeleteAsync(Guid id)
{
await RequestAsync(nameof(DeleteAsync), new ClientProxyRequestTypeValue

@ -69,6 +69,13 @@ namespace Volo.Blogging
return _postAppService.GetListByUserIdAsync(userId);
}
[HttpGet]
[Route("{blogId}/latest/{count}")]
public Task<List<PostWithDetailsDto>> GetLatestBlogPostsAsync(Guid blogId, int count)
{
return _postAppService.GetLatestBlogPostsAsync(blogId, count);
}
[HttpDelete]
[Route("{id}")]
public Task DeleteAsync(Guid id)

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Nito.AsyncEx;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
@ -66,5 +67,14 @@ namespace Volo.Blogging.Posts
return await query.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<List<Post>> GetLatestBlogPostsAsync(Guid blogId, int count, CancellationToken cancellationToken = default)
{
var query = (await GetMongoQueryableAsync(cancellationToken)).Where(x => x.BlogId == blogId)
.OrderByDescending(x => x.CreationTime)
.Take(count);
return await query.ToListAsync(GetCancellationToken(cancellationToken));
}
}
}

@ -1,4 +1,5 @@
@page
@using System.Globalization
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Http.Extensions
@using Microsoft.Extensions.Options
@ -11,6 +12,7 @@
@inject IAuthorizationService Authorization
@inject IOptionsSnapshot<BloggingTwitterOptions> twitterOptions
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Volo.Blogging.Localization
@using Volo.Blogging.Pages.Blog
@inject IHtmlLocalizer<BloggingResource> L
@ -49,314 +51,442 @@
}
<div class="vs-blog vs-blog-detail">
<abp-input asp-for="FocusCommentId" class="m-0" />
<div class="container">
<div class="row">
<div class="col-12 col-md-8 col-lg-7 mx-auto">
<section class="hero-section">
<div class="hero-articles">
<div class="hero-content">
<h1 class="mb-3" id="PostTitle">@Model.Post.Title</h1>
<abp-input asp-for="FocusCommentId" class="m-0"/>
<div class="container-xl">
<div class="row">
<div class="col-12 col-md-8 col-lg-8 mx-auto">
<section class="hero-section">
<div class="hero-articles">
<div class="hero-content">
<h1 class="mb-3" id="PostTitle">@Model.Post.Title</h1>
<div class="article-owner">
<div class="article-infos">
<div class="user-card mt-3 mb-4">
<div class="row">
<div class="col-auto pe-1">
@if (Model.Post.Writer != null)
{
<a href="/Members/@Model.Post.Writer.UserName" aria-label="Go to user profile">
<img gravatar-email="@Model.Post.Writer.Email" default-image="Identicon" class="article-avatar" alt="User Avatar"/>
</a>
}
</div>
<div class="col ps-1">
@if (Model.Post.Writer != null)
{
<h5 class="mt-2 mb-1">
<a href="/Members/@Model.Post.Writer.UserName">
@(Model.Post.Writer.UserName)
</a>
<span>@BloggingPageHelper.ConvertDatetimeToTimeAgo(Model.Post.CreationTime)</span>
</h5>
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", @Model.Post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Update))
{
<span class="seperator">|</span>
<a asp-page="./Edit" asp-route-postId="@Model.Post.Id" asp-route-blogShortName="@Model.BlogShortName">
<i class="fa fa-pencil"></i> @L["Edit"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Delete) || (CurrentUser.Id.HasValue && CurrentUser.Id == Model.Post.CreatorId))
{
<span class="seperator">|</span>
<a href="#" id="DeletePostLink" data-postid="@Model.Post.Id" data-blogShortName="@Model.BlogShortName">
<i class="fa fa-trash"></i> @L["Delete"]
</a>
}
<div class="float-end">
<a href="#" target="_blank" class="me-2" id="TwitterShareLink" title="Twitter">
<i class="fa fa-twitter fa-lg"></i>
</a>
<a href="#" target="_blank" class="me-2" id="LinkedinShareLink" title="LinkedIn">
<i class="fa fa-linkedin fa-lg"></i>
</a>
<a href="#" target="_blank" class="me-2" id="FacebookShareLink" title="Facebook">
<i class="fa fa-facebook fa-lg"></i>
</a>
<a href="#" target="_blank" class="me-2" id="EmailShareLink" title="E-mail">
<i class="fa fa-envelope-square fa-lg"></i>
</a>
<button class="copy-link me-2" id="CopyLink" title="CopyLink">
<i class="fa fa-link fa-lg"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="img-container mb-3">
<img src="@Model.Post.CoverImage" alt="Cover Image"/>
</div>
</div>
</section>
<section class="post-content">
<p>
@Html.Raw(BloggingPageHelper.RenderMarkdownToHtml(Model.Post.Content))
</p>
</section>
</div>
<div class="col-12 col-md-4 col-lg-3">
<div class="list-group">
<div class="col-auto pe-2">
@if (Model.Post.Writer != null)
{
<a href="/Members/@Model.Post.Writer.UserName" aria-label="Go to user profile">
<img gravatar-email="@Model.Post.Writer.Email" default-image="Identicon" class="article-avatar rounded-circle" alt="User Avatar"/>
</a>
}
</div>
<div class="col ps-1">
<h5 class="mt-2 mb-1">
<a href="/Members/@Model.Post.Writer.UserName">
@if (Model.Post.Writer.Name != null && Model.Post.Writer.Surname != null)
{
<p class="fw-bold pt-2 fs-5">@Model.Post.Writer.Name @Model.Post.Writer.Surname</p>
}
else
{
<p class="fw-bold pt-2 fs-5">@Model.Post.Writer.UserName</p>
}
</a>
</h5>
<div class="position-relative">
@if (Model.Post.Writer.JobTitle != null)
{
<p class="fw-lighter">@Model.Post.Writer.JobTitle</p>
}
</div>
@if (CurrentUser.UserName == Model.Post.Writer.UserName)
{
<a class="fw-lighter" href="/Members/@Model.Post.Writer.UserName#edit-profile">@L["EditProfile"] <i class="fas fa-edit"></i></a>
}
</div>
<hr/>
<p class="fs-3 fw-bold text-dark">More from Blog</p>
@for (var index = 0; index < Model.LatestPosts.Count && index < 5; index++)
{
if (Model.LatestPosts[index].Id != Model.Post.Id)
{
var post = Model.LatestPosts[index];
<section class="box-articles p-0">
<div class="row align-middle">
<div class="article-owner">
<div class="article-infos">
<div class="user-card mt-3 mb-4">
<div class="user-card pt-3">
<div class="row">
<div class="col-auto pe-1">
@if (Model.Post.Writer != null)
@if (post.Writer != null)
{
<a href="/Members/@Model.Post.Writer.UserName" aria-label="Go to user profile">
<img gravatar-email="@Model.Post.Writer.Email" default-image="Identicon" class="article-avatar" alt="User Avatar" />
<a href="/Members/@post.Writer.UserName" aria-label="Go to user profile">
<img gravatar-email="@post.Writer.Email" default-image="Identicon" class="last-post-image" alt="User Avatar"/>
</a>
}
</div>
<div class="col ps-1">
@if (Model.Post.Writer != null)
{
<h5 class="mt-2 mb-1">
<a href="/Members/@Model.Post.Writer.UserName">
@(Model.Post.Writer.UserName)
</a>
<span>@BloggingPageHelper.ConvertDatetimeToTimeAgo(Model.Post.CreationTime)</span>
</h5>
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", @Model.Post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Update))
{
<span class="seperator">|</span>
<a asp-page="./Edit" asp-route-postId="@Model.Post.Id" asp-route-blogShortName="@Model.BlogShortName">
<i class="fa fa-pencil"></i> @L["Edit"]
<div class="col">
<h5 class="last-post-name">
<a href="/Members/@post.Writer.UserName">
@(post.Writer.UserName)
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Posts.Delete) || (CurrentUser.Id.HasValue && CurrentUser.Id == Model.Post.CreatorId))
{
<span class="seperator">|</span>
<a href="#" id="DeletePostLink" data-postid="@Model.Post.Id" data-blogShortName="@Model.BlogShortName">
<i class="fa fa-trash"></i> @L["Delete"]
</a>
}
</h5>
</div>
</div>
</div>
</div>
<div class="col mt-2">
<p>
<a class="last-post-title" asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName">@post.Title</a>
</p>
</div>
</div>
</div>
<div class="img-container mb-3">
<img src="@Model.Post.CoverImage" alt="Cover Image" />
</div>
</div>
</section>
</div>
</section>
}
}
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-8 col-lg-8 mx-auto">
@if (Model.Post.Tags.Count > 0)
{
<div class="tags">
<h5>@L["TagsInThisArticle"]</h5>
@foreach (var tag in Model.Post.Tags)
{
<a asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
}
</div>
}
@if (Model.CommentsWithReplies.Count > 0)
{
<abp-row v-align="Start">
<abp-column size-sm="_12">
<p class="float-start"><i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]</p>
@if (hasCommentingPermission)
{
<a abp-button="Primary" class="btn-rounded float-end active" href="#LeaveComment">@L["LeaveComment"]</a>
}
else
{
<a abp-button="Primary" class="btn-rounded float-end active" href="/Account/Login?returnUrl=@System.Web.HttpUtility.UrlEncode(@Request.Path)">@L["LeaveComment"]</a>
}
</abp-column>
</abp-row>
<div class="row">
<div class="col-12 col-md-8 col-lg-7 mx-auto">
<section class="post-content">
<p>
@Html.Raw(BloggingPageHelper.RenderMarkdownToHtml(Model.Post.Content))
<div class="comment-area">
@foreach (var commentWithRepliesDto in Model.CommentsWithReplies)
{
<div class="media">
<img gravatar-email="@commentWithRepliesDto.Comment.Writer.Email" default-image="Identicon" class="d-flex me-3 rounded-circle comment-avatar" alt="User Avatar"/>
<div class="media-body">
<h5 class="comment-owner">
@(commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName)
<span>@BloggingPageHelper.ConvertDatetimeToTimeAgo(commentWithRepliesDto.Comment.CreationTime)</span>
</h5>
<p id="@commentWithRepliesDto.Comment.Id">
@commentWithRepliesDto.Comment.Text
</p>
</section>
</div>
</div>
<div class="comment-buttons">
<div class="row">
<div class="col-12 col-md-8 col-lg-7 mx-auto">
<hr />
<div class="mb-2 mt-1">
@(L["ShareOn"].Value + " :")
<a href="#" target="_blank" class="me-2" id="TwitterShareLink" title="Twitter">
Twitter <i class="fa fa-twitter"></i>
</a>
<a href="#" target="_blank" class="me-2" id="LinkedinShareLink" title="LinkedIn">
Linkedin <i class="fa fa-linkedin"></i>
</a>
<a href="#" target="_blank" class="me-2" id="EmailShareLink" title="E-mail">
E-mail <i class="fa fa-envelope-square"></i>
</a>
</div>
@if (Model.Post.Tags.Count > 0)
{
<div class="tags">
<h5>@L["TagsInThisArticle"]</h5>
@foreach (var tag in Model.Post.Tags)
@if (hasCommentingPermission)
{
<a asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@tag.Name" class="tag">@tag.Name</a>
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
</div>
}
@if (Model.CommentsWithReplies.Count > 0)
{
<abp-row v-align="Start">
<abp-column size-sm="_12">
<p class="float-start"><i class="fa fa-comment"></i> @L["CommentWithCount", @Model.CommentCount]</p>
@if (hasCommentingPermission)
{
<a abp-button="Primary" class="btn-rounded float-end active" href="#LeaveComment">@L["LeaveComment"]</a>
}
else
{
<a abp-button="Primary" class="btn-rounded float-end active" href="/Account/Login?returnUrl=@System.Web.HttpUtility.UrlEncode(@Request.Path)">@L["LeaveComment"]</a>
}
</abp-column>
</abp-row>
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
<div class="comment-area">
@foreach (var commentWithRepliesDto in Model.CommentsWithReplies)
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="media">
<img gravatar-email="@commentWithRepliesDto.Comment.Writer.Email" default-image="Identicon" class="d-flex me-3 rounded-circle comment-avatar" alt="User Avatar" />
<div class="media-body">
<h5 class="comment-owner">
@(commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName)
<span>@BloggingPageHelper.ConvertDatetimeToTimeAgo(commentWithRepliesDto.Comment.CreationTime)</span>
</h5>
<p id="@commentWithRepliesDto.Comment.Id">
@commentWithRepliesDto.Comment.Text
</p>
<div class="comment-buttons">
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
@if (hasCommentingPermission)
{
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix p-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" type="hidden"/>
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden/>
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Comment"].Value"/>
<abp-button button-type="Danger" class="btn-rounded float-end replyCancelButton" text="@L["Cancel"].Value"/>
</form>
</div>
</div>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix p-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@commentWithRepliesDto.Comment.Id" hidden/>
<input name="concurrencyStamp" value="@commentWithRepliesDto.Comment.ConcurrencyStamp" hidden/>
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4">@commentWithRepliesDto.Comment.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value"/>
<abp-button button-type="Danger" class="btn-rounded float-end editCancelButton" text="@L["Cancel"].Value"/>
</form>
</div>
</div>
</div>
}
@foreach (var reply in commentWithRepliesDto.Replies)
{
<div class="media">
<img gravatar-email="@reply.Writer.Email" default-image="Identicon" class="d-flex me-3 rounded-circle comment-avatar" alt="User Avatar"/>
<div class="media-body">
<h5 class="comment-owner">
@(reply.Writer == null ? "" : reply.Writer.UserName)
<span>@BloggingPageHelper.ConvertDatetimeToTimeAgo(reply.CreationTime)</span>
</h5>
<p id="@reply.Id">
@reply.Text
</p>
<div class="comment-buttons">
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix p-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" type="hidden" />
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Comment"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-end replyCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix p-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<input name="concurrencyStamp" value="@commentWithRepliesDto.Comment.ConcurrencyStamp" hidden />
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4">@commentWithRepliesDto.Comment.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-end editCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
</div>
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@reply.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
@foreach (var reply in commentWithRepliesDto.Replies)
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="media">
<img gravatar-email="@reply.Writer.Email" default-image="Identicon" class="d-flex me-3 rounded-circle comment-avatar" alt="User Avatar" />
<div class="media-body">
<h5 class="comment-owner">
@(reply.Writer == null ? "" : reply.Writer.UserName)
<span>@BloggingPageHelper.ConvertDatetimeToTimeAgo(reply.CreationTime)</span>
</h5>
<p id="@reply.Id">
@reply.Text
</p>
<div class="comment-buttons">
@if (hasCommentingPermission)
{
<a href="#" class="tag replyLink" data-relpyid="@commentWithRepliesDto.Comment.Id">
<i class="fa fa-reply" aria-hidden="true"></i> @L["Reply"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Delete) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag deleteLink" data-deleteid="@reply.Id">
<i class="fa fa-trash" aria-hidden="true"></i> @L["Delete"]
</a>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@reply.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
<span class="seperator">|</span>
<a href="#" class="tag updateLink" data-updateid="@reply.Id">
<i class="fa fa-pencil" aria-hidden="true"></i> @L["Edit"]
</a>
}
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix bg-light py-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden />
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-end replyCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
@if (hasCommentingPermission)
{
<div class="comment-form mt-4 replyForm">
<div class="clearfix bg-light py-4">
<h3 class="mt-0">
@L["ReplyTo", commentWithRepliesDto.Comment.Writer == null ? "" : commentWithRepliesDto.Comment.Writer.UserName]
</h3>
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden/>
<input name="repliedCommentId" value="@commentWithRepliesDto.Comment.Id" hidden/>
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4"></textarea>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix bg-light py-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@reply.Id" hidden />
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4">@reply.Text</textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value" />
<abp-button button-type="Danger" class="btn-rounded float-end editCancelButton" text="@L["Cancel"].Value" />
</form>
</div>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value"/>
<abp-button button-type="Danger" class="btn-rounded float-end replyCancelButton" text="@L["Cancel"].Value"/>
</form>
</div>
</div>
</div>
}
@if (await Authorization.IsGrantedAsync(BloggingPermissions.Comments.Update) || (CurrentUser.Id == commentWithRepliesDto.Comment.CreatorId))
{
<div class="comment-form mt-4 editForm">
<div class="clearfix bg-light py-4">
<div>
<form class="editFormClass">
<input name="commentId" value="@reply.Id" hidden/>
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4">@reply.Text</textarea>
</div>
}
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value"/>
<abp-button button-type="Danger" class="btn-rounded float-end editCancelButton" text="@L["Cancel"].Value"/>
</form>
</div>
</div>
}
</div>
</div>
}
</div>
}
@if (hasCommentingPermission)
{
<div class="comment-form mt-4" id="LeaveComment">
<div class="vs-blog-title mb-0">
<h3>@L["LeaveComment"]</h3>
</div>
<div class="clearfix bg-light py-4">
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden />
<input name="repliedCommentId" id="repliedCommentId" hidden />
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4"></textarea>
</div>
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value" />
</form>
}
</div>
</div>
}
</div>
</div>
}
</div>
}
@if (hasCommentingPermission)
{
<div class="comment-form mt-4" id="LeaveComment">
<div class="vs-blog-title mb-0">
<h3>@L["LeaveComment"]</h3>
</div>
<div class="clearfix bg-light py-4">
<div>
<form method="post">
<input name="postId" value="@Model.Post.Id" hidden/>
<input name="repliedCommentId" id="repliedCommentId" hidden/>
<div class="mb-3">
<textarea class="form-control" name="text" id="textBoxId" rows="4"></textarea>
</div>
}
else
{
<a abp-button="Primary" class="btn-rounded float-end active mt-3" href="/Account/Login?returnUrl=@System.Web.HttpUtility.UrlEncode(@Request.Path)">@L["LeaveComment"]</a>
}
<abp-button button-type="Primary" class="btn-rounded float-end" type="submit" text="@L["Submit"].Value"/>
</form>
</div>
</div>
</div>
}
else
{
<a abp-button="Primary" class="btn-rounded float-end active" href="/Account/Login?returnUrl=@System.Web.HttpUtility.UrlEncode(@Request.Path)">@L["LeaveComment"]</a>
}
<div class="hero articles">
<div class="hero content">
<p class="fs-3 fw-bold text-dark">More from @CultureInfo.CurrentCulture.TextInfo.ToTitleCase(Model.Post.Writer.UserName)</p>
@for (var index = 0; index < Model.PostsList.Count; index++)
{
if (Model.PostsList[index].Id != Model.Post.Id)
{
var post = Model.PostsList[index];
<div class="post-item">
<div class="post-type-cont">
<a href="/members/@Model.Post.Writer.UserName" class="text-decoration-none">
<img gravatar-email="@Model.Post.Writer.Email" default-image="Identicon" class="post-member-img rounded-circle d-block"/>
</a>
<span class="post-type">
<i class="fas fa-pen-nib"></i>
@L["Blog"].Value.ToUpper()
</span>
</div>
<div class="post-detail-cont">
<div class="post-info fs-12 mb-2">
<a href="/members/@Model.Post.Writer.UserName" class="text-decoration-none">
<span class="text-dark dot">@Model.Post.Writer.UserName</span>
</a>
<span class="text-dark-200">@post.CreationTime.ToString("MMMM yyyy")</span>
</div>
<h2 class="post-title mt-2">
<a asp-page="./Detail" asp-route-postUrl="@post.Url">
@post.Title
</a>
</h2>
<p class="post-desc">
<a asp-page="./Detail" asp-route-postUrl="@post.Url">
@post.Description.TruncateWithPostfix(150)
</a>
<a asp-page="./Detail" asp-route-postUrl="@post.Url" class="readMore">@L["ReadMore"]</a>
</p>
</div>
<div class="post-img-cont">
<div class="post-list-span text-center post">
<img src="@post.CoverImage" class="box-articles">
</div>
</div>
</div>
}
}
</div>
</div>
</div>
<div class="col-12 col-md-4 col-lg-3">
</div>
</div>
</div>
</div>
<p id="BlogFullName" name="@Model.Blog.Name" hidden></p>

@ -40,6 +40,13 @@ namespace Volo.Blogging.Pages.Blog.Posts
public BlogDto Blog { get; set; }
public List<PostWithDetailsDto> PostsList { get; set; }
public IReadOnlyList<PostWithDetailsDto> LatestPosts { get; set; }
[BindProperty(SupportsGet = true)]
public string TagName { get; set; }
public DetailModel(IPostAppService postAppService, IBlogAppService blogAppService, ICommentAppService commentAppService)
{
_postAppService = postAppService;
@ -79,6 +86,8 @@ namespace Volo.Blogging.Pages.Blog.Posts
{
Blog = await _blogAppService.GetByShortNameAsync(BlogShortName);
Post = await _postAppService.GetForReadingAsync(new GetPostInput { BlogId = Blog.Id, Url = PostUrl });
PostsList = await _postAppService.GetListByUserIdAsync(Post.Writer.Id);
LatestPosts = await _postAppService.GetLatestBlogPostsAsync(Blog.Id, 5);
CommentsWithReplies = await _commentAppService.GetHierarchicalListOfPostAsync(Post.Id);
CountComments();
}

@ -15,6 +15,12 @@
pageHeader + ' | ' + blogName + ' | ' + window.location.href
)
);
$('#FacebookShareLink').attr(
'href',
'https://www.facebook.com/sharer/sharer.php?u=' +
encodeURI(window.location.href)
);
$('#LinkedinShareLink').attr(
'href',
@ -44,7 +50,16 @@
'&'
);
};
$('#CopyLink').click(function (event) {
event.preventDefault();
var $temp = $('<input>');
$('body').append($temp);
$temp.val(window.location.href).select();
document.execCommand('copy');
$temp.remove();
});
$('div .replyForm').hide();
$('div .editForm').hide();

@ -320,3 +320,31 @@ div.vs-blog {
text-decoration: none; }
div.vs-blog > .form-group {
margin: 0 !important; }
.last-post-title{
font-size: 15px;
line-height: 20px;
font-weight: 700;
letter-spacing: 0px;
text-decoration: none;
color: black;
}
.last-post-image{
width: 30px;
height: 30px;
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
border-bottom-left-radius: 50%;
}
.last-post-name{
padding-right: 2px;
max-height: 16px;
padding-top: 0.5em;
padding-right: 0.5em;
}
#CopyLink{
padding: 0;
border: none;
background: none;
}

@ -1,7 +1,12 @@
@page
@using System.Globalization
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Utils
@using Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers
@using Volo.Abp.Users
@using Volo.Blogging
@using Volo.Blogging.Areas.Blog.Helpers.TagHelpers
@using Volo.Blogging.Localization
@model Volo.Blogging.Pages.Members.IndexModel
@inject IStringLocalizer<BloggingResource> L
@ -10,6 +15,10 @@
ViewBag.Title = @Model.User.UserName.ToUpper() + " - " + L["Blogs"].Value;
}
@section scripts {
<abp-script src="/Pages/Members/Index.js" />
}
@section styles {
<abp-style src="/Pages/Members/Index.css"/>
}
@ -23,74 +32,167 @@
<div class="d-inline-block position-relative">
<img gravatar-email="@Model.User.Email" default-image="Identicon" class="post-member-img rounded-circle d-block"/>
</div>
@if (Model.User.UserName != null)
@if (Model.User.Name != null && Model.User.Surname != null)
{
<h2 class="m-1">@Model.User.Name @Model.User.Surname</h2>
}
@if (Model.User.Company != null)
{
<h4 class="m-2">@Model.User.Company</h4>
}
@if (Model.User.JobTitle != null)
{
<h2 class="m-0">@Model.User.UserName</h2>
<h4>@Model.User.JobTitle</h4>
}
<small class="d-block mt-4">@L["UserName"].Value.ToUpper()</small>
<small class="d-block">@L["UserName"].Value.ToUpper()</small>
<h5>@Model.User.UserName</h5>
@if (Model.User.WebSite != null)
{
<small>@L["PersonalWebsite"].Value.ToUpper()</small>
<h5>
<a href="@Model.User.WebSite">@Model.User.WebSite</a>
</h5>
}
@if (Model.User.Twitter != null || Model.User.Github != null || Model.User.Linkedin != null)
{
<small>@L["Social"].Value.ToUpper()</small>
<ul class="d-flex justify-content-center">
@if (Model.User.Twitter != null)
{
<li class="mx-3">
<a href="https://twitter.com/@Model.User.Twitter">
<div class="icon-twitter-v1"></div>
</a>
</li>
}
@if (Model.User.Github != null)
{
<li class="mx-3">
<a href="https://github.com/@Model.User.Github">
<div class="icon-github large bg-dark"></div>
</a>
</li>
}
@if (Model.User.Linkedin != null)
{
<li class="mx-3">
<a href="@Model.User.Linkedin">
<div class="icon-linkedin-v1"></div>
</a>
</li>
}
</ul>
}
@if (Model.User.Biography != null)
{
<div class="m-2 mt-4">
<small>@L["Biography"].Value.ToUpper()</small>
<p>@Model.User.Biography</p>
</div>
}
</div>
</div>
</div>
@if (Model.Posts is not null && Model.Posts.Any())
{
<div class="col-md-8">
<div class="col-md-8">
<abp-tabs>
<abp-tab name="all-posts" title="All Blog Posts">
<div class="mt-4 pt-3">
@foreach (var post in Model.Posts)
{
<div class="post-item">
<div class="post-type-cont">
<a href="@Model.GetMemberProfileUrl(Model.User)" class="text-decoration-none">
<img gravatar-email="@Model.User.Email" default-image="Identicon" class="post-member-img rounded-circle d-block"/>
</a>
<span class="post-type">
<i class="fas fa-pen-nib"></i>
@L["Blog"].Value.ToUpper()
</span>
</div>
<div class="post-detail-cont">
<div class="post-info fs-12 mb-2">
<a href="@Model.GetMemberProfileUrl(Model.User)" class="text-decoration-none">
<span class="text-dark dot">@Model.User.UserName</span>
</a>
<span class="text-dark-200 dot">@post.CreationTime.ToString("MMMM yyyy")</span>
<span class="text-dark-200">@post.ReadCount.ToString() @L["Views"]</span>
</div>
<h3 class="post-title mb-3">
<a href="@Model.GetBlogPostUrl(post)">
@post.Title
</a>
</h3>
<p class="post-desc">
<a href="@Model.GetBlogPostUrl(post)">
@post.Description.TruncateWithPostfix(150)
</a>
<a href="@Model.GetBlogPostUrl(post)" class="readMore">@L["ReadMore"]</a>
</p>
</div>
<div class="post-img-cont">
<div class="post-list-span text-center post">
<img src="@post.CoverImage" class="box-articles">
</div>
@if (Model.Posts is not null && Model.Posts.Any())
{
<abp-tab name="all-posts" title="Blog Posts">
<div class="mt-4 pt-3">
@foreach (var post in Model.Posts)
{
<div class="post-item">
<div class="post-type-cont">
<a href="@Model.GetMemberProfileUrl(Model.User)" class="text-decoration-none">
<img gravatar-email="@Model.User.Email" default-image="Identicon" class="post-member-img rounded-circle d-block"/>
</a>
<span class="post-type">
<i class="fas fa-pen-nib"></i>
@L["Blog"].Value.ToUpper()
</span>
</div>
<div class="post-detail-cont">
<div class="post-info fs-12 mb-2">
<a href="@Model.GetMemberProfileUrl(Model.User)" class="text-decoration-none">
<span class="text-dark dot">@Model.User.UserName</span>
</a>
<span class="text-dark-200 dot">@post.CreationTime.ToString("MMMM yyyy")</span>
<span class="text-dark-200">@post.ReadCount.ToString() @L["Views"]</span>
</div>
<h3 class="post-title mb-3">
<a href="@Model.GetBlogPostUrl(post)">
@post.Title
</a>
</h3>
<p class="post-desc">
<a href="@Model.GetBlogPostUrl(post)">
@post.Description.TruncateWithPostfix(150)
</a>
<a href="@Model.GetBlogPostUrl(post)" class="readMore">@L["ReadMore"]</a>
</p>
</div>
<div class="post-img-cont">
<div class="post-list-span text-center post">
<img src="@post.CoverImage" class="box-articles">
</div>
</div>
</div>
}
</div>
</abp-tab>
}
else
{
<div class="col-md-8">
<div class="mt-5 pt-6">
<p>@L["MemberNotPublishedPostYet"]</p>
</div>
</div>
}
@if (CurrentUser.UserName == Model.User.UserName)
{
<abp-tab name="edit-profile" title="Edit Profile">
<div class="mt-4 mb-3 pt-3">
<form method="post">
<abp-row>
<abp-column size="_12" v-align="Center">
<abp-input class="form-control" asp-for="CustomUserUpdate.Name" required-symbol="false" label="@($"{L["Name"].Value} ({L["Optional"].Value})")" value="@Model.User.Name"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.Surname" required-symbol="false" label="@($"{L["Surname"].Value} ({L["Optional"].Value})")" value="@Model.User.Surname"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.WebSite" required-symbol="false" label="@($"{L["WebSite"].Value} ({L["Optional"].Value})")" value="@Model.User.WebSite" info="@L["UpdateUserWebSiteInfo"].Value"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.Twitter" required-symbol="false" label="Twitter @($"{L["UserName"].Value} ({L["Optional"].Value})")" value="@Model.User.Twitter" info="@L["UpdateUserTwitterInfo"].Value"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.Github" required-symbol="false" label="Github @($"{L["UserName"].Value} ({L["Optional"].Value})")" value="@Model.User.Github" info="@L["UpdateUserGithubInfo"].Value"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.Linkedin" required-symbol="false" label="Linkedin @($"{L["FullURL"].Value} ({L["Optional"].Value})")" value="@Model.User.Linkedin" info="@L["UpdateUserLinkedinInfo"].Value"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.Company" required-symbol="false" label="@($"{L["Company"].Value} ({L["Optional"].Value})")" value="@Model.User.Company" info="@L["UpdateUserCompanyInfo"].Value"></abp-input>
<abp-input class="form-control" asp-for="CustomUserUpdate.JobTitle" required-symbol="false" label="@($"{L["JobTitle"].Value} ({L["Optional"].Value})")" value="@Model.User.JobTitle" info="@L["UpdateUserJobTitleInfo"].Value"></abp-input>
<div class="mb-3">
<label class="form-label">@L["Biography"] (@L["Optional"])</label>
<textarea id="CustomUserUpdate_Biography" name="CustomUserUpdate.Biography" class="form-control " rows="7" data-val-length="The field Biography must be a string with a maximum length of 1000." data-val-length-max="1000" maxlength="1000">@Model.User.Biography</textarea>
</div>
</abp-column>
</abp-row>
<div class="d-grid gap-2">
<button id="btnSubmit" type="submit" class="btn btn-primary">@L["Submit"]</button>
</div>
</div>
}
</div>
</abp-tab>
</form>
</div>
</abp-tab>
}
</abp-tabs>
</div>
}
else
{
<div class="col-md-8">
<div class="mt-5 pt-6">
<p>@L["MemberNotPublishedPostYet"]</p>
</div>
</div>
}
</div>
</div>
</main>

@ -23,6 +23,9 @@ public class IndexModel : AbpPageModel
public Dictionary<Guid, string> BlogShortNameMap { get; set; }
[BindProperty]
public CustomIdentityBlogUserUpdateDto CustomUserUpdate { get; set; }
public IndexModel(IPostAppService postAppService, IMemberAppService memberAppService, IBlogAppService blogAppService)
{
_postAppService = postAppService;
@ -51,6 +54,14 @@ public class IndexModel : AbpPageModel
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
await _memberAppService.UpdateUserProfileAsync(CustomUserUpdate);
return Redirect($"/members/{CurrentUser.UserName}");
}
public string GetBlogPostUrl(PostWithDetailsDto post)
{
var blogShortName = BlogShortNameMap[post.BlogId];

@ -1,3 +1,6 @@
.post-desc {
overflow-wrap: break-word;
}
}
a:not(#all-posts-tab,#edit-profile-tab) {
color: unset!important;
}

@ -0,0 +1,9 @@
$(function () {
if (window.location.hash === '#edit-profile'){
$('#all-posts-tab').removeClass('active');
$('#all-posts').removeClass('show').removeClass('active');
$('#edit-profile-tab').addClass('active');
$('#edit-profile').addClass('show').addClass('active');
window.location.hash = '';
}
});
Loading…
Cancel
Save