Forum güncellemeleri
This commit is contained in:
parent
0ff184fcdf
commit
50cb3eaf97
14 changed files with 298 additions and 65 deletions
|
|
@ -93,6 +93,10 @@ public class ForumTopicDto : FullAuditedEntityDto<Guid>
|
|||
public Guid CategoryId { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public string AuthorTitle { get; set; }
|
||||
public string AuthorEmail { get; set; }
|
||||
public string AuthorPhoneNumber { get; set; }
|
||||
public string AuthorDepartment { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public int ReplyCount { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
|
|
@ -155,6 +159,10 @@ public class ForumPostDto : FullAuditedEntityDto<Guid>
|
|||
public string Content { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public string AuthorTitle { get; set; }
|
||||
public string AuthorEmail { get; set; }
|
||||
public string AuthorPhoneNumber { get; set; }
|
||||
public string AuthorDepartment { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
public bool IsAcceptedAnswer { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Sozsoft.Platform.Extensions;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Authorization;
|
||||
using Volo.Abp.Domain.Entities;
|
||||
|
|
@ -20,19 +22,24 @@ public class ForumAppService : PlatformAppService, IForumAppService
|
|||
private readonly IRepository<ForumTopic, Guid> _topicRepository;
|
||||
private readonly IRepository<ForumPost, Guid> _postRepository;
|
||||
private readonly IIdentityUserRepository _identityUserRepository;
|
||||
private readonly IRepository<Department, Guid> _departmentRepository;
|
||||
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
|
||||
|
||||
|
||||
public ForumAppService(
|
||||
IRepository<ForumCategory, Guid> categoryRepository,
|
||||
IRepository<ForumTopic, Guid> topicRepository,
|
||||
IRepository<ForumPost, Guid> postRepository,
|
||||
IIdentityUserRepository identityUserRepository)
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IRepository<Department, Guid> departmentRepository,
|
||||
IRepository<JobPosition, Guid> jobPositionRepository)
|
||||
{
|
||||
_categoryRepository = categoryRepository;
|
||||
_topicRepository = topicRepository;
|
||||
_postRepository = postRepository;
|
||||
_identityUserRepository = identityUserRepository;
|
||||
|
||||
_departmentRepository = departmentRepository;
|
||||
_jobPositionRepository = jobPositionRepository;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
|
|
@ -337,13 +344,32 @@ public class ForumAppService : PlatformAppService, IForumAppService
|
|||
[UnitOfWork]
|
||||
public async Task<ForumTopicDto> CreateTopicAsync(CreateForumTopicDto input)
|
||||
{
|
||||
var author = await _identityUserRepository.FindAsync(CurrentUser.Id.Value)
|
||||
?? throw new EntityNotFoundException(typeof(IdentityUser), CurrentUser.Id.Value);
|
||||
var jobPositionId = author.GetJobPositionId();
|
||||
var jobPosition = jobPositionId == Guid.Empty
|
||||
? null
|
||||
: await _jobPositionRepository.FindAsync(jobPositionId);
|
||||
var departmentId = author.GetDepartmentId();
|
||||
if (departmentId == Guid.Empty && jobPosition != null)
|
||||
{
|
||||
departmentId = jobPosition.DepartmentId;
|
||||
}
|
||||
var department = departmentId == Guid.Empty
|
||||
? null
|
||||
: await _departmentRepository.FindAsync(departmentId);
|
||||
|
||||
var topic = new ForumTopic(
|
||||
GuidGenerator.Create(),
|
||||
input.Title,
|
||||
input.Content,
|
||||
input.CategoryId,
|
||||
CurrentUser.Id.Value,
|
||||
CurrentUser.Name,
|
||||
author.GetFullName(),
|
||||
jobPosition?.Name ?? string.Empty,
|
||||
author.Email ?? string.Empty,
|
||||
author.PhoneNumber ?? string.Empty,
|
||||
department?.Name ?? string.Empty,
|
||||
input.TenantId
|
||||
)
|
||||
{
|
||||
|
|
@ -438,12 +464,31 @@ public class ForumAppService : PlatformAppService, IForumAppService
|
|||
[UnitOfWork]
|
||||
public async Task<ForumPostDto> CreatePostAsync(CreateForumPostDto input)
|
||||
{
|
||||
var author = await _identityUserRepository.FindAsync(CurrentUser.Id.Value)
|
||||
?? throw new EntityNotFoundException(typeof(IdentityUser), CurrentUser.Id.Value);
|
||||
var jobPositionId = author.GetJobPositionId();
|
||||
var jobPosition = jobPositionId == Guid.Empty
|
||||
? null
|
||||
: await _jobPositionRepository.FindAsync(jobPositionId);
|
||||
var departmentId = author.GetDepartmentId();
|
||||
if (departmentId == Guid.Empty && jobPosition != null)
|
||||
{
|
||||
departmentId = jobPosition.DepartmentId;
|
||||
}
|
||||
var department = departmentId == Guid.Empty
|
||||
? null
|
||||
: await _departmentRepository.FindAsync(departmentId);
|
||||
|
||||
var post = new ForumPost(
|
||||
GuidGenerator.Create(),
|
||||
input.TopicId,
|
||||
input.Content,
|
||||
CurrentUser.Id.Value,
|
||||
CurrentUser.Name,
|
||||
author.GetFullName(),
|
||||
jobPosition?.Name ?? string.Empty,
|
||||
author.Email ?? string.Empty,
|
||||
author.PhoneNumber ?? string.Empty,
|
||||
department?.Name ?? string.Empty,
|
||||
input.ParentPostId,
|
||||
input.TenantId
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ public class ForumPost : FullAuditedEntity<Guid>
|
|||
public string Content { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public string AuthorTitle { get; set; }
|
||||
public string AuthorEmail { get; set; }
|
||||
public string AuthorPhoneNumber { get; set; }
|
||||
public string AuthorDepartment { get; set; }
|
||||
public int? LikeCount { get; set; }
|
||||
public bool? IsAcceptedAnswer { get; set; } = false;
|
||||
public Guid? ParentPostId { get; set; }
|
||||
|
|
@ -21,12 +25,27 @@ public class ForumPost : FullAuditedEntity<Guid>
|
|||
|
||||
protected ForumPost() { }
|
||||
|
||||
public ForumPost(Guid id, Guid topicId, string content, Guid authorId, string authorName, Guid? parentPostId = null, Guid? tenantId = null) : base(id)
|
||||
public ForumPost(
|
||||
Guid id,
|
||||
Guid topicId,
|
||||
string content,
|
||||
Guid authorId,
|
||||
string authorName,
|
||||
string authorTitle,
|
||||
string authorEmail,
|
||||
string authorPhoneNumber,
|
||||
string authorDepartment,
|
||||
Guid? parentPostId = null,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
TopicId = topicId;
|
||||
Content = content;
|
||||
AuthorId = authorId;
|
||||
AuthorName = authorName;
|
||||
AuthorTitle = authorTitle;
|
||||
AuthorEmail = authorEmail;
|
||||
AuthorPhoneNumber = authorPhoneNumber;
|
||||
AuthorDepartment = authorDepartment;
|
||||
ParentPostId = parentPostId;
|
||||
LikeCount = 0;
|
||||
IsAcceptedAnswer = false;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ public class ForumTopic : FullAuditedEntity<Guid>
|
|||
public ForumCategory Category { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public string AuthorTitle { get; set; }
|
||||
public string AuthorEmail { get; set; }
|
||||
public string AuthorPhoneNumber { get; set; }
|
||||
public string AuthorDepartment { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public int ReplyCount { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
|
|
@ -28,13 +32,28 @@ public class ForumTopic : FullAuditedEntity<Guid>
|
|||
|
||||
protected ForumTopic() { }
|
||||
|
||||
public ForumTopic(Guid id, string title, string content, Guid categoryId, Guid authorId, string authorName, Guid? tenantId = null) : base(id)
|
||||
public ForumTopic(
|
||||
Guid id,
|
||||
string title,
|
||||
string content,
|
||||
Guid categoryId,
|
||||
Guid authorId,
|
||||
string authorName,
|
||||
string authorTitle,
|
||||
string authorEmail,
|
||||
string authorPhoneNumber,
|
||||
string authorDepartment,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Title = title;
|
||||
Content = content;
|
||||
CategoryId = categoryId;
|
||||
AuthorId = authorId;
|
||||
AuthorName = authorName;
|
||||
AuthorTitle = authorTitle;
|
||||
AuthorEmail = authorEmail;
|
||||
AuthorPhoneNumber = authorPhoneNumber;
|
||||
AuthorDepartment = authorDepartment;
|
||||
ViewCount = 0;
|
||||
ReplyCount = 0;
|
||||
LikeCount = 0;
|
||||
|
|
|
|||
|
|
@ -573,6 +573,10 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Content).IsRequired();
|
||||
b.Property(x => x.AuthorName).HasMaxLength(128);
|
||||
b.Property(x => x.AuthorTitle).HasMaxLength(128);
|
||||
b.Property(x => x.AuthorEmail).HasMaxLength(256);
|
||||
b.Property(x => x.AuthorPhoneNumber).HasMaxLength(32);
|
||||
b.Property(x => x.AuthorDepartment).HasMaxLength(128);
|
||||
b.Property(x => x.LastPostUserName).HasMaxLength(128);
|
||||
|
||||
b.HasMany(x => x.Posts)
|
||||
|
|
@ -588,6 +592,10 @@ public class PlatformDbContext :
|
|||
|
||||
b.Property(x => x.Content).IsRequired();
|
||||
b.Property(x => x.AuthorName).HasMaxLength(128);
|
||||
b.Property(x => x.AuthorTitle).HasMaxLength(128);
|
||||
b.Property(x => x.AuthorEmail).HasMaxLength(256);
|
||||
b.Property(x => x.AuthorPhoneNumber).HasMaxLength(32);
|
||||
b.Property(x => x.AuthorDepartment).HasMaxLength(128);
|
||||
|
||||
b.HasOne(x => x.Topic)
|
||||
.WithMany(x => x.Posts)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260711200250_Initial")]
|
||||
[Migration("20260713103339_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -6303,6 +6303,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AuthorDepartment")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -6310,6 +6318,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorPhoneNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AuthorTitle")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
|
@ -6373,6 +6389,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AuthorDepartment")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -6380,6 +6404,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorPhoneNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AuthorTitle")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -3022,6 +3022,10 @@ namespace Sozsoft.Platform.Migrations
|
|||
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AuthorName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
AuthorTitle = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
AuthorEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
AuthorPhoneNumber = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: true),
|
||||
AuthorDepartment = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
ViewCount = table.Column<int>(type: "int", nullable: false),
|
||||
ReplyCount = table.Column<int>(type: "int", nullable: false),
|
||||
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||
|
|
@ -3392,6 +3396,10 @@ namespace Sozsoft.Platform.Migrations
|
|||
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AuthorName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
AuthorTitle = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
AuthorEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
AuthorPhoneNumber = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: true),
|
||||
AuthorDepartment = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
LikeCount = table.Column<int>(type: "int", nullable: true),
|
||||
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: true),
|
||||
ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
|
|
@ -6300,6 +6300,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AuthorDepartment")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -6307,6 +6315,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorPhoneNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AuthorTitle")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
|
@ -6370,6 +6386,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AuthorDepartment")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -6377,6 +6401,14 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("AuthorPhoneNumber")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("AuthorTitle")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom'
|
|||
import { store } from './store'
|
||||
import { DynamicRoutesProvider } from './routes/dynamicRoutesContext'
|
||||
import { ComponentProvider } from './contexts/ComponentContext'
|
||||
import { registerServiceWorker } from './views/version/swRegistration'
|
||||
import { useEffect } from 'react'
|
||||
import DevExtremeBoundary from './components/devExtreme/DevExtremeBoundary'
|
||||
|
||||
|
|
@ -20,11 +21,7 @@ const queryClient = new QueryClient({
|
|||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
void import('./views/version/swRegistration')
|
||||
.then(({ registerServiceWorker }) => registerServiceWorker())
|
||||
.catch((error: unknown) => {
|
||||
console.warn('Service worker could not be registered.', error)
|
||||
})
|
||||
registerServiceWorker()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export interface ForumTopic {
|
|||
categoryId: string
|
||||
authorId: string
|
||||
authorName: string
|
||||
authorTitle?: string
|
||||
authorEmail?: string
|
||||
authorPhoneNumber?: string
|
||||
authorDepartment?: string
|
||||
viewCount: number
|
||||
replyCount: number
|
||||
likeCount: number
|
||||
|
|
@ -43,6 +47,10 @@ export interface ForumPost {
|
|||
content: string
|
||||
authorId: string
|
||||
authorName: string
|
||||
authorTitle?: string
|
||||
authorEmail?: string
|
||||
authorPhoneNumber?: string
|
||||
authorDepartment?: string
|
||||
likeCount: number
|
||||
isAcceptedAnswer: boolean
|
||||
parentPostId?: string
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { useState } from 'react'
|
||||
import { AnimatePresence } from 'framer-motion'
|
||||
import { FaHeart, FaCheckCircle, FaReply } from 'react-icons/fa'
|
||||
import { ForumPost } from '@/proxy/forum/forum'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
import { Avatar, Button } from '@/components/ui'
|
||||
import UserProfileCard from '@/views/intranet/SocialWall/UserProfileCard'
|
||||
import { formatForumDate } from './utils'
|
||||
|
||||
interface PostCardProps {
|
||||
|
|
@ -21,28 +24,43 @@ export function ForumPostCard({
|
|||
isLiked = false,
|
||||
}: PostCardProps) {
|
||||
const { translate } = useLocalization()
|
||||
const [showUserCard, setShowUserCard] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 ${isFirst ? 'border-l-4 border-l-blue-500 dark:border-l-blue-400' : ''}`}
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={AVATAR_URL(post.authorId, post.tenantId)}
|
||||
onError={(e) => {
|
||||
e.currentTarget.onerror = null
|
||||
e.currentTarget.src = '/img/others/default-profile.png'
|
||||
}}
|
||||
alt="User"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div
|
||||
className="relative flex-shrink-0"
|
||||
onMouseEnter={() => setShowUserCard(true)}
|
||||
onMouseLeave={() => setShowUserCard(false)}
|
||||
>
|
||||
<Avatar shape="circle" size={40} src={AVATAR_URL(post.authorId, post.tenantId)} />
|
||||
<AnimatePresence>
|
||||
{showUserCard && (
|
||||
<UserProfileCard
|
||||
user={{
|
||||
id: post.authorId,
|
||||
name: post.authorName,
|
||||
title: post.authorTitle || '',
|
||||
email: post.authorEmail,
|
||||
phoneNumber: post.authorPhoneNumber,
|
||||
department: post.authorDepartment,
|
||||
tenantId: post.tenantId || '',
|
||||
}}
|
||||
position="bottom"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">{post.authorName}</h4>
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{post.authorName}
|
||||
</h4>
|
||||
{post.isAcceptedAnswer && (
|
||||
<div className="flex items-center space-x-1 bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-200 px-2 py-1 rounded-full text-xs">
|
||||
<FaCheckCircle className="w-3 h-3" />
|
||||
|
|
@ -67,14 +85,12 @@ export function ForumPostCard({
|
|||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
className="!inline-flex !h-auto items-center gap-1 !rounded-none !border-0 !bg-transparent !px-0 py-1 text-sm text-gray-600 transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent dark:text-gray-300"
|
||||
onClick={() => onLike(post.id, isFirst)}
|
||||
className={`!inline-flex !h-auto items-center gap-1 rounded-full !px-3 py-1 text-sm transition-colors ${
|
||||
isLiked
|
||||
? '!bg-red-100 text-red-600 hover:!bg-red-200 dark:!bg-red-900 dark:text-red-200 dark:hover:!bg-red-800'
|
||||
: '!bg-gray-100 text-gray-600 hover:!bg-gray-200 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
|
||||
<FaHeart
|
||||
className={`h-4 w-4 ${isLiked ? 'text-red-500' : 'text-gray-500 dark:text-gray-400'}`}
|
||||
/>
|
||||
<span>{post.likeCount}</span>
|
||||
</Button>
|
||||
|
||||
|
|
@ -83,8 +99,8 @@ export function ForumPostCard({
|
|||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => onReply(post.id)}
|
||||
className="!inline-flex !h-auto items-center gap-1 rounded-full !bg-gray-100 !px-3 py-1 text-sm text-gray-600 transition-colors hover:!bg-gray-200 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700"
|
||||
onClick={() => onReply(post.id)}
|
||||
>
|
||||
<FaReply className="w-4 h-4" />
|
||||
<span>{translate('::App.Forum.PostManagement.PostReply')}</span>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { useState } from 'react'
|
||||
import { AnimatePresence } from 'framer-motion'
|
||||
import { FaComment, FaHeart, FaEye, FaThumbtack, FaLock, FaCheckCircle } from 'react-icons/fa'
|
||||
import { ForumTopic } from '@/proxy/forum/forum'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Avatar } from '@/components/ui'
|
||||
import UserProfileCard from '@/views/intranet/SocialWall/UserProfileCard'
|
||||
import { formatForumRelativeDate } from './utils'
|
||||
|
||||
interface TopicCardProps {
|
||||
|
|
@ -11,11 +15,12 @@ interface TopicCardProps {
|
|||
|
||||
export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
|
||||
const { translate } = useLocalization()
|
||||
const [showUserCard, setShowUserCard] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md hover:border-blue-200 dark:hover:border-blue-500 transition-all duration-200 cursor-pointer group"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Sol taraf: Başlık, içerik, istatistik */}
|
||||
|
|
@ -29,7 +34,9 @@ export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
|
|||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2">{topic.content}</p>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2">
|
||||
{topic.content}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center space-x-1" title={translate('::App.Platform.Views')}>
|
||||
|
|
@ -49,16 +56,34 @@ export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
|
|||
|
||||
{/* Sağ taraf: Avatar + Yazar bilgisi */}
|
||||
<div className="flex flex-col items-center justify-start w-24 text-center space-y-1">
|
||||
<img
|
||||
src={AVATAR_URL(topic.authorId, topic.tenantId)}
|
||||
onError={(e) => {
|
||||
e.currentTarget.onerror = null
|
||||
e.currentTarget.src = '/img/others/default-profile.png'
|
||||
}}
|
||||
alt="User"
|
||||
className="w-10 h-10 rounded-full border"
|
||||
/>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-200">{topic.authorName}</div>
|
||||
<div
|
||||
className="relative flex-shrink-0"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onMouseEnter={() => setShowUserCard(true)}
|
||||
onMouseLeave={() => setShowUserCard(false)}
|
||||
>
|
||||
<Avatar shape="circle" size={40} src={AVATAR_URL(topic.authorId, topic.tenantId)} />
|
||||
<AnimatePresence>
|
||||
{showUserCard && (
|
||||
<UserProfileCard
|
||||
align="right"
|
||||
user={{
|
||||
id: topic.authorId,
|
||||
name: topic.authorName,
|
||||
title: topic.authorTitle || '',
|
||||
email: topic.authorEmail,
|
||||
phoneNumber: topic.authorPhoneNumber,
|
||||
department: topic.authorDepartment,
|
||||
tenantId: topic.tenantId || '',
|
||||
}}
|
||||
position="bottom"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{topic.authorName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatForumRelativeDate(topic.creationTime)}
|
||||
</div>
|
||||
|
|
@ -70,8 +95,9 @@ export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
|
|||
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>
|
||||
{translate('::App.Forum.TopicManagement.Lastreplyby')}{' '}
|
||||
<span className="font-medium text-gray-700 dark:text-gray-200">{topic.lastPostUserName}</span>
|
||||
{' '}
|
||||
<span className="font-medium text-gray-700 dark:text-gray-200">
|
||||
{topic.lastPostUserName}
|
||||
</span>{' '}
|
||||
<span>{formatForumRelativeDate(topic.lastPostDate)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -224,9 +224,11 @@ export function ForumView({
|
|||
onReply={handleReply}
|
||||
isLiked={likedPosts.has(post.id)}
|
||||
/>
|
||||
{post.children.length > 0 && (
|
||||
<div className="pl-6 border-gray-200 dark:border-gray-700 mt-4">{renderPosts(post.children)}</div>
|
||||
)}
|
||||
{post.children.length > 0 && (
|
||||
<div className="pl-6 border-gray-200 dark:border-gray-700 mt-4">
|
||||
{renderPosts(post.children)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
|
@ -277,7 +279,7 @@ export function ForumView({
|
|||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<FaSpinner className="w-8 h-8 animate-spin text-blue-600" />
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-300">Loading forum data...</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-300">Loading forum data...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -289,7 +291,9 @@ export function ForumView({
|
|||
<div className="flex items-center justify-between mb-4">
|
||||
{/* Left Side: Breadcrumb */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{viewState !== 'categories' && <FaArrowLeft className="w-4 h-4 text-gray-700 dark:text-gray-200" />}
|
||||
{viewState !== 'categories' && (
|
||||
<FaArrowLeft className="w-4 h-4 text-gray-700 dark:text-gray-200" />
|
||||
)}
|
||||
<nav className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{selectedCategory && (
|
||||
<>
|
||||
|
|
@ -303,8 +307,8 @@ export function ForumView({
|
|||
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
||||
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Forum</div>
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Forum</div>
|
||||
</Button>
|
||||
<span>/</span>
|
||||
<Button
|
||||
|
|
@ -325,7 +329,9 @@ export function ForumView({
|
|||
{selectedTopic && (
|
||||
<>
|
||||
<span>/</span>
|
||||
<span className="text-gray-900 dark:text-gray-100 font-medium">{selectedTopic.title}</span>
|
||||
<span className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{selectedTopic.title}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
|
@ -437,6 +443,10 @@ export function ForumView({
|
|||
content: selectedTopic.content,
|
||||
authorId: selectedTopic.authorId,
|
||||
authorName: selectedTopic.authorName,
|
||||
authorTitle: selectedTopic.authorTitle,
|
||||
authorEmail: selectedTopic.authorEmail,
|
||||
authorPhoneNumber: selectedTopic.authorPhoneNumber,
|
||||
authorDepartment: selectedTopic.authorDepartment,
|
||||
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
|
||||
isAcceptedAnswer: false,
|
||||
parentPostId: undefined,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaEnvelope, FaPhone, FaBriefcase, FaMapMarkerAlt } from 'react-icons/fa'
|
||||
import { FaEnvelope, FaPhone, FaBriefcase } from 'react-icons/fa'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { Avatar } from '@/components/ui'
|
||||
|
||||
|
|
@ -15,16 +15,21 @@ interface UserProfileCardProps {
|
|||
tenantId: string
|
||||
}
|
||||
position?: 'top' | 'bottom'
|
||||
align?: 'left' | 'right'
|
||||
}
|
||||
|
||||
const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bottom' }) => {
|
||||
const UserProfileCard: React.FC<UserProfileCardProps> = ({
|
||||
user,
|
||||
position = 'bottom',
|
||||
align = 'left',
|
||||
}) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={`absolute left-0 ${
|
||||
className={`absolute ${align === 'left' ? 'left-0' : 'right-0'} ${
|
||||
position === 'bottom' ? 'top-full mt-2' : 'bottom-full mb-2'
|
||||
} z-50 w-72 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 p-4`}
|
||||
>
|
||||
|
|
@ -33,13 +38,13 @@ const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bot
|
|||
<Avatar size={48} shape="circle" src={AVATAR_URL(user.id, user.tenantId)} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-lg mb-1">
|
||||
{user.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
|
||||
<FaBriefcase className="w-4 h-4" />
|
||||
{user.title}
|
||||
</p>
|
||||
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-lg mb-1 text-left">{user.name}</h3>
|
||||
{user.title && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
|
||||
<FaBriefcase className="w-4 h-4" />
|
||||
{user.title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -56,7 +61,7 @@ const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bot
|
|||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{user.phoneNumber && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FaPhone className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
|
|
@ -79,7 +84,7 @@ const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bot
|
|||
|
||||
{/* Arrow indicator */}
|
||||
<div
|
||||
className={`absolute left-6 ${
|
||||
className={`absolute ${align === 'left' ? 'left-6' : 'right-3'} ${
|
||||
position === 'bottom' ? '-top-2' : '-bottom-2'
|
||||
} w-4 h-4 bg-white dark:bg-gray-800 border-l border-t border-gray-200 dark:border-gray-700 transform ${
|
||||
position === 'bottom' ? 'rotate-45' : '-rotate-135'
|
||||
|
|
|
|||
Loading…
Reference in a new issue