Forum güncellemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-07-13 15:28:23 +03:00
parent 0ff184fcdf
commit 50cb3eaf97
14 changed files with 298 additions and 65 deletions

View file

@ -93,6 +93,10 @@ public class ForumTopicDto : FullAuditedEntityDto<Guid>
public Guid CategoryId { get; set; } public Guid CategoryId { get; set; }
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
public string AuthorName { 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 ViewCount { get; set; }
public int ReplyCount { get; set; } public int ReplyCount { get; set; }
public int LikeCount { get; set; } public int LikeCount { get; set; }
@ -155,6 +159,10 @@ public class ForumPostDto : FullAuditedEntityDto<Guid>
public string Content { get; set; } public string Content { get; set; }
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
public string AuthorName { 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 int LikeCount { get; set; }
public bool IsAcceptedAnswer { get; set; } public bool IsAcceptedAnswer { get; set; }

View file

@ -4,6 +4,8 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Authorization; using Volo.Abp.Authorization;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
@ -20,19 +22,24 @@ public class ForumAppService : PlatformAppService, IForumAppService
private readonly IRepository<ForumTopic, Guid> _topicRepository; private readonly IRepository<ForumTopic, Guid> _topicRepository;
private readonly IRepository<ForumPost, Guid> _postRepository; private readonly IRepository<ForumPost, Guid> _postRepository;
private readonly IIdentityUserRepository _identityUserRepository; private readonly IIdentityUserRepository _identityUserRepository;
private readonly IRepository<Department, Guid> _departmentRepository;
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
public ForumAppService( public ForumAppService(
IRepository<ForumCategory, Guid> categoryRepository, IRepository<ForumCategory, Guid> categoryRepository,
IRepository<ForumTopic, Guid> topicRepository, IRepository<ForumTopic, Guid> topicRepository,
IRepository<ForumPost, Guid> postRepository, IRepository<ForumPost, Guid> postRepository,
IIdentityUserRepository identityUserRepository) IIdentityUserRepository identityUserRepository,
IRepository<Department, Guid> departmentRepository,
IRepository<JobPosition, Guid> jobPositionRepository)
{ {
_categoryRepository = categoryRepository; _categoryRepository = categoryRepository;
_topicRepository = topicRepository; _topicRepository = topicRepository;
_postRepository = postRepository; _postRepository = postRepository;
_identityUserRepository = identityUserRepository; _identityUserRepository = identityUserRepository;
_departmentRepository = departmentRepository;
_jobPositionRepository = jobPositionRepository;
} }
// Search functionality // Search functionality
@ -337,13 +344,32 @@ public class ForumAppService : PlatformAppService, IForumAppService
[UnitOfWork] [UnitOfWork]
public async Task<ForumTopicDto> CreateTopicAsync(CreateForumTopicDto input) 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( var topic = new ForumTopic(
GuidGenerator.Create(), GuidGenerator.Create(),
input.Title, input.Title,
input.Content, input.Content,
input.CategoryId, input.CategoryId,
CurrentUser.Id.Value, 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 input.TenantId
) )
{ {
@ -438,12 +464,31 @@ public class ForumAppService : PlatformAppService, IForumAppService
[UnitOfWork] [UnitOfWork]
public async Task<ForumPostDto> CreatePostAsync(CreateForumPostDto input) 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( var post = new ForumPost(
GuidGenerator.Create(), GuidGenerator.Create(),
input.TopicId, input.TopicId,
input.Content, input.Content,
CurrentUser.Id.Value, 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.ParentPostId,
input.TenantId input.TenantId
); );

View file

@ -10,6 +10,10 @@ public class ForumPost : FullAuditedEntity<Guid>
public string Content { get; set; } public string Content { get; set; }
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
public string AuthorName { 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 int? LikeCount { get; set; }
public bool? IsAcceptedAnswer { get; set; } = false; public bool? IsAcceptedAnswer { get; set; } = false;
public Guid? ParentPostId { get; set; } public Guid? ParentPostId { get; set; }
@ -21,12 +25,27 @@ public class ForumPost : FullAuditedEntity<Guid>
protected ForumPost() { } 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; TopicId = topicId;
Content = content; Content = content;
AuthorId = authorId; AuthorId = authorId;
AuthorName = authorName; AuthorName = authorName;
AuthorTitle = authorTitle;
AuthorEmail = authorEmail;
AuthorPhoneNumber = authorPhoneNumber;
AuthorDepartment = authorDepartment;
ParentPostId = parentPostId; ParentPostId = parentPostId;
LikeCount = 0; LikeCount = 0;
IsAcceptedAnswer = false; IsAcceptedAnswer = false;

View file

@ -12,6 +12,10 @@ public class ForumTopic : FullAuditedEntity<Guid>
public ForumCategory Category { get; set; } public ForumCategory Category { get; set; }
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
public string AuthorName { 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 ViewCount { get; set; }
public int ReplyCount { get; set; } public int ReplyCount { get; set; }
public int LikeCount { get; set; } public int LikeCount { get; set; }
@ -28,13 +32,28 @@ public class ForumTopic : FullAuditedEntity<Guid>
protected ForumTopic() { } 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; Title = title;
Content = content; Content = content;
CategoryId = categoryId; CategoryId = categoryId;
AuthorId = authorId; AuthorId = authorId;
AuthorName = authorName; AuthorName = authorName;
AuthorTitle = authorTitle;
AuthorEmail = authorEmail;
AuthorPhoneNumber = authorPhoneNumber;
AuthorDepartment = authorDepartment;
ViewCount = 0; ViewCount = 0;
ReplyCount = 0; ReplyCount = 0;
LikeCount = 0; LikeCount = 0;

View file

@ -573,6 +573,10 @@ public class PlatformDbContext :
b.Property(x => x.Title).IsRequired().HasMaxLength(256); b.Property(x => x.Title).IsRequired().HasMaxLength(256);
b.Property(x => x.Content).IsRequired(); b.Property(x => x.Content).IsRequired();
b.Property(x => x.AuthorName).HasMaxLength(128); 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.Property(x => x.LastPostUserName).HasMaxLength(128);
b.HasMany(x => x.Posts) b.HasMany(x => x.Posts)
@ -588,6 +592,10 @@ public class PlatformDbContext :
b.Property(x => x.Content).IsRequired(); b.Property(x => x.Content).IsRequired();
b.Property(x => x.AuthorName).HasMaxLength(128); 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) b.HasOne(x => x.Topic)
.WithMany(x => x.Posts) .WithMany(x => x.Posts)

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260711200250_Initial")] [Migration("20260713103339_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -6303,6 +6303,14 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("Id") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("AuthorDepartment")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("AuthorEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("AuthorId") b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -6310,6 +6318,14 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("nvarchar(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") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@ -6373,6 +6389,14 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("Id") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("AuthorDepartment")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("AuthorEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("AuthorId") b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -6380,6 +6404,14 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("nvarchar(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") b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");

View file

@ -3022,6 +3022,10 @@ namespace Sozsoft.Platform.Migrations
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
AuthorId = 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), 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), ViewCount = table.Column<int>(type: "int", nullable: false),
ReplyCount = table.Column<int>(type: "int", nullable: false), ReplyCount = table.Column<int>(type: "int", nullable: false),
LikeCount = 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), Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
AuthorId = 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), 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), LikeCount = table.Column<int>(type: "int", nullable: true),
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: true), IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: true),
ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),

View file

@ -6300,6 +6300,14 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("Id") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("AuthorDepartment")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("AuthorEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("AuthorId") b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -6307,6 +6315,14 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("nvarchar(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") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@ -6370,6 +6386,14 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("Id") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("AuthorDepartment")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("AuthorEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid>("AuthorId") b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -6377,6 +6401,14 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("nvarchar(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") b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");

View file

@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom'
import { store } from './store' import { store } from './store'
import { DynamicRoutesProvider } from './routes/dynamicRoutesContext' import { DynamicRoutesProvider } from './routes/dynamicRoutesContext'
import { ComponentProvider } from './contexts/ComponentContext' import { ComponentProvider } from './contexts/ComponentContext'
import { registerServiceWorker } from './views/version/swRegistration'
import { useEffect } from 'react' import { useEffect } from 'react'
import DevExtremeBoundary from './components/devExtreme/DevExtremeBoundary' import DevExtremeBoundary from './components/devExtreme/DevExtremeBoundary'
@ -20,11 +21,7 @@ const queryClient = new QueryClient({
function App() { function App() {
useEffect(() => { useEffect(() => {
void import('./views/version/swRegistration') registerServiceWorker()
.then(({ registerServiceWorker }) => registerServiceWorker())
.catch((error: unknown) => {
console.warn('Service worker could not be registered.', error)
})
}, []) }, [])
return ( return (

View file

@ -23,6 +23,10 @@ export interface ForumTopic {
categoryId: string categoryId: string
authorId: string authorId: string
authorName: string authorName: string
authorTitle?: string
authorEmail?: string
authorPhoneNumber?: string
authorDepartment?: string
viewCount: number viewCount: number
replyCount: number replyCount: number
likeCount: number likeCount: number
@ -43,6 +47,10 @@ export interface ForumPost {
content: string content: string
authorId: string authorId: string
authorName: string authorName: string
authorTitle?: string
authorEmail?: string
authorPhoneNumber?: string
authorDepartment?: string
likeCount: number likeCount: number
isAcceptedAnswer: boolean isAcceptedAnswer: boolean
parentPostId?: string parentPostId?: string

View file

@ -1,8 +1,11 @@
import { useState } from 'react'
import { AnimatePresence } from 'framer-motion'
import { FaHeart, FaCheckCircle, FaReply } from 'react-icons/fa' import { FaHeart, FaCheckCircle, FaReply } from 'react-icons/fa'
import { ForumPost } from '@/proxy/forum/forum' import { ForumPost } from '@/proxy/forum/forum'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' 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' import { formatForumDate } from './utils'
interface PostCardProps { interface PostCardProps {
@ -21,28 +24,43 @@ export function ForumPostCard({
isLiked = false, isLiked = false,
}: PostCardProps) { }: PostCardProps) {
const { translate } = useLocalization() const { translate } = useLocalization()
const [showUserCard, setShowUserCard] = useState(false)
return ( return (
<div <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' : ''}`} 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 items-start space-x-4">
<div className="flex-shrink-0"> <div
<img className="relative flex-shrink-0"
src={AVATAR_URL(post.authorId, post.tenantId)} onMouseEnter={() => setShowUserCard(true)}
onError={(e) => { onMouseLeave={() => setShowUserCard(false)}
e.currentTarget.onerror = null >
e.currentTarget.src = '/img/others/default-profile.png' <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 || '',
}} }}
alt="User" position="bottom"
className="w-10 h-10 rounded-full"
/> />
)}
</AnimatePresence>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-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 && ( {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"> <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" /> <FaCheckCircle className="w-3 h-3" />
@ -67,14 +85,12 @@ export function ForumPostCard({
type="button" type="button"
variant="plain" variant="plain"
shape="none" 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)} 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> <span>{post.likeCount}</span>
</Button> </Button>
@ -83,8 +99,8 @@ export function ForumPostCard({
type="button" type="button"
variant="plain" variant="plain"
shape="none" 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" 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" /> <FaReply className="w-4 h-4" />
<span>{translate('::App.Forum.PostManagement.PostReply')}</span> <span>{translate('::App.Forum.PostManagement.PostReply')}</span>

View file

@ -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 { FaComment, FaHeart, FaEye, FaThumbtack, FaLock, FaCheckCircle } from 'react-icons/fa'
import { ForumTopic } from '@/proxy/forum/forum' import { ForumTopic } from '@/proxy/forum/forum'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Avatar } from '@/components/ui'
import UserProfileCard from '@/views/intranet/SocialWall/UserProfileCard'
import { formatForumRelativeDate } from './utils' import { formatForumRelativeDate } from './utils'
interface TopicCardProps { interface TopicCardProps {
@ -11,11 +15,12 @@ interface TopicCardProps {
export function ForumTopicCard({ topic, onClick }: TopicCardProps) { export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
const { translate } = useLocalization() const { translate } = useLocalization()
const [showUserCard, setShowUserCard] = useState(false)
return ( return (
<div <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" 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"> <div className="flex items-start justify-between">
{/* Sol taraf: Başlık, içerik, istatistik */} {/* Sol taraf: Başlık, içerik, istatistik */}
@ -29,7 +34,9 @@ export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
</h3> </h3>
</div> </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-4 text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center space-x-1" title={translate('::App.Platform.Views')}> <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 */} {/* Sağ taraf: Avatar + Yazar bilgisi */}
<div className="flex flex-col items-center justify-start w-24 text-center space-y-1"> <div className="flex flex-col items-center justify-start w-24 text-center space-y-1">
<img <div
src={AVATAR_URL(topic.authorId, topic.tenantId)} className="relative flex-shrink-0"
onError={(e) => { onClick={(event) => event.stopPropagation()}
e.currentTarget.onerror = null onMouseEnter={() => setShowUserCard(true)}
e.currentTarget.src = '/img/others/default-profile.png' 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 || '',
}} }}
alt="User" position="bottom"
className="w-10 h-10 rounded-full border"
/> />
<div className="text-sm font-medium text-gray-700 dark:text-gray-200">{topic.authorName}</div> )}
</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"> <div className="text-xs text-gray-500 dark:text-gray-400">
{formatForumRelativeDate(topic.creationTime)} {formatForumRelativeDate(topic.creationTime)}
</div> </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"> <div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<span> <span>
{translate('::App.Forum.TopicManagement.Lastreplyby')}{' '} {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>{formatForumRelativeDate(topic.lastPostDate)}</span>
</span> </span>
</div> </div>

View file

@ -225,7 +225,9 @@ export function ForumView({
isLiked={likedPosts.has(post.id)} isLiked={likedPosts.has(post.id)}
/> />
{post.children.length > 0 && ( {post.children.length > 0 && (
<div className="pl-6 border-gray-200 dark:border-gray-700 mt-4">{renderPosts(post.children)}</div> <div className="pl-6 border-gray-200 dark:border-gray-700 mt-4">
{renderPosts(post.children)}
</div>
)} )}
</div> </div>
)) ))
@ -289,7 +291,9 @@ export function ForumView({
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
{/* Left Side: Breadcrumb */} {/* Left Side: Breadcrumb */}
<div className="flex items-center space-x-2"> <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"> <nav className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
{selectedCategory && ( {selectedCategory && (
<> <>
@ -325,7 +329,9 @@ export function ForumView({
{selectedTopic && ( {selectedTopic && (
<> <>
<span>/</span> <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> </nav>
@ -437,6 +443,10 @@ export function ForumView({
content: selectedTopic.content, content: selectedTopic.content,
authorId: selectedTopic.authorId, authorId: selectedTopic.authorId,
authorName: selectedTopic.authorName, authorName: selectedTopic.authorName,
authorTitle: selectedTopic.authorTitle,
authorEmail: selectedTopic.authorEmail,
authorPhoneNumber: selectedTopic.authorPhoneNumber,
authorDepartment: selectedTopic.authorDepartment,
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount, likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
isAcceptedAnswer: false, isAcceptedAnswer: false,
parentPostId: undefined, parentPostId: undefined,

View file

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { motion } from 'framer-motion' 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_URL } from '@/constants/app.constant'
import { Avatar } from '@/components/ui' import { Avatar } from '@/components/ui'
@ -15,16 +15,21 @@ interface UserProfileCardProps {
tenantId: string tenantId: string
} }
position?: 'top' | 'bottom' 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 ( return (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }} initial={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }} exit={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
transition={{ duration: 0.15 }} 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' 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`} } 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)} /> <Avatar size={48} shape="circle" src={AVATAR_URL(user.id, user.tenantId)} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-lg mb-1"> <h3 className="font-bold text-gray-900 dark:text-gray-100 text-lg mb-1 text-left">{user.name}</h3>
{user.name} {user.title && (
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1"> <p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<FaBriefcase className="w-4 h-4" /> <FaBriefcase className="w-4 h-4" />
{user.title} {user.title}
</p> </p>
)}
</div> </div>
</div> </div>
@ -79,7 +84,7 @@ const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bot
{/* Arrow indicator */} {/* Arrow indicator */}
<div <div
className={`absolute left-6 ${ className={`absolute ${align === 'left' ? 'left-6' : 'right-3'} ${
position === 'bottom' ? '-top-2' : '-bottom-2' 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 ${ } 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' position === 'bottom' ? 'rotate-45' : '-rotate-135'