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 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; }

View file

@ -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
);

View file

@ -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;

View file

@ -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;

View file

@ -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)

View file

@ -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");

View file

@ -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),

View file

@ -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");

View file

@ -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 (

View file

@ -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

View file

@ -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>

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 { 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>

View file

@ -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,

View file

@ -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'