Eventlere Images ve Yorumlar
This commit is contained in:
parent
c97e7c4afa
commit
6fa266f23e
17 changed files with 680 additions and 196 deletions
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using Sozsoft.Platform.Identity.Dto;
|
||||
|
||||
namespace Sozsoft.Platform.Intranet;
|
||||
|
||||
public class EventCommentDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid EventId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public UserInfoViewModel User { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int Likes { get; set; }
|
||||
public DateTime CreationTime { get; set; }
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Sozsoft.Platform.Identity.Dto;
|
||||
|
||||
namespace Sozsoft.Platform.Intranet;
|
||||
|
|
@ -18,5 +19,7 @@ public class EventDto
|
|||
public int ParticipantsCount { get; set; }
|
||||
public int Likes { get; set; }
|
||||
public bool IsPublished { get; set; }
|
||||
public string Photos { get; set; }
|
||||
public List<EventCommentDto> Comments { get; set; } = [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Volo.Abp.Application.Services;
|
||||
|
||||
namespace Sozsoft.Platform.Intranet;
|
||||
|
|
@ -13,4 +15,6 @@ public interface IIntranetAppService : IApplicationService
|
|||
Task<SocialCommentDto> CommentSocialPostAsync(System.Guid id, string content);
|
||||
Task VoteSocialPollAsync(System.Guid postId, System.Guid optionId);
|
||||
Task IncrementAnnouncementViewCountAsync(System.Guid id);
|
||||
Task<List<EventCommentDto>> GetEventCommentsAsync(Guid eventId);
|
||||
Task<EventCommentDto> CreateEventCommentAsync(Guid eventId, string content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
private readonly IRepository<SocialLike, Guid> _socialLikeRepository;
|
||||
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
|
||||
private readonly IRepository<SocialPollOption, Guid> _socialPollOptionRepository;
|
||||
private readonly IRepository<EventComment, Guid> _eventCommentRepository;
|
||||
|
||||
public IntranetAppService(
|
||||
ICurrentTenant currentTenant,
|
||||
|
|
@ -61,7 +62,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
IRepository<SocialComment, Guid> socialCommentRepository,
|
||||
IRepository<SocialLike, Guid> socialLikeRepository,
|
||||
IRepository<SocialMedia, Guid> socialMediaRepository,
|
||||
IRepository<SocialPollOption, Guid> socialPollOptionRepository
|
||||
IRepository<SocialPollOption, Guid> socialPollOptionRepository,
|
||||
IRepository<EventComment, Guid> eventCommentRepository
|
||||
)
|
||||
{
|
||||
_currentTenant = currentTenant;
|
||||
|
|
@ -81,6 +83,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
_socialLikeRepository = socialLikeRepository;
|
||||
_socialMediaRepository = socialMediaRepository;
|
||||
_socialPollOptionRepository = socialPollOptionRepository;
|
||||
_eventCommentRepository = eventCommentRepository;
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
|
|
@ -130,14 +133,24 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
if (events.Count == 0)
|
||||
return [];
|
||||
|
||||
// Tüm unique user ID'lerini topla (event'ler ve comment'ler için)
|
||||
var eventIds = events.Select(e => e.Id).ToList();
|
||||
|
||||
// Load all comments for these events
|
||||
var commentsQueryable = await _eventCommentRepository.GetQueryableAsync();
|
||||
var allComments = await AsyncExecuter.ToListAsync(
|
||||
commentsQueryable.Where(c => eventIds.Contains(c.EventId)).OrderBy(c => c.CreationTime)
|
||||
);
|
||||
|
||||
// Collect all unique user IDs
|
||||
var userIds = new HashSet<Guid>();
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt.UserId.HasValue)
|
||||
{
|
||||
userIds.Add(evt.UserId.Value);
|
||||
}
|
||||
}
|
||||
foreach (var comment in allComments)
|
||||
{
|
||||
userIds.Add(comment.UserId);
|
||||
}
|
||||
|
||||
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
|
||||
|
|
@ -147,12 +160,33 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict));
|
||||
|
||||
var commentsByEvent = allComments.GroupBy(c => c.EventId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
var result = new List<EventDto>();
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (!evt.UserId.HasValue || !userDict.TryGetValue(evt.UserId.Value, out var user))
|
||||
continue;
|
||||
|
||||
var commentDtos = new List<EventCommentDto>();
|
||||
if (commentsByEvent.TryGetValue(evt.Id, out var eventComments))
|
||||
{
|
||||
foreach (var c in eventComments)
|
||||
{
|
||||
commentDtos.Add(new EventCommentDto
|
||||
{
|
||||
Id = c.Id,
|
||||
EventId = c.EventId,
|
||||
UserId = c.UserId,
|
||||
User = userDict.TryGetValue(c.UserId, out var commentUser) ? commentUser : null,
|
||||
Content = c.Content,
|
||||
Likes = c.Likes,
|
||||
CreationTime = c.CreationTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var calendarEvent = new EventDto
|
||||
{
|
||||
Id = evt.Id,
|
||||
|
|
@ -165,7 +199,9 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
User = user,
|
||||
ParticipantsCount = evt.ParticipantsCount,
|
||||
Likes = evt.Likes,
|
||||
IsPublished = evt.isPublished
|
||||
IsPublished = evt.isPublished,
|
||||
Photos = evt.Photos,
|
||||
Comments = commentDtos
|
||||
};
|
||||
|
||||
result.Add(calendarEvent);
|
||||
|
|
@ -174,6 +210,63 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<EventCommentDto>> GetEventCommentsAsync(Guid eventId)
|
||||
{
|
||||
var commentsQueryable = await _eventCommentRepository.GetQueryableAsync();
|
||||
var comments = await AsyncExecuter.ToListAsync(
|
||||
commentsQueryable.Where(c => c.EventId == eventId).OrderBy(c => c.CreationTime)
|
||||
);
|
||||
|
||||
if (comments.Count == 0)
|
||||
return [];
|
||||
|
||||
var userIds = comments.Select(c => c.UserId).Distinct().ToList();
|
||||
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
|
||||
var users = await _identityUserRepository.GetListAsync();
|
||||
var userDict = users
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict));
|
||||
|
||||
return comments.Select(c => new EventCommentDto
|
||||
{
|
||||
Id = c.Id,
|
||||
EventId = c.EventId,
|
||||
UserId = c.UserId,
|
||||
User = userDict.TryGetValue(c.UserId, out var u) ? u : null,
|
||||
Content = c.Content,
|
||||
Likes = c.Likes,
|
||||
CreationTime = c.CreationTime
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<EventCommentDto> CreateEventCommentAsync(Guid eventId, string content)
|
||||
{
|
||||
var comment = new EventComment
|
||||
{
|
||||
EventId = eventId,
|
||||
UserId = CurrentUser.Id ?? Guid.Empty,
|
||||
Content = content,
|
||||
Likes = 0
|
||||
};
|
||||
|
||||
comment = await _eventCommentRepository.InsertAsync(comment, autoSave: true);
|
||||
|
||||
var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync();
|
||||
var user = await _identityUserRepository.FindAsync(comment.UserId);
|
||||
var userViewModel = user != null ? MapUserInfoViewModel(user, departmentDict, jobPositionDict) : null;
|
||||
|
||||
return new EventCommentDto
|
||||
{
|
||||
Id = comment.Id,
|
||||
EventId = comment.EventId,
|
||||
UserId = comment.UserId,
|
||||
User = userViewModel,
|
||||
Content = comment.Content,
|
||||
Likes = comment.Likes,
|
||||
CreationTime = comment.CreationTime
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<UserInfoViewModel>> GetBirthdaysAsync()
|
||||
{
|
||||
var today = DateTime.Now;
|
||||
|
|
|
|||
|
|
@ -12842,6 +12842,12 @@
|
|||
}
|
||||
],
|
||||
"LanguageFieldTitles": [
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.Photos",
|
||||
"en": "Photos",
|
||||
"tr": "Fotoğraflar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.WorkHour",
|
||||
|
|
|
|||
|
|
@ -4176,6 +4176,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
new EditingFormItemDto { Order = 6, DataField = "UserId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 7, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea },
|
||||
new EditingFormItemDto { Order = 8, DataField = "Status", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 9, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions },
|
||||
]}
|
||||
}),
|
||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
||||
|
|
@ -4393,6 +4394,22 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson
|
||||
},
|
||||
// Photos
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Photos",
|
||||
CaptionName = "App.Listform.ListformField.Photos",
|
||||
Width = 250,
|
||||
ListOrderNo = 12,
|
||||
Visible = false,
|
||||
IsActive = true,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson
|
||||
}
|
||||
});
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
|
|
@ -26,6 +27,10 @@ public class Event : FullAuditedEntity<Guid>, IMultiTenant
|
|||
public int Likes { get; set; }
|
||||
public bool isPublished { get; set; } = false;
|
||||
|
||||
public string Photos { get; set; }
|
||||
|
||||
public ICollection<EventComment> Comments { get; set; } = [];
|
||||
|
||||
Guid? IMultiTenant.TenantId => TenantId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Sozsoft.Platform.Entities;
|
||||
|
||||
public class EventComment : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public Guid EventId { get; set; }
|
||||
public Event Event { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int Likes { get; set; }
|
||||
|
||||
Guid? IMultiTenant.TenantId => TenantId;
|
||||
}
|
||||
|
|
@ -113,6 +113,7 @@ public class PlatformDbContext :
|
|||
public DbSet<Event> Events { get; set; }
|
||||
public DbSet<EventCategory> EventCategories { get; set; }
|
||||
public DbSet<EventType> EventTypes { get; set; }
|
||||
public DbSet<EventComment> EventComments { get; set; }
|
||||
|
||||
public DbSet<Announcement> Announcements { get; set; }
|
||||
|
||||
|
|
@ -1239,6 +1240,7 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.Description).HasMaxLength(1024);
|
||||
b.Property(x => x.Status).HasMaxLength(20);
|
||||
b.Property(x => x.isPublished).HasDefaultValue(false);
|
||||
b.Property(x => x.Photos).HasColumnType("text");
|
||||
|
||||
b.HasOne(x => x.Category)
|
||||
.WithMany(x => x.Events)
|
||||
|
|
@ -1250,5 +1252,20 @@ public class PlatformDbContext :
|
|||
.HasForeignKey(x => x.TypeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<EventComment>(b =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.EventComment)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Content).HasMaxLength(512);
|
||||
b.Property(x => x.Likes).HasDefaultValue(0);
|
||||
|
||||
// Event -> EventComment (1 - N)
|
||||
b.HasOne(x => x.Event)
|
||||
.WithMany(x => x.Comments)
|
||||
.HasForeignKey(x => x.EventId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260506141749_Initial")]
|
||||
[Migration("20260507140651_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -603,7 +603,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
@ -2001,6 +2001,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<int>("ParticipantsCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Photos")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Place")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
|
@ -2133,7 +2136,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
|
@ -2143,59 +2146,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.ToTable("Adm_T_EventComment", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventPhoto", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.ToTable("Adm_T_EventPhoto", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventType", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -7437,17 +7387,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventPhoto", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Event", "Event")
|
||||
.WithMany("Photos")
|
||||
.HasForeignKey("EventId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.JobPosition", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Department", "Department")
|
||||
|
|
@ -7879,8 +7818,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
modelBuilder.Entity("Sozsoft.Platform.Entities.Event", b =>
|
||||
{
|
||||
b.Navigation("Comments");
|
||||
|
||||
b.Navigation("Photos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventCategory", b =>
|
||||
|
|
@ -471,7 +471,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Excerpt = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: false),
|
||||
ImageUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ImageUrl = table.Column<string>(type: "text", nullable: true),
|
||||
Category = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
PublishDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
|
|
@ -1973,6 +1973,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
ParticipantsCount = table.Column<int>(type: "int", nullable: false),
|
||||
Likes = table.Column<int>(type: "int", nullable: false),
|
||||
isPublished = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
Photos = table.Column<string>(type: "text", nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
|
|
@ -2709,7 +2710,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
EventId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
Likes = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
|
|
@ -2731,33 +2732,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_EventPhoto",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
EventId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Url = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Adm_T_EventPhoto", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Adm_T_EventPhoto_Adm_T_Event_EventId",
|
||||
column: x => x.EventId,
|
||||
principalTable: "Adm_T_Event",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_SocialPollOption",
|
||||
columns: table => new
|
||||
|
|
@ -3270,11 +3244,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
table: "Adm_T_EventComment",
|
||||
column: "EventId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_EventPhoto_EventId",
|
||||
table: "Adm_T_EventPhoto",
|
||||
column: "EventId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_JobPosition_DepartmentId",
|
||||
table: "Adm_T_JobPosition",
|
||||
|
|
@ -3627,9 +3596,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_EventComment");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_EventPhoto");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_IpRestriction");
|
||||
|
||||
|
|
@ -600,7 +600,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
@ -1998,6 +1998,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<int>("ParticipantsCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Photos")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Place")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
|
@ -2130,7 +2133,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
|
@ -2140,59 +2143,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.ToTable("Adm_T_EventComment", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventPhoto", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.ToTable("Adm_T_EventPhoto", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventType", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -7434,17 +7384,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventPhoto", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Event", "Event")
|
||||
.WithMany("Photos")
|
||||
.HasForeignKey("EventId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.JobPosition", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Department", "Department")
|
||||
|
|
@ -7876,8 +7815,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
modelBuilder.Entity("Sozsoft.Platform.Entities.Event", b =>
|
||||
{
|
||||
b.Navigation("Comments");
|
||||
|
||||
b.Navigation("Photos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.EventCategory", b =>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { UserInfoViewModel } from "../admin/models"
|
||||
import { UserInfoViewModel } from '../admin/models'
|
||||
|
||||
export interface IntranetDashboardDto {
|
||||
events: EventDto[]
|
||||
|
|
@ -23,7 +23,7 @@ export interface EventDto {
|
|||
participantsCount: number
|
||||
likes: number
|
||||
isPublished: boolean
|
||||
photos: string[]
|
||||
photos: string
|
||||
comments: EventCommentDto[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models'
|
||||
import { EventCommentDto, IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models'
|
||||
import apiService, { Config } from './api.service'
|
||||
|
||||
export interface CreateSocialPostInput {
|
||||
|
|
@ -95,6 +95,25 @@ export class IntranetService {
|
|||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
||||
getEventComments = (eventId: string, config?: Partial<Config>) =>
|
||||
apiService.fetchData<EventCommentDto[]>(
|
||||
{
|
||||
method: 'GET',
|
||||
url: `/api/app/intranet/event-comments/${eventId}`,
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
||||
createEventComment = (eventId: string, content: string, config?: Partial<Config>) =>
|
||||
apiService.fetchData<EventCommentDto>(
|
||||
{
|
||||
method: 'POST',
|
||||
url: `/api/app/intranet/event-comment/${eventId}`,
|
||||
params: { content },
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
}
|
||||
|
||||
export const intranetService = new IntranetService()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import Surveys from './widgets/Surveys'
|
|||
// Modals
|
||||
import SurveyModal from './widgets/SurveyModal'
|
||||
import AnnouncementModal from './widgets/AnnouncementModal'
|
||||
import EventModal from './widgets/EventModal'
|
||||
|
||||
// Social Wall
|
||||
import SocialWall from './SocialWall'
|
||||
|
|
@ -20,6 +21,7 @@ import { Container } from '@/components/shared'
|
|||
import { usePermission } from '@/utils/hooks/usePermission'
|
||||
import {
|
||||
AnnouncementDto,
|
||||
EventDto,
|
||||
IntranetDashboardDto,
|
||||
SurveyAnswerDto,
|
||||
SurveyDto,
|
||||
|
|
@ -41,6 +43,7 @@ const WIDGET_ORDER_KEY = 'dashboard-widget-order'
|
|||
const IntranetDashboard: React.FC = () => {
|
||||
const { checkPermission } = usePermission()
|
||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<AnnouncementDto | null>(null)
|
||||
const [selectedEvent, setSelectedEvent] = useState<EventDto | null>(null)
|
||||
const [selectedSurvey, setSelectedSurvey] = useState<SurveyDto | null>(null)
|
||||
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
||||
|
|
@ -242,7 +245,7 @@ const IntranetDashboard: React.FC = () => {
|
|||
const renderWidgetComponent = (widgetId: string) => {
|
||||
switch (widgetId) {
|
||||
case 'upcoming-events':
|
||||
return <UpcomingEvents events={intranetDashboard?.events || []} />
|
||||
return <UpcomingEvents events={intranetDashboard?.events || []} onEventClick={setSelectedEvent} />
|
||||
case 'today-birthdays':
|
||||
return <TodayBirthdays employees={intranetDashboard?.birthdays || []} />
|
||||
case 'documents':
|
||||
|
|
@ -625,6 +628,15 @@ const IntranetDashboard: React.FC = () => {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedEvent && (
|
||||
<EventModal
|
||||
event={selectedEvent}
|
||||
onClose={() => setSelectedEvent(null)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedAnnouncement && (
|
||||
<AnnouncementModal
|
||||
|
|
|
|||
398
ui/src/views/intranet/widgets/EventModal.tsx
Normal file
398
ui/src/views/intranet/widgets/EventModal.tsx
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
FaTimes,
|
||||
FaChevronLeft,
|
||||
FaChevronRight,
|
||||
FaMapMarkerAlt,
|
||||
FaCalendarAlt,
|
||||
FaUsers,
|
||||
FaExpand,
|
||||
FaCommentAlt,
|
||||
FaPaperPlane,
|
||||
} from 'react-icons/fa'
|
||||
import { EventCommentDto, EventDto } from '@/proxy/intranet/models'
|
||||
import useLocale from '@/utils/hooks/useLocale'
|
||||
import { currentLocalDate } from '@/utils/dateUtils'
|
||||
import Avatar from '@/components/ui/Avatar/Avatar'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { intranetService } from '@/services/intranet.service'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
interface EventModalProps {
|
||||
event: EventDto
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const imgSrc = (img: string) => {
|
||||
if (
|
||||
img.startsWith('data:') ||
|
||||
img.startsWith('http://') ||
|
||||
img.startsWith('https://') ||
|
||||
img.startsWith('/')
|
||||
)
|
||||
return img
|
||||
return `data:image/jpeg;base64,${img}`
|
||||
}
|
||||
|
||||
const EventModal: React.FC<EventModalProps> = ({ event, onClose }) => {
|
||||
const currentLocale = useLocale()
|
||||
const photos = (event.photos || '').split('|').filter(Boolean)
|
||||
|
||||
// Photo slider state
|
||||
const [activePhoto, setActivePhoto] = useState(0)
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||
const [lightboxIndex, setLightboxIndex] = useState(0)
|
||||
|
||||
// Comments state
|
||||
const [comments, setComments] = useState<EventCommentDto[]>(event.comments || [])
|
||||
const [commentText, setCommentText] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const commentInputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const commentsEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Refresh comments from API on open
|
||||
intranetService.getEventComments(event.id).then((res) => {
|
||||
if (res.data) setComments(res.data)
|
||||
})
|
||||
}, [event.id])
|
||||
|
||||
const scrollToBottom = () => {
|
||||
commentsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleSubmitComment = async () => {
|
||||
if (!commentText.trim() || submitting) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await intranetService.createEventComment(event.id, commentText.trim())
|
||||
if (res.data) {
|
||||
setComments((prev) => [...prev, res.data!])
|
||||
setCommentText('')
|
||||
setTimeout(scrollToBottom, 100)
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmitComment()
|
||||
}
|
||||
}
|
||||
|
||||
const openLightbox = (idx: number) => {
|
||||
setLightboxIndex(idx)
|
||||
setLightboxOpen(true)
|
||||
}
|
||||
|
||||
const closeLightbox = () => setLightboxOpen(false)
|
||||
|
||||
const prevLightbox = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setLightboxIndex((i) => (i - 1 + photos.length) % photos.length)
|
||||
}
|
||||
|
||||
const nextLightbox = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setLightboxIndex((i) => (i + 1) % photos.length)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/60 z-40"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-3xl flex flex-col"
|
||||
style={{ maxHeight: '90vh' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white leading-tight">
|
||||
{event.name}
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-4 mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<FaCalendarAlt className="w-3.5 h-3.5 text-green-500" />
|
||||
{currentLocalDate(event.date, currentLocale || 'tr')}
|
||||
</span>
|
||||
{event.place && (
|
||||
<span className="flex items-center gap-1">
|
||||
<FaMapMarkerAlt className="w-3.5 h-3.5 text-red-500" />
|
||||
{event.place}
|
||||
</span>
|
||||
)}
|
||||
{event.participantsCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<FaUsers className="w-3.5 h-3.5 text-blue-500" />
|
||||
{event.participantsCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{event.user && (
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Avatar
|
||||
size={28}
|
||||
shape="circle"
|
||||
src={AVATAR_URL(event.user.id, event.user.tenantId)}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{event.user.fullName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
|
||||
>
|
||||
<FaTimes className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable body */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Photo Gallery */}
|
||||
{photos.length > 0 && (
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
{/* Main photo */}
|
||||
<div className="relative rounded-lg overflow-hidden bg-gray-900 mb-2">
|
||||
<img
|
||||
src={imgSrc(photos[activePhoto])}
|
||||
alt={`${event.name} ${activePhoto + 1}`}
|
||||
className="w-full object-cover cursor-pointer"
|
||||
style={{ maxHeight: '320px' }}
|
||||
onClick={() => openLightbox(activePhoto)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => openLightbox(activePhoto)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-black/50 hover:bg-black/70 rounded-lg text-white transition-colors"
|
||||
title="Tam ekran"
|
||||
>
|
||||
<FaExpand className="w-4 h-4" />
|
||||
</button>
|
||||
{photos.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setActivePhoto((i) => (i - 1 + photos.length) % photos.length)
|
||||
}}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
|
||||
>
|
||||
<FaChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setActivePhoto((i) => (i + 1) % photos.length)
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
|
||||
>
|
||||
<FaChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Thumbnail strip */}
|
||||
{photos.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
{photos.map((photo, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setActivePhoto(idx)}
|
||||
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all ${
|
||||
idx === activePhoto
|
||||
? 'border-green-500 opacity-100'
|
||||
: 'border-transparent opacity-60 hover:opacity-90'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={imgSrc(photo)}
|
||||
alt={`Thumbnail ${idx + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<div className="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line">
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Comments Section */}
|
||||
<div className="p-5">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||
<FaCommentAlt className="w-4 h-4 text-green-500" />
|
||||
Yorumlar ({comments.length})
|
||||
</h3>
|
||||
|
||||
{/* Comment List */}
|
||||
<div className="space-y-4 mb-4 max-h-64 overflow-y-auto">
|
||||
{comments.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500 text-center py-6">
|
||||
Henüz yorum yok. İlk yorumu sen yap!
|
||||
</p>
|
||||
) : (
|
||||
comments.map((comment) => (
|
||||
<div key={comment.id} className="flex gap-3">
|
||||
{comment.user && (
|
||||
<Avatar
|
||||
size={32}
|
||||
shape="circle"
|
||||
src={AVATAR_URL(comment.user.id, comment.user.tenantId)}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/60 rounded-xl px-3 py-2">
|
||||
<p className="text-xs font-semibold text-gray-800 dark:text-gray-200">
|
||||
{comment.user?.fullName ?? 'Kullanıcı'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-0.5 whitespace-pre-line">
|
||||
{comment.content}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 px-1">
|
||||
{dayjs(comment.creationTime).fromNow()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={commentsEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Comment Input */}
|
||||
<div className="flex gap-2 items-end">
|
||||
<textarea
|
||||
ref={commentInputRef}
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Bir yorum yaz... (Enter ile gönder)"
|
||||
rows={2}
|
||||
className="flex-1 resize-none px-3 py-2 text-sm rounded-xl border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSubmitComment}
|
||||
disabled={!commentText.trim() || submitting}
|
||||
className="p-2.5 bg-green-500 hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-colors flex-shrink-0"
|
||||
>
|
||||
<FaPaperPlane className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 flex-shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm font-medium"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Lightbox */}
|
||||
<AnimatePresence>
|
||||
{lightboxOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/95 z-[60]"
|
||||
onClick={closeLightbox}
|
||||
/>
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center">
|
||||
<button
|
||||
onClick={closeLightbox}
|
||||
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
||||
>
|
||||
<FaTimes className="w-8 h-8" />
|
||||
</button>
|
||||
{photos.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={prevLightbox}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<button
|
||||
onClick={nextLightbox}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronRight className="w-6 h-6" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<motion.img
|
||||
key={lightboxIndex}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
src={imgSrc(photos[lightboxIndex])}
|
||||
alt={`${event.name} ${lightboxIndex + 1}`}
|
||||
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{photos.length > 1 && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
{photos.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setLightboxIndex(idx)
|
||||
}}
|
||||
className={`w-2.5 h-2.5 rounded-full transition-all ${
|
||||
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventModal
|
||||
|
|
@ -6,7 +6,29 @@ import useLocale from '@/utils/hooks/useLocale'
|
|||
import { currentLocalDate } from '@/utils/dateUtils'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
const UpcomingEvents: React.FC<{ events: EventDto[] }> = ({ events }) => {
|
||||
interface UpcomingEventsProps {
|
||||
events: EventDto[]
|
||||
onEventClick?: (event: EventDto) => void
|
||||
}
|
||||
|
||||
const getFirstPhoto = (photos?: string): string | null => {
|
||||
if (!photos) return null
|
||||
const parts = photos.split('|').filter(Boolean)
|
||||
return parts.length > 0 ? parts[0] : null
|
||||
}
|
||||
|
||||
const photoSrc = (img: string) => {
|
||||
if (
|
||||
img.startsWith('data:') ||
|
||||
img.startsWith('http://') ||
|
||||
img.startsWith('https://') ||
|
||||
img.startsWith('/')
|
||||
)
|
||||
return img
|
||||
return `data:image/jpeg;base64,${img}`
|
||||
}
|
||||
|
||||
const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick }) => {
|
||||
const currentLocale = useLocale()
|
||||
const { translate } = useLocalization()
|
||||
|
||||
|
|
@ -25,17 +47,30 @@ const UpcomingEvents: React.FC<{ events: EventDto[] }> = ({ events }) => {
|
|||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{upcomingEvents.length > 0 ? (
|
||||
upcomingEvents.slice(0, 3).map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-green-500"
|
||||
>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{event.name}</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
{currentLocalDate(event.date, currentLocale || 'tr')} - {event.place}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
upcomingEvents.slice(0, 3).map((event) => {
|
||||
const firstPhoto = getFirstPhoto(event.photos)
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
onClick={() => onEventClick?.(event)}
|
||||
className={`p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-green-500 flex items-center gap-3 ${onEventClick ? 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600/50 transition-colors' : ''}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{event.name}</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
{currentLocalDate(event.date, currentLocale || 'tr')} - {event.place}
|
||||
</p>
|
||||
</div>
|
||||
{firstPhoto && (
|
||||
<img
|
||||
src={photoSrc(firstPhoto)}
|
||||
alt={event.name}
|
||||
className="w-14 h-14 rounded-lg object-cover flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||
{translate('::App.Platform.Intranet.Widgets.UpcomingEvents.NoEvent')}
|
||||
|
|
@ -47,3 +82,4 @@ const UpcomingEvents: React.FC<{ events: EventDto[] }> = ({ events }) => {
|
|||
}
|
||||
|
||||
export default UpcomingEvents
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue