SocialWall
This commit is contained in:
parent
3219265c12
commit
bc192a584b
21 changed files with 706 additions and 241 deletions
|
|
@ -0,0 +1,37 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Intranet;
|
||||||
|
|
||||||
|
public class CreateSocialPostInput
|
||||||
|
{
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON string containing location data (name, address, lat, lng, placeId).
|
||||||
|
/// </summary>
|
||||||
|
public string? LocationJson { get; set; }
|
||||||
|
|
||||||
|
public CreateSocialPostMediaInput? Media { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateSocialPostMediaInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// "image", "video", or "poll"
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URLs for image/video type media.
|
||||||
|
/// </summary>
|
||||||
|
public string[]? Urls { get; set; }
|
||||||
|
|
||||||
|
// Poll fields
|
||||||
|
public string? PollQuestion { get; set; }
|
||||||
|
public List<CreateSocialPollOptionInput>? PollOptions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateSocialPollOptionInput
|
||||||
|
{
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -6,5 +6,11 @@ namespace Sozsoft.Platform.Intranet;
|
||||||
public interface IIntranetAppService : IApplicationService
|
public interface IIntranetAppService : IApplicationService
|
||||||
{
|
{
|
||||||
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
|
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
|
||||||
Task UpdateSurveyResponseAsync(SubmitSurveyInput input);
|
Task CreateSurveyResponseAsync(SubmitSurveyInput input);
|
||||||
|
Task<SocialPostDto> CreateSocialPostAsync(CreateSocialPostInput input);
|
||||||
|
Task DeleteSocialPostAsync(System.Guid id);
|
||||||
|
Task<SocialPostDto> LikeSocialPostAsync(System.Guid id);
|
||||||
|
Task<SocialCommentDto> CommentSocialPostAsync(System.Guid id, string content);
|
||||||
|
Task VoteSocialPollAsync(System.Guid postId, System.Guid optionId);
|
||||||
|
Task IncrementAnnouncementViewCountAsync(System.Guid id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
private readonly IRepository<SurveyResponse, Guid> _surveyResponseRepository;
|
private readonly IRepository<SurveyResponse, Guid> _surveyResponseRepository;
|
||||||
private readonly IRepository<SurveyAnswer, Guid> _surveyAnswerRepository;
|
private readonly IRepository<SurveyAnswer, Guid> _surveyAnswerRepository;
|
||||||
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
|
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
|
||||||
|
private readonly IRepository<SocialComment, Guid> _socialCommentRepository;
|
||||||
|
private readonly IRepository<SocialLike, Guid> _socialLikeRepository;
|
||||||
|
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
|
||||||
|
private readonly IRepository<SocialPollOption, Guid> _socialPollOptionRepository;
|
||||||
|
|
||||||
public IntranetAppService(
|
public IntranetAppService(
|
||||||
ICurrentTenant currentTenant,
|
ICurrentTenant currentTenant,
|
||||||
|
|
@ -50,7 +54,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
IRepository<Survey, Guid> surveyRepository,
|
IRepository<Survey, Guid> surveyRepository,
|
||||||
IRepository<SurveyResponse, Guid> surveyResponseRepository,
|
IRepository<SurveyResponse, Guid> surveyResponseRepository,
|
||||||
IRepository<SurveyAnswer, Guid> surveyAnswerRepository,
|
IRepository<SurveyAnswer, Guid> surveyAnswerRepository,
|
||||||
IRepository<SocialPost, Guid> socialPostRepository
|
IRepository<SocialPost, Guid> socialPostRepository,
|
||||||
|
IRepository<SocialComment, Guid> socialCommentRepository,
|
||||||
|
IRepository<SocialLike, Guid> socialLikeRepository,
|
||||||
|
IRepository<SocialMedia, Guid> socialMediaRepository,
|
||||||
|
IRepository<SocialPollOption, Guid> socialPollOptionRepository
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_currentTenant = currentTenant;
|
_currentTenant = currentTenant;
|
||||||
|
|
@ -65,6 +73,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
_surveyResponseRepository = surveyResponseRepository;
|
_surveyResponseRepository = surveyResponseRepository;
|
||||||
_surveyAnswerRepository = surveyAnswerRepository;
|
_surveyAnswerRepository = surveyAnswerRepository;
|
||||||
_socialPostRepository = socialPostRepository;
|
_socialPostRepository = socialPostRepository;
|
||||||
|
_socialCommentRepository = socialCommentRepository;
|
||||||
|
_socialLikeRepository = socialLikeRepository;
|
||||||
|
_socialMediaRepository = socialMediaRepository;
|
||||||
|
_socialPollOptionRepository = socialPollOptionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnitOfWork]
|
[UnitOfWork]
|
||||||
|
|
@ -236,7 +248,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
var queryable = await _socialPostRepository
|
var queryable = await _socialPostRepository
|
||||||
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
|
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
|
||||||
|
|
||||||
var socialPosts = await AsyncExecuter.ToListAsync(queryable);
|
var socialPosts = await AsyncExecuter.ToListAsync(queryable.OrderByDescending(p => p.CreationTime));
|
||||||
|
|
||||||
var dtos = ObjectMapper.Map<List<SocialPost>, List<SocialPostDto>>(socialPosts);
|
var dtos = ObjectMapper.Map<List<SocialPost>, List<SocialPostDto>>(socialPosts);
|
||||||
|
|
||||||
|
|
@ -306,9 +318,41 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var dto in dtos)
|
||||||
|
{
|
||||||
|
dto.IsOwnPost = dto.UserId == CurrentUser.Id;
|
||||||
|
dto.IsLiked = dto.Likes.Any(l => l.UserId == CurrentUser.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnrichPollOptionsAsync(dtos);
|
||||||
|
|
||||||
return dtos;
|
return dtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnrichPollOptionsAsync(IEnumerable<SocialPostDto> dtos)
|
||||||
|
{
|
||||||
|
var pollMediaIds = dtos
|
||||||
|
.Where(d => d.Media?.Type == "poll")
|
||||||
|
.Select(d => d.Media!.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (pollMediaIds.Count == 0) return;
|
||||||
|
|
||||||
|
var optionsQueryable = await _socialPollOptionRepository.GetQueryableAsync();
|
||||||
|
var allOptions = await AsyncExecuter.ToListAsync(
|
||||||
|
optionsQueryable.Where(o => pollMediaIds.Contains(o.SocialMediaId) && !o.IsDeleted));
|
||||||
|
|
||||||
|
var optionsByMedia = allOptions
|
||||||
|
.GroupBy(o => o.SocialMediaId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
foreach (var dto in dtos)
|
||||||
|
{
|
||||||
|
if (dto.Media?.Type == "poll" && optionsByMedia.TryGetValue(dto.Media.Id, out var opts))
|
||||||
|
dto.Media.PollOptions = ObjectMapper.Map<List<SocialPollOption>, List<SocialPollOptionDto>>(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<FileItemDto>> GetIntranetDocumentsAsync(string folderPath)
|
public async Task<List<FileItemDto>> GetIntranetDocumentsAsync(string folderPath)
|
||||||
{
|
{
|
||||||
var items = new List<FileItemDto>();
|
var items = new List<FileItemDto>();
|
||||||
|
|
@ -382,8 +426,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
public async Task CreateSurveyResponseAsync(SubmitSurveyInput input)
|
||||||
public async Task UpdateSurveyResponseAsync(SubmitSurveyInput input)
|
|
||||||
{
|
{
|
||||||
var survey = await _surveyRepository.GetAsync(input.SurveyId);
|
var survey = await _surveyRepository.GetAsync(input.SurveyId);
|
||||||
|
|
||||||
|
|
@ -455,5 +498,221 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||||
survey.Responses++;
|
survey.Responses++;
|
||||||
await _surveyRepository.UpdateAsync(survey);
|
await _surveyRepository.UpdateAsync(survey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<SocialPostDto> CreateSocialPostAsync(CreateSocialPostInput input)
|
||||||
|
{
|
||||||
|
var post = new SocialPost(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
UserId = CurrentUser.Id,
|
||||||
|
Content = input.Content,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.LocationJson))
|
||||||
|
{
|
||||||
|
var locData = System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(input.LocationJson);
|
||||||
|
post.Location = new SocialLocation(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
SocialPostId = post.Id,
|
||||||
|
Name = locData.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? string.Empty : string.Empty,
|
||||||
|
Address = locData.TryGetProperty("address", out var addrProp) ? addrProp.GetString() : null,
|
||||||
|
Lat = locData.TryGetProperty("lat", out var latProp) && latProp.TryGetDouble(out var latVal) ? latVal : null,
|
||||||
|
Lng = locData.TryGetProperty("lng", out var lngProp) && lngProp.TryGetDouble(out var lngVal) ? lngVal : null,
|
||||||
|
PlaceId = locData.TryGetProperty("placeId", out var placeIdProp) ? placeIdProp.GetString() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Media != null)
|
||||||
|
{
|
||||||
|
var media = new SocialMedia(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
SocialPostId = post.Id,
|
||||||
|
Type = input.Media.Type,
|
||||||
|
Urls = input.Media.Urls ?? [],
|
||||||
|
PollQuestion = input.Media.PollQuestion,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.Media.PollOptions is { Count: > 0 })
|
||||||
|
{
|
||||||
|
media.PollOptions = input.Media.PollOptions
|
||||||
|
.Select(o => new SocialPollOption(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
SocialMediaId = media.Id,
|
||||||
|
Text = o.Text,
|
||||||
|
Votes = 0,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
post.Media = media;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _socialPostRepository.InsertAsync(post, autoSave: true);
|
||||||
|
|
||||||
|
// Reload with full navigation properties for mapping
|
||||||
|
var queryable = await _socialPostRepository
|
||||||
|
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
|
||||||
|
var savedPost = await AsyncExecuter.FirstOrDefaultAsync(queryable.Where(p => p.Id == post.Id));
|
||||||
|
|
||||||
|
var dto = ObjectMapper.Map<SocialPost, SocialPostDto>(savedPost!);
|
||||||
|
dto.IsOwnPost = true;
|
||||||
|
|
||||||
|
if (CurrentUser.Id.HasValue)
|
||||||
|
{
|
||||||
|
var user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value);
|
||||||
|
if (user != null)
|
||||||
|
dto.User = ObjectMapper.Map<IdentityUser, UserInfoViewModel>(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnrichPollOptionsAsync([dto]);
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteSocialPostAsync(Guid id)
|
||||||
|
{
|
||||||
|
var post = await _socialPostRepository.GetAsync(id);
|
||||||
|
if (post.UserId != CurrentUser.Id)
|
||||||
|
throw new Volo.Abp.Authorization.AbpAuthorizationException("You can only delete your own posts.");
|
||||||
|
|
||||||
|
await _socialPostRepository.DeleteAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/app/intranet/like-social-post")]
|
||||||
|
public async Task<SocialPostDto> LikeSocialPostAsync(Guid id)
|
||||||
|
{
|
||||||
|
var post = await _socialPostRepository.GetAsync(id);
|
||||||
|
|
||||||
|
var likeQueryable = await _socialLikeRepository.GetQueryableAsync();
|
||||||
|
var existingLike = await AsyncExecuter.FirstOrDefaultAsync(
|
||||||
|
likeQueryable.Where(l => l.SocialPostId == id && l.UserId == CurrentUser.Id));
|
||||||
|
|
||||||
|
bool isNowLiked;
|
||||||
|
if (existingLike != null)
|
||||||
|
{
|
||||||
|
await _socialLikeRepository.DeleteAsync(existingLike.Id);
|
||||||
|
post.LikeCount = Math.Max(0, post.LikeCount - 1);
|
||||||
|
isNowLiked = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _socialLikeRepository.InsertAsync(new SocialLike(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
SocialPostId = id,
|
||||||
|
UserId = CurrentUser.Id,
|
||||||
|
});
|
||||||
|
post.LikeCount++;
|
||||||
|
isNowLiked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
post.IsLiked = isNowLiked;
|
||||||
|
await _socialPostRepository.UpdateAsync(post, autoSave: true);
|
||||||
|
|
||||||
|
var queryable = await _socialPostRepository
|
||||||
|
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes);
|
||||||
|
var updated = await AsyncExecuter.FirstOrDefaultAsync(queryable.Where(p => p.Id == id));
|
||||||
|
var dto = ObjectMapper.Map<SocialPost, SocialPostDto>(updated!);
|
||||||
|
|
||||||
|
// Resolve user info
|
||||||
|
var userIds = new List<Guid?> { dto.UserId }
|
||||||
|
.Union(dto.Comments.Select(c => c.UserId))
|
||||||
|
.Union(dto.Likes.Select(l => l.UserId))
|
||||||
|
.Where(uid => uid.HasValue)
|
||||||
|
.Select(uid => uid!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (userIds.Count > 0)
|
||||||
|
{
|
||||||
|
var allDepartments = await _departmentRepository.GetListAsync();
|
||||||
|
var departmentDict = allDepartments.ToDictionary(d => d.Id, d => d.Name);
|
||||||
|
var allJobPositions = await _jobPositionRepository.GetListAsync();
|
||||||
|
var jobPositionDict = allJobPositions.ToDictionary(j => j.Id, j => j);
|
||||||
|
var users = await _identityUserRepository.GetListAsync();
|
||||||
|
var userMap = users.Where(u => userIds.Contains(u.Id)).ToDictionary(u => u.Id, u =>
|
||||||
|
{
|
||||||
|
var vm = ObjectMapper.Map<IdentityUser, UserInfoViewModel>(u);
|
||||||
|
var deptId = u.GetDepartmentId();
|
||||||
|
if (deptId != Guid.Empty && departmentDict.TryGetValue(deptId, out var deptName))
|
||||||
|
{
|
||||||
|
vm.DepartmentId = deptId;
|
||||||
|
vm.Departments = [new AssignedDepartmentViewModel { Id = deptId, Name = deptName, IsAssigned = true }];
|
||||||
|
}
|
||||||
|
var jobPosId = u.GetJobPositionId();
|
||||||
|
if (jobPosId != Guid.Empty && jobPositionDict.TryGetValue(jobPosId, out var jobPosition))
|
||||||
|
{
|
||||||
|
vm.JobPositionId = jobPosId;
|
||||||
|
vm.JobPositions = [new AssignedJobPoisitionViewModel { Id = jobPosId, Name = jobPosition.Name, DepartmentId = jobPosition.DepartmentId, IsAssigned = true }];
|
||||||
|
}
|
||||||
|
return vm;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dto.UserId.HasValue && userMap.TryGetValue(dto.UserId.Value, out var postUser))
|
||||||
|
dto.User = postUser;
|
||||||
|
foreach (var comment in dto.Comments)
|
||||||
|
if (comment.UserId.HasValue && userMap.TryGetValue(comment.UserId.Value, out var commentUser))
|
||||||
|
comment.User = commentUser;
|
||||||
|
foreach (var like in dto.Likes)
|
||||||
|
if (like.UserId.HasValue && userMap.TryGetValue(like.UserId.Value, out var likeUser))
|
||||||
|
like.User = likeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnrichPollOptionsAsync([dto]);
|
||||||
|
|
||||||
|
dto.IsLiked = isNowLiked;
|
||||||
|
dto.IsOwnPost = dto.UserId == CurrentUser.Id;
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/app/intranet/comment-social-post")]
|
||||||
|
public async Task<SocialCommentDto> CommentSocialPostAsync(Guid id, string content)
|
||||||
|
{
|
||||||
|
var comment = new SocialComment(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
SocialPostId = id,
|
||||||
|
UserId = CurrentUser.Id,
|
||||||
|
Content = content,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _socialCommentRepository.InsertAsync(comment, autoSave: true);
|
||||||
|
|
||||||
|
var dto = ObjectMapper.Map<SocialComment, SocialCommentDto>(comment);
|
||||||
|
|
||||||
|
if (CurrentUser.Id.HasValue)
|
||||||
|
{
|
||||||
|
var user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value);
|
||||||
|
if (user != null)
|
||||||
|
dto.User = ObjectMapper.Map<IdentityUser, UserInfoViewModel>(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/app/intranet/vote-social-poll")]
|
||||||
|
public async Task VoteSocialPollAsync(Guid postId, Guid optionId)
|
||||||
|
{
|
||||||
|
var post = await _socialPostRepository.GetAsync(postId);
|
||||||
|
|
||||||
|
var mediaQueryable = await _socialMediaRepository.GetQueryableAsync();
|
||||||
|
var media = await AsyncExecuter.FirstOrDefaultAsync(
|
||||||
|
mediaQueryable.Where(m => m.SocialPostId == postId));
|
||||||
|
|
||||||
|
if (media == null) return;
|
||||||
|
|
||||||
|
var option = await _socialPollOptionRepository.GetAsync(optionId);
|
||||||
|
option.Votes++;
|
||||||
|
await _socialPollOptionRepository.UpdateAsync(option, autoSave: true);
|
||||||
|
|
||||||
|
media.PollTotalVotes = (media.PollTotalVotes ?? 0) + 1;
|
||||||
|
media.PollUserVoteId = optionId.ToString();
|
||||||
|
await _socialMediaRepository.UpdateAsync(media, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/app/intranet/{id}/announcement-view")]
|
||||||
|
public async Task IncrementAnnouncementViewCountAsync(Guid id)
|
||||||
|
{
|
||||||
|
var announcement = await _announcementRepository.GetAsync(id);
|
||||||
|
announcement.ViewCount++;
|
||||||
|
await _announcementRepository.UpdateAsync(announcement, autoSave: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12372,6 +12372,12 @@
|
||||||
"tr": "Anketi Doldur",
|
"tr": "Anketi Doldur",
|
||||||
"en": "Fill Survey"
|
"en": "Fill Survey"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses",
|
||||||
|
"tr": "Yanıtları Görüntüle",
|
||||||
|
"en": "View Responses"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Intranet.Widgets.ActiveSurveys.NoActive",
|
"key": "App.Platform.Intranet.Widgets.ActiveSurveys.NoActive",
|
||||||
|
|
|
||||||
|
|
@ -3935,10 +3935,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
CaptionName = "App.Listform.ListformField.SocialPostId",
|
CaptionName = "App.Listform.ListformField.SocialPostId",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 3,
|
ListOrderNo = 3,
|
||||||
Visible = true,
|
Visible = false,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.SocialPost), "Id", "Content"),
|
|
||||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ public class SocialPost : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
public SocialMedia Media { get; set; }
|
public SocialMedia Media { get; set; }
|
||||||
public ICollection<SocialComment> Comments { get; set; }
|
public ICollection<SocialComment> Comments { get; set; }
|
||||||
public ICollection<SocialLike> Likes { get; set; }
|
public ICollection<SocialLike> Likes { get; set; }
|
||||||
|
|
||||||
|
public SocialPost(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant
|
public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
|
|
@ -36,6 +41,11 @@ public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
public double? Lat { get; set; }
|
public double? Lat { get; set; }
|
||||||
public double? Lng { get; set; }
|
public double? Lng { get; set; }
|
||||||
public string? PlaceId { get; set; }
|
public string? PlaceId { get; set; }
|
||||||
|
|
||||||
|
public SocialLocation(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant
|
public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
|
|
@ -55,6 +65,11 @@ public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
public string? PollUserVoteId { get; set; }
|
public string? PollUserVoteId { get; set; }
|
||||||
|
|
||||||
public ICollection<SocialPollOption> PollOptions { get; set; }
|
public ICollection<SocialPollOption> PollOptions { get; set; }
|
||||||
|
|
||||||
|
public SocialMedia(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
|
public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
|
|
@ -66,6 +81,13 @@ public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
|
|
||||||
public string Text { get; set; }
|
public string Text { get; set; }
|
||||||
public int Votes { get; set; }
|
public int Votes { get; set; }
|
||||||
|
|
||||||
|
public SocialPollOption(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SocialPollOption() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
|
public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
|
|
@ -78,6 +100,13 @@ public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
public SocialComment(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SocialComment() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
|
public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
|
|
@ -88,4 +117,9 @@ public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
public SocialPost SocialPost { get; set; }
|
public SocialPost SocialPost { get; set; }
|
||||||
|
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
|
||||||
|
public SocialLike(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1162,7 +1162,6 @@ public class PlatformDbContext :
|
||||||
b.ConfigureByConvention();
|
b.ConfigureByConvention();
|
||||||
|
|
||||||
b.Property(x => x.Type).IsRequired().HasMaxLength(64);
|
b.Property(x => x.Type).IsRequired().HasMaxLength(64);
|
||||||
b.Property(x => x.Urls).HasMaxLength(2048);
|
|
||||||
b.Property(x => x.PollQuestion).HasMaxLength(512);
|
b.Property(x => x.PollQuestion).HasMaxLength(512);
|
||||||
b.Property(x => x.PollUserVoteId).HasMaxLength(128);
|
b.Property(x => x.PollUserVoteId).HasMaxLength(128);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Sozsoft.Platform.Migrations
|
namespace Sozsoft.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20260506093149_Initial")]
|
[Migration("20260506113136_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -4085,8 +4085,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
.HasColumnType("nvarchar(64)");
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
b.PrimitiveCollection<string>("Urls")
|
b.PrimitiveCollection<string>("Urls")
|
||||||
.HasMaxLength(2048)
|
.HasColumnType("nvarchar(max)");
|
||||||
.HasColumnType("nvarchar(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
|
@ -2040,7 +2040,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
SocialPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
SocialPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
Urls = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
|
Urls = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
PollQuestion = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
PollQuestion = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||||
PollTotalVotes = table.Column<int>(type: "int", nullable: true),
|
PollTotalVotes = table.Column<int>(type: "int", nullable: true),
|
||||||
PollEndsAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
PollEndsAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
|
@ -4082,8 +4082,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
.HasColumnType("nvarchar(64)");
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
b.PrimitiveCollection<string>("Urls")
|
b.PrimitiveCollection<string>("Urls")
|
||||||
.HasMaxLength(2048)
|
.HasColumnType("nvarchar(max)");
|
||||||
.HasColumnType("nvarchar(2048)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1301,7 +1301,7 @@
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"publishDate": "12-10-2024",
|
"publishDate": "12-10-2024",
|
||||||
"isPinned": true,
|
"isPinned": true,
|
||||||
"viewCount": 156,
|
"viewCount": 0,
|
||||||
"imageUrl": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80"
|
"imageUrl": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1313,7 +1313,7 @@
|
||||||
"publishDate": "08-10-2024",
|
"publishDate": "08-10-2024",
|
||||||
"expiryDate": "05-11-2024",
|
"expiryDate": "05-11-2024",
|
||||||
"isPinned": true,
|
"isPinned": true,
|
||||||
"viewCount": 89
|
"viewCount": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "💻 Sistem Bakımı Duyurusu",
|
"title": "💻 Sistem Bakımı Duyurusu",
|
||||||
|
|
@ -1323,7 +1323,7 @@
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"publishDate": "08-10-2024",
|
"publishDate": "08-10-2024",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"viewCount": 234
|
"viewCount": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "🎓 React İleri Seviye Eğitimi",
|
"title": "🎓 React İleri Seviye Eğitimi",
|
||||||
|
|
@ -1333,7 +1333,7 @@
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"publishDate": "09-10-2024",
|
"publishDate": "09-10-2024",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"viewCount": 67
|
"viewCount": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "⚠️ Güvenlik Politikası Güncellemesi",
|
"title": "⚠️ Güvenlik Politikası Güncellemesi",
|
||||||
|
|
@ -1343,7 +1343,7 @@
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"publishDate": "04-10-2024",
|
"publishDate": "04-10-2024",
|
||||||
"isPinned": true,
|
"isPinned": true,
|
||||||
"viewCount": 312
|
"viewCount": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Surveys": [
|
"Surveys": [
|
||||||
|
|
@ -1515,44 +1515,44 @@
|
||||||
{
|
{
|
||||||
"content": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
|
"content": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"likeCount": 24,
|
"likeCount": 0,
|
||||||
"isLiked": true,
|
"isLiked": false,
|
||||||
"isOwnPost": false
|
"isOwnPost": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
"content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"likeCount": 18,
|
"likeCount": 0,
|
||||||
"isLiked": false,
|
"isLiked": false,
|
||||||
"isOwnPost": true
|
"isOwnPost": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨",
|
"content": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨",
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"likeCount": 42,
|
"likeCount": 0,
|
||||||
"isLiked": true,
|
"isLiked": false,
|
||||||
"isOwnPost": false
|
"isOwnPost": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪",
|
"content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪",
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"likeCount": 31,
|
"likeCount": 0,
|
||||||
"isLiked": false,
|
"isLiked": false,
|
||||||
"isOwnPost": false
|
"isOwnPost": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.",
|
"content": "Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.",
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"likeCount": 56,
|
"likeCount": 0,
|
||||||
"isLiked": true,
|
"isLiked": false,
|
||||||
"isOwnPost": false
|
"isOwnPost": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯",
|
"content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯",
|
||||||
"userName": "system@sozsoft.com",
|
"userName": "system@sozsoft.com",
|
||||||
"likeCount": 18,
|
"likeCount": 0,
|
||||||
"isLiked": false,
|
"isLiked": false,
|
||||||
"isOwnPost": false
|
"isOwnPost": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SocialLocations": [
|
"SocialLocations": [
|
||||||
|
|
@ -1585,7 +1585,7 @@
|
||||||
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||||
"type": "poll",
|
"type": "poll",
|
||||||
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
||||||
"pollTotalVotes": 40,
|
"pollTotalVotes": 0,
|
||||||
"pollEndsAt": "2024-10-20T23:59:59",
|
"pollEndsAt": "2024-10-20T23:59:59",
|
||||||
"pollUserVoteId": "p3"
|
"pollUserVoteId": "p3"
|
||||||
},
|
},
|
||||||
|
|
@ -1611,25 +1611,25 @@
|
||||||
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||||
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
||||||
"Text": "Kullanıcı profilleri",
|
"Text": "Kullanıcı profilleri",
|
||||||
"Votes": 12
|
"Votes": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||||
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
||||||
"Text": "Bildirim sistemi",
|
"Text": "Bildirim sistemi",
|
||||||
"Votes": 8
|
"Votes": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||||
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
||||||
"Text": "Mesajlaşma",
|
"Text": "Mesajlaşma",
|
||||||
"Votes": 15
|
"Votes": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
|
||||||
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
|
||||||
"Text": "Raporlama",
|
"Text": "Raporlama",
|
||||||
"Votes": 5
|
"Votes": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SocialComments": [
|
"SocialComments": [
|
||||||
|
|
|
||||||
|
|
@ -992,7 +992,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
|
|
||||||
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName);
|
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName);
|
||||||
|
|
||||||
await _socialPostRepository.InsertAsync(new SocialPost
|
await _socialPostRepository.InsertAsync(new SocialPost(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
UserId = user != null ? user.Id : null,
|
UserId = user != null ? user.Id : null,
|
||||||
Content = item.Content,
|
Content = item.Content,
|
||||||
|
|
@ -1013,7 +1013,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
if (exists)
|
if (exists)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
await _socialLocationRepository.InsertAsync(new SocialLocation
|
await _socialLocationRepository.InsertAsync(new SocialLocation(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
SocialPostId = post != null ? post.Id : Guid.Empty,
|
SocialPostId = post != null ? post.Id : Guid.Empty,
|
||||||
Name = item.Name,
|
Name = item.Name,
|
||||||
|
|
@ -1035,7 +1035,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
if (exists)
|
if (exists)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
await _socialMediaRepository.InsertAsync(new SocialMedia
|
await _socialMediaRepository.InsertAsync(new SocialMedia(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
SocialPostId = post != null ? post.Id : Guid.Empty,
|
SocialPostId = post != null ? post.Id : Guid.Empty,
|
||||||
Type = item.Type,
|
Type = item.Type,
|
||||||
|
|
@ -1061,7 +1061,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
if (exists)
|
if (exists)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
await _socialPollOptionRepository.InsertAsync(new SocialPollOption
|
await _socialPollOptionRepository.InsertAsync(new SocialPollOption(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
SocialMediaId = media != null ? media.Id : Guid.Empty,
|
SocialMediaId = media != null ? media.Id : Guid.Empty,
|
||||||
Text = item.Text,
|
Text = item.Text,
|
||||||
|
|
@ -1081,7 +1081,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName);
|
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName);
|
||||||
await _socialCommentRepository.InsertAsync(new SocialComment
|
await _socialCommentRepository.InsertAsync(new SocialComment(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
UserId = user != null ? user.Id : null,
|
UserId = user != null ? user.Id : null,
|
||||||
SocialPostId = post != null ? post.Id : Guid.Empty,
|
SocialPostId = post != null ? post.Id : Guid.Empty,
|
||||||
|
|
@ -1101,7 +1101,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName);
|
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName);
|
||||||
await _socialLikeRepository.InsertAsync(new SocialLike
|
await _socialLikeRepository.InsertAsync(new SocialLike(Guid.NewGuid())
|
||||||
{
|
{
|
||||||
SocialPostId = post != null ? post.Id : Guid.Empty,
|
SocialPostId = post != null ? post.Id : Guid.Empty,
|
||||||
UserId = user != null ? user.Id : null
|
UserId = user != null ? user.Id : null
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export interface SurveyDto {
|
||||||
// Sosyal Duvar - Comment Interface
|
// Sosyal Duvar - Comment Interface
|
||||||
export interface SocialCommentDto {
|
export interface SocialCommentDto {
|
||||||
id: string
|
id: string
|
||||||
creator: UserInfoViewModel
|
user: UserInfoViewModel
|
||||||
content: string
|
content: string
|
||||||
creationTime: Date
|
creationTime: Date
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
import { IntranetDashboardDto } from '@/proxy/intranet/models'
|
import { IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models'
|
||||||
import apiService, { Config } from './api.service'
|
import apiService, { Config } from './api.service'
|
||||||
|
|
||||||
|
export interface CreateSocialPostInput {
|
||||||
|
content: string
|
||||||
|
locationJson?: string
|
||||||
|
media?: {
|
||||||
|
type: 'image' | 'video' | 'poll'
|
||||||
|
urls?: string[]
|
||||||
|
pollQuestion?: string
|
||||||
|
pollOptions?: { text: string }[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntranetService {
|
export class IntranetService {
|
||||||
apiName = 'Default'
|
apiName = 'Default'
|
||||||
|
|
||||||
|
|
@ -13,7 +24,7 @@ export class IntranetService {
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
||||||
updateSurveyResponse = (
|
createSurveyResponse = (
|
||||||
surveyId: string,
|
surveyId: string,
|
||||||
answers: { questionId: string; questionType: string; value: string }[],
|
answers: { questionId: string; questionType: string; value: string }[],
|
||||||
config?: Partial<Config>,
|
config?: Partial<Config>,
|
||||||
|
|
@ -21,11 +32,69 @@ export class IntranetService {
|
||||||
apiService.fetchData<void>(
|
apiService.fetchData<void>(
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/app/intranet/update-survey-response',
|
url: '/api/app/intranet/survey-response',
|
||||||
data: { surveyId, answers },
|
data: { surveyId, answers },
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createSocialPost = (input: CreateSocialPostInput, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<SocialPostDto, CreateSocialPostInput>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/app/intranet/social-post',
|
||||||
|
data: input,
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
|
deleteSocialPost = (id: string, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<void>(
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/api/app/intranet/${id}/social-post`,
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
|
likeSocialPost = (id: string, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<SocialPostDto>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/app/intranet/like-social-post`,
|
||||||
|
params: { id },
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
|
commentSocialPost = (id: string, content: string, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<SocialCommentDto>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/app/intranet/comment-social-post`,
|
||||||
|
params: { id, content },
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
|
voteSocialPoll = (postId: string, optionId: string, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<void>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/app/intranet/vote-social-poll`,
|
||||||
|
params: { postId, optionId },
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
|
incrementAnnouncementViewCount = (id: string, config?: Partial<Config>) =>
|
||||||
|
apiService.fetchData<void>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/app/intranet/${id}/announcement-view`,
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const intranetService = new IntranetService()
|
export const intranetService = new IntranetService()
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ const IntranetDashboard: React.FC = () => {
|
||||||
const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => {
|
const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => {
|
||||||
if (!selectedSurvey) return
|
if (!selectedSurvey) return
|
||||||
try {
|
try {
|
||||||
await intranetService.updateSurveyResponse(
|
await intranetService.createSurveyResponse(
|
||||||
selectedSurvey.id,
|
selectedSurvey.id,
|
||||||
answers.map((a) => ({
|
answers.map((a) => ({
|
||||||
questionId: a.questionId,
|
questionId: a.questionId,
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,14 @@ import React, { useState, useRef } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
|
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
|
||||||
import {
|
import { FaChartBar, FaSmile, FaTimes, FaImages, FaMapMarkerAlt } from 'react-icons/fa'
|
||||||
FaChartBar,
|
|
||||||
FaSmile,
|
|
||||||
FaTimes,
|
|
||||||
FaImages,
|
|
||||||
FaMapMarkerAlt
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import MediaManager from './MediaManager'
|
import MediaManager from './MediaManager'
|
||||||
import LocationPicker from './LocationPicker'
|
import LocationPicker from './LocationPicker'
|
||||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
import { SocialMediaDto } from '@/proxy/intranet/models'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { useStoreState } from '@/store/store'
|
||||||
|
import { Avatar } from '@/components/ui'
|
||||||
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
|
||||||
interface CreatePostProps {
|
interface CreatePostProps {
|
||||||
onCreatePost: (post: {
|
onCreatePost: (post: {
|
||||||
|
|
@ -28,12 +26,8 @@ interface CreatePostProps {
|
||||||
}) => void
|
}) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { Avatar } from '@/components/ui'
|
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
|
||||||
const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
const { translate } = useLocalization();
|
const { translate } = useLocalization()
|
||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null)
|
const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null)
|
||||||
const [mediaItems, setMediaItems] = useState<SocialMediaDto[]>([])
|
const [mediaItems, setMediaItems] = useState<SocialMediaDto[]>([])
|
||||||
|
|
@ -58,22 +52,26 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
if (mediaType === 'media' && mediaItems.length > 0) {
|
if (mediaType === 'media' && mediaItems.length > 0) {
|
||||||
media = {
|
media = {
|
||||||
type: 'mixed' as const,
|
type: 'mixed' as const,
|
||||||
mediaItems
|
mediaItems,
|
||||||
}
|
}
|
||||||
} else if (mediaType === 'poll' && pollQuestion && pollOptions.filter(o => o.trim()).length >= 2) {
|
} else if (
|
||||||
|
mediaType === 'poll' &&
|
||||||
|
pollQuestion &&
|
||||||
|
pollOptions.filter((o) => o.trim()).length >= 2
|
||||||
|
) {
|
||||||
media = {
|
media = {
|
||||||
type: 'poll' as const,
|
type: 'poll' as const,
|
||||||
poll: {
|
poll: {
|
||||||
question: pollQuestion,
|
question: pollQuestion,
|
||||||
options: pollOptions.filter(o => o.trim()).map(text => ({ text }))
|
options: pollOptions.filter((o) => o.trim()).map((text) => ({ text })),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreatePost({
|
onCreatePost({
|
||||||
content,
|
content,
|
||||||
media,
|
media,
|
||||||
location: location || undefined
|
location: location || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
|
|
@ -169,7 +167,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.Placeholder')}
|
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.Placeholder')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none transition-all',
|
'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none transition-all',
|
||||||
isExpanded ? 'min-h-[120px]' : 'min-h-[48px]'
|
isExpanded ? 'min-h-[120px]' : 'min-h-[48px]',
|
||||||
)}
|
)}
|
||||||
rows={isExpanded ? 4 : 1}
|
rows={isExpanded ? 4 : 1}
|
||||||
/>
|
/>
|
||||||
|
|
@ -187,7 +185,8 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} ({mediaItems.length})
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} (
|
||||||
|
{mediaItems.length})
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -195,7 +194,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
clearMedia()
|
clearMedia()
|
||||||
}}
|
}}
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle')}
|
title={translate(
|
||||||
|
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -211,7 +212,10 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-24 bg-gray-900 rounded-lg relative">
|
<div className="w-full h-24 bg-gray-900 rounded-lg relative">
|
||||||
<video src={item.urls?.[0]} className="w-full h-full object-cover rounded-lg" />
|
<video
|
||||||
|
src={item.urls?.[0]}
|
||||||
|
className="w-full h-full object-cover rounded-lg"
|
||||||
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
|
||||||
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
|
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
|
||||||
|
|
@ -250,12 +254,16 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}</h4>
|
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}
|
||||||
|
</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLocation(null)}
|
onClick={() => setLocation(null)}
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle')}
|
title={translate(
|
||||||
|
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -284,7 +292,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}</h4>
|
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}
|
||||||
|
</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -300,7 +310,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
type="text"
|
type="text"
|
||||||
value={pollQuestion}
|
value={pollQuestion}
|
||||||
onChange={(e) => setPollQuestion(e.target.value)}
|
onChange={(e) => setPollQuestion(e.target.value)}
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.PollQuestionPlaceholder')}
|
placeholder={translate(
|
||||||
|
'::App.Platform.Intranet.SocialWall.CreatePost.PollQuestionPlaceholder',
|
||||||
|
)}
|
||||||
className="w-full px-4 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-4 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -310,7 +322,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
type="text"
|
type="text"
|
||||||
value={option}
|
value={option}
|
||||||
onChange={(e) => updatePollOption(index, e.target.value)}
|
onChange={(e) => updatePollOption(index, e.target.value)}
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.PollOptionPlaceholder')}
|
placeholder={translate(
|
||||||
|
'::App.Platform.Intranet.SocialWall.CreatePost.PollOptionPlaceholder',
|
||||||
|
)}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
{pollOptions.length > 2 && (
|
{pollOptions.length > 2 && (
|
||||||
|
|
@ -365,9 +379,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
'p-2 rounded-full transition-colors',
|
'p-2 rounded-full transition-colors',
|
||||||
mediaType === 'media'
|
mediaType === 'media'
|
||||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||||
)}
|
)}
|
||||||
title={mediaType === 'media' ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditMediaTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle')}
|
title={
|
||||||
|
mediaType === 'media'
|
||||||
|
? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditMediaTitle')
|
||||||
|
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FaImages className="w-5 h-5" />
|
<FaImages className="w-5 h-5" />
|
||||||
{mediaType === 'media' && mediaItems.length > 0 && (
|
{mediaType === 'media' && mediaItems.length > 0 && (
|
||||||
|
|
@ -389,9 +407,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
'p-2 rounded-full transition-colors',
|
'p-2 rounded-full transition-colors',
|
||||||
mediaType === 'poll'
|
mediaType === 'poll'
|
||||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||||
)}
|
)}
|
||||||
title={mediaType === 'poll' ? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')}
|
title={
|
||||||
|
mediaType === 'poll'
|
||||||
|
? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')
|
||||||
|
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FaChartBar className="w-5 h-5" />
|
<FaChartBar className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -410,9 +432,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
'p-2 rounded-full transition-colors',
|
'p-2 rounded-full transition-colors',
|
||||||
location
|
location
|
||||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||||
)}
|
)}
|
||||||
title={location ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')}
|
title={
|
||||||
|
location
|
||||||
|
? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle')
|
||||||
|
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FaMapMarkerAlt className="w-5 h-5" />
|
<FaMapMarkerAlt className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -420,10 +446,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
{/* Emoji Picker */}
|
{/* Emoji Picker */}
|
||||||
{showEmojiPicker && (
|
{showEmojiPicker && (
|
||||||
<div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50">
|
<div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50">
|
||||||
<EmojiPicker
|
<EmojiPicker onEmojiClick={handleEmojiClick} autoFocusSearch={false} />
|
||||||
onEmojiClick={handleEmojiClick}
|
|
||||||
autoFocusSearch={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -453,10 +476,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
{/* Location Picker Modal */}
|
{/* Location Picker Modal */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showLocationPicker && (
|
{showLocationPicker && (
|
||||||
<LocationPicker
|
<LocationPicker onSelect={setLocation} onClose={() => setShowLocationPicker(false)} />
|
||||||
onSelect={setLocation}
|
|
||||||
onClose={() => setShowLocationPicker(false)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,26 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
const files = e.target.files
|
const files = e.target.files
|
||||||
if (!files) return
|
if (!files) return
|
||||||
|
|
||||||
const newMedia: SocialMediaDto[] = Array.from(files).map((file) => ({
|
const fileArray = Array.from(files)
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
const readers = fileArray.map(
|
||||||
type: file.type.startsWith('video/') ? 'video' : 'image',
|
(file) =>
|
||||||
urls: [URL.createObjectURL(file)],
|
new Promise<SocialMediaDto>((resolve) => {
|
||||||
file
|
const reader = new FileReader()
|
||||||
}))
|
reader.onload = () => {
|
||||||
|
resolve({
|
||||||
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
|
type: file.type.startsWith('video/') ? 'video' : 'image',
|
||||||
|
urls: [reader.result as string],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
Promise.all(readers).then((newMedia) => {
|
||||||
|
onChange([...media, ...newMedia])
|
||||||
|
})
|
||||||
|
|
||||||
onChange([...media, ...newMedia])
|
|
||||||
e.target.value = ''
|
e.target.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import 'dayjs/locale/tr'
|
import 'dayjs/locale/tr'
|
||||||
import { FaHeart, FaRegHeart, FaRegCommentAlt, FaTrash, FaPaperPlane } from 'react-icons/fa'
|
import { FaHeart, FaRegHeart, FaRegCommentAlt, FaTrash, FaPaperPlane } from 'react-icons/fa'
|
||||||
import MediaLightbox from './MediaLightbox'
|
import MediaLightbox from './MediaLightbox'
|
||||||
import LocationMap from './LocationMap'
|
|
||||||
import UserProfileCard from './UserProfileCard'
|
import UserProfileCard from './UserProfileCard'
|
||||||
import { SocialPostDto } from '@/proxy/intranet/models'
|
import { SocialPostDto } from '@/proxy/intranet/models'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
|
|
@ -369,16 +368,16 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
<Avatar
|
<Avatar
|
||||||
size={32}
|
size={32}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
src={AVATAR_URL(comment.creator.id, comment.creator.tenantId)}
|
src={AVATAR_URL(comment.user.id, comment.user.tenantId)}
|
||||||
/>
|
/>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{hoveredCommentAuthor === comment.id && (
|
{hoveredCommentAuthor === comment.id && (
|
||||||
<UserProfileCard
|
<UserProfileCard
|
||||||
user={{
|
user={{
|
||||||
id: comment.creator.id || '',
|
id: comment.user.id || '',
|
||||||
name: comment.creator.fullName || '',
|
name: comment.user.fullName || '',
|
||||||
title: comment.creator.jobPositions?.[0]?.name || '',
|
title: comment.user.jobPositions?.[0]?.name || '',
|
||||||
tenantId: comment.creator.tenantId || '',
|
tenantId: comment.user.tenantId || '',
|
||||||
}}
|
}}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
/>
|
/>
|
||||||
|
|
@ -388,7 +387,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
|
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
|
||||||
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
||||||
{comment.creator.fullName}
|
{comment.user.fullName}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
|
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,29 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { AnimatePresence } from 'framer-motion'
|
import { AnimatePresence } from 'framer-motion'
|
||||||
import PostItem from './PostItem'
|
import PostItem from './PostItem'
|
||||||
import CreatePost from './CreatePost'
|
import CreatePost from './CreatePost'
|
||||||
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
|
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { IdentityUserDto } from '@/proxy/admin/models'
|
import { intranetService } from '@/services/intranet.service'
|
||||||
|
|
||||||
const SocialWall: React.FC<{ posts: SocialPostDto[] }> = ({ posts }) => {
|
const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: SocialPostDto[]) => void }> = ({ posts: initialPosts, onPostsChange }) => {
|
||||||
// const [posts, setPosts] = useState<SocialPost[]>(mockSocialPosts)
|
const [posts, setPosts] = useState<SocialPostDto[]>(initialPosts)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPosts(initialPosts)
|
||||||
|
}, [initialPosts])
|
||||||
const [filter, setFilter] = useState<'all' | 'mine'>('all')
|
const [filter, setFilter] = useState<'all' | 'mine'>('all')
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
// Ali Öztürk'ü "Siz" kullanıcısı olarak kullan
|
|
||||||
const { user } = useStoreState((state) => state.auth)
|
const { user } = useStoreState((state) => state.auth)
|
||||||
const currentUserAuthor: IdentityUserDto = {
|
|
||||||
id: user?.id || '0',
|
const updatePosts = (updated: SocialPostDto[]) => {
|
||||||
userName: user.userName || 'unknown',
|
setPosts(updated)
|
||||||
name: user?.name || 'Siz',
|
onPostsChange?.(updated)
|
||||||
email: user?.email || '',
|
|
||||||
isActive: true,
|
|
||||||
emailConfirmed: true,
|
|
||||||
phoneNumberConfirmed: true,
|
|
||||||
lockoutEnabled: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreatePost = (postData: {
|
const handleCreatePost = async (postData: {
|
||||||
content: string
|
content: string
|
||||||
location?: string
|
location?: string
|
||||||
media?: {
|
media?: {
|
||||||
|
|
@ -37,128 +35,105 @@ const SocialWall: React.FC<{ posts: SocialPostDto[] }> = ({ posts }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
let mediaForPost = undefined
|
let mediaInput: { type: 'image' | 'video' | 'poll'; urls?: string[]; pollQuestion?: string; pollOptions?: { text: string }[] } | undefined
|
||||||
|
|
||||||
if (postData.media) {
|
if (postData.media) {
|
||||||
if (postData.media.type === 'mixed' && postData.media.mediaItems) {
|
if (postData.media.type === 'mixed' && postData.media.mediaItems) {
|
||||||
// Convert MediaItems to post format
|
|
||||||
const images = postData.media.mediaItems.filter((m) => m.type === 'image')
|
const images = postData.media.mediaItems.filter((m) => m.type === 'image')
|
||||||
const videos = postData.media.mediaItems.filter((m) => m.type === 'video')
|
const videos = postData.media.mediaItems.filter((m) => m.type === 'video')
|
||||||
|
|
||||||
if (images.length > 0 && videos.length === 0) {
|
if (images.length > 0 && videos.length === 0) {
|
||||||
mediaForPost = {
|
mediaInput = {
|
||||||
type: 'image' as const,
|
type: 'image',
|
||||||
urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[],
|
urls: images.map((i) => i.urls?.[0]).filter(Boolean) as string[],
|
||||||
}
|
}
|
||||||
} else if (videos.length > 0 && images.length === 0) {
|
} else if (videos.length > 0 && images.length === 0) {
|
||||||
mediaForPost = {
|
mediaInput = { type: 'video', urls: videos[0].urls || [] }
|
||||||
type: 'video' as const,
|
|
||||||
urls: videos[0].urls || [],
|
|
||||||
}
|
|
||||||
} else if (images.length > 0 || videos.length > 0) {
|
} else if (images.length > 0 || videos.length > 0) {
|
||||||
// Mixed media - use first image for now
|
mediaInput = {
|
||||||
mediaForPost = {
|
type: 'image',
|
||||||
type: 'image' as const,
|
urls: images.map((i) => i.urls?.[0]).filter(Boolean) as string[],
|
||||||
urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (postData.media.type === 'poll' && postData.media.poll) {
|
} else if (postData.media.type === 'poll' && postData.media.poll) {
|
||||||
mediaForPost = {
|
mediaInput = {
|
||||||
type: 'poll' as const,
|
type: 'poll',
|
||||||
pollQuestion: postData.media.poll.question,
|
pollQuestion: postData.media.poll.question,
|
||||||
pollOptions: postData.media.poll.options.map((opt, index) => ({
|
pollOptions: postData.media.poll.options,
|
||||||
id: `opt-${index}`,
|
|
||||||
text: opt.text,
|
|
||||||
votes: 0,
|
|
||||||
})),
|
|
||||||
pollTotalVotes: 0,
|
|
||||||
pollEndsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPost: SocialPostDto = {
|
try {
|
||||||
id: Date.now().toString(),
|
const response = await intranetService.createSocialPost({
|
||||||
user: currentUserAuthor,
|
content: postData.content,
|
||||||
content: postData.content,
|
locationJson: postData.location,
|
||||||
creationTime: new Date(),
|
media: mediaInput,
|
||||||
media: mediaForPost,
|
})
|
||||||
locationJson: postData.location,
|
updatePosts([response.data, ...posts])
|
||||||
likeCount: 0,
|
} catch {
|
||||||
isLiked: false,
|
// error handled by apiService
|
||||||
likeUsers: [],
|
|
||||||
comments: [],
|
|
||||||
isOwnPost: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setPosts([newPost, ...posts])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLike = (postId: string) => {
|
const handleLike = async (postId: string) => {
|
||||||
// setPosts(
|
try {
|
||||||
// posts.map((post) => {
|
const response = await intranetService.likeSocialPost(postId)
|
||||||
// if (post.id === postId) {
|
updatePosts(posts.map((p) => (p.id === postId ? response.data : p)))
|
||||||
// return {
|
} catch {
|
||||||
// ...post,
|
// error handled by apiService
|
||||||
// likeCount: post.isLiked ? post.likeCount - 1 : post.likeCount + 1,
|
}
|
||||||
// isLiked: !post.isLiked
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return post
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleComment = (postId: string, content: string) => {
|
const handleComment = async (postId: string, content: string) => {
|
||||||
// setPosts(
|
try {
|
||||||
// posts.map((post) => {
|
const response = await intranetService.commentSocialPost(postId, content)
|
||||||
// if (post.id === postId) {
|
updatePosts(
|
||||||
// const commentAuthor = currentUserAuthor
|
posts.map((p) =>
|
||||||
// const newComment = {
|
p.id === postId ? { ...p, comments: [...(p.comments || []), response.data] } : p,
|
||||||
// id: Date.now().toString(),
|
),
|
||||||
// creator: commentAuthor,
|
)
|
||||||
// content,
|
} catch {
|
||||||
// creationTime: new Date()
|
// error handled by apiService
|
||||||
// }
|
}
|
||||||
// return {
|
|
||||||
// ...post,
|
|
||||||
// comments: [...post.comments, newComment]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return post
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (postId: string) => {
|
const handleDelete = async (postId: string) => {
|
||||||
if (window.confirm(translate('::App.Platform.Intranet.SocialWall.DeleteConfirm'))) {
|
if (window.confirm(translate('::App.Platform.Intranet.SocialWall.DeleteConfirm'))) {
|
||||||
// setPosts(posts.filter((post) => post.id !== postId))
|
try {
|
||||||
|
await intranetService.deleteSocialPost(postId)
|
||||||
|
updatePosts(posts.filter((p) => p.id !== postId))
|
||||||
|
} catch {
|
||||||
|
// error handled by apiService
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleVote = (postId: string, optionId: string) => {
|
const handleVote = async (postId: string, optionId: string) => {
|
||||||
// setPosts(
|
try {
|
||||||
// posts.map((post) => {
|
await intranetService.voteSocialPoll(postId, optionId)
|
||||||
// if (post.id === postId && post.media?.type === 'poll' && post.media.pollOptions) {
|
updatePosts(
|
||||||
// // If user already voted, don't allow voting again
|
posts.map((p) => {
|
||||||
// if (post.media.pollUserVoteId) {
|
if (p.id === postId && p.media?.type === 'poll' && p.media.pollOptions) {
|
||||||
// return post
|
if (p.media.pollUserVoteId) return p
|
||||||
// }
|
return {
|
||||||
// return {
|
...p,
|
||||||
// ...post,
|
media: {
|
||||||
// media: {
|
...p.media,
|
||||||
// ...post.media,
|
pollOptions: p.media.pollOptions.map((opt) =>
|
||||||
// pollOptions: post.media.pollOptions.map((opt) =>
|
opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt,
|
||||||
// opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt
|
),
|
||||||
// ),
|
pollTotalVotes: (p.media.pollTotalVotes || 0) + 1,
|
||||||
// pollTotalVotes: (post.media.pollTotalVotes || 0) + 1,
|
pollUserVoteId: optionId,
|
||||||
// pollUserVoteId: optionId
|
},
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
return p
|
||||||
// return post
|
}),
|
||||||
// })
|
)
|
||||||
// )
|
} catch {
|
||||||
|
// error handled by apiService
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredPosts = filter === 'mine' ? posts.filter((post) => post.isOwnPost) : posts
|
const filteredPosts = filter === 'mine' ? posts.filter((post) => post.isOwnPost) : posts
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
|
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
|
||||||
|
|
@ -7,18 +7,21 @@ import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
import { currentLocalDate } from '@/utils/dateUtils'
|
||||||
import Avatar from '@/components/ui/Avatar/Avatar'
|
import Avatar from '@/components/ui/Avatar/Avatar'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
import { intranetService } from '@/services/intranet.service'
|
||||||
|
|
||||||
interface AnnouncementModalProps {
|
interface AnnouncementModalProps {
|
||||||
announcement: AnnouncementDto
|
announcement: AnnouncementDto
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
|
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
|
||||||
announcement,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
intranetService.incrementAnnouncementViewCount(announcement.id)
|
||||||
|
}, [announcement.id])
|
||||||
|
|
||||||
const getCategoryColor = (category: string) => {
|
const getCategoryColor = (category: string) => {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
|
@ -111,21 +114,42 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||||
{/* Images if exist */}
|
{/* Images if exist */}
|
||||||
{announcement.imageUrl && (() => {
|
{announcement.imageUrl &&
|
||||||
const images = announcement.imageUrl.split('|').filter(Boolean)
|
(() => {
|
||||||
return images.length > 0 ? (
|
const images = announcement.imageUrl.split('|').filter(Boolean)
|
||||||
<div className={`mb-6 ${images.length > 1 ? 'grid grid-cols-2 gap-2' : ''}`}>
|
if (images.length === 0) return null
|
||||||
{images.map((img, idx) => (
|
const getGridClass = (count: number) => {
|
||||||
<img
|
if (count === 1) return ''
|
||||||
key={idx}
|
if (count === 2) return 'grid grid-cols-2 gap-2'
|
||||||
src={img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`}
|
if (count === 3) return 'grid grid-cols-3 gap-2'
|
||||||
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
|
return 'grid grid-cols-2 gap-2'
|
||||||
className="w-full rounded-lg object-cover"
|
}
|
||||||
/>
|
const getImgClass = (count: number, idx: number) => {
|
||||||
))}
|
if (count === 1) return 'w-full rounded-lg object-cover max-h-96'
|
||||||
</div>
|
if (count === 3 && idx === 0) return 'col-span-3 w-full rounded-lg object-cover max-h-64'
|
||||||
) : null
|
if (count >= 4 && idx === 0) return 'col-span-2 w-full rounded-lg object-cover max-h-64'
|
||||||
})()}
|
return 'w-full rounded-lg object-cover h-40'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`mb-6 ${getGridClass(images.length)}`}>
|
||||||
|
{images.map((img, idx) => (
|
||||||
|
<img
|
||||||
|
key={idx}
|
||||||
|
src={
|
||||||
|
img.startsWith('data:') ||
|
||||||
|
img.startsWith('http://') ||
|
||||||
|
img.startsWith('https://') ||
|
||||||
|
img.startsWith('/')
|
||||||
|
? img
|
||||||
|
: `data:image/jpeg;base64,${img}`
|
||||||
|
}
|
||||||
|
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
|
||||||
|
className={getImgClass(images.length, idx)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Full Content */}
|
{/* Full Content */}
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
|
|
||||||
|
|
@ -26,26 +26,48 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-3 space-y-4">
|
||||||
{surveys?.map((survey, index) => {
|
{surveys?.map((survey, index) => {
|
||||||
const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day')
|
const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day')
|
||||||
const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal'
|
const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal'
|
||||||
|
const isCompleted = !!survey.myResponse
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={survey.id}
|
key={survey.id}
|
||||||
onClick={() => onTakeSurvey(survey)}
|
onClick={() => onTakeSurvey(survey)}
|
||||||
className="group relative p-5 rounded-xl bg-white dark:bg-gray-750 border border-gray-200 dark:border-gray-600 hover:border-purple-300 dark:hover:border-purple-500 cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1"
|
className={`group relative p-5 rounded-xl border cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1 ${
|
||||||
|
isCompleted
|
||||||
|
? 'bg-green-50 dark:bg-green-900/20 border-green-300 dark:border-green-700 hover:border-green-400 dark:hover:border-green-500'
|
||||||
|
: 'bg-white dark:bg-gray-750 border-gray-200 dark:border-gray-600 hover:border-purple-300 dark:hover:border-purple-500'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{/* Background gradient on hover */}
|
{/* Background gradient on hover */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/10 dark:to-pink-900/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
<div className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 ${
|
||||||
|
isCompleted
|
||||||
|
? 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/10 dark:to-emerald-900/10'
|
||||||
|
: 'bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/10 dark:to-pink-900/10'
|
||||||
|
}`}></div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Survey Title */}
|
{/* Survey Title */}
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<h4 className="text-base font-semibold text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300 transition-colors">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
{survey.title}
|
{isCompleted && (
|
||||||
</h4>
|
<span className="flex-shrink-0 inline-flex items-center justify-center w-5 h-5 bg-green-500 rounded-full">
|
||||||
|
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h4 className={`text-base font-semibold transition-colors ${
|
||||||
|
isCompleted
|
||||||
|
? 'text-green-800 dark:text-green-300 group-hover:text-green-700 dark:group-hover:text-green-200'
|
||||||
|
: 'text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300'
|
||||||
|
}`}>
|
||||||
|
{survey.title}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`px-2 py-1 text-center rounded-full text-xs font-medium ${
|
className={`px-2 py-1 text-center rounded-full text-xs font-medium ${
|
||||||
urgency === 'urgent'
|
urgency === 'urgent'
|
||||||
|
|
@ -119,8 +141,14 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white text-sm font-medium rounded-lg transition-all duration-300 transform group-hover:scale-[1.02] shadow-sm hover:shadow-md">
|
<button className={`w-full flex items-center justify-center gap-2 px-4 py-3 text-white text-sm font-medium rounded-lg transition-all duration-300 transform group-hover:scale-[1.02] shadow-sm hover:shadow-md ${
|
||||||
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
|
isCompleted
|
||||||
|
? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600'
|
||||||
|
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700'
|
||||||
|
}`}>
|
||||||
|
{isCompleted
|
||||||
|
? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses')
|
||||||
|
: translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
|
||||||
<FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
|
<FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue