<button kaldırıldı <Button olarak değiştirildi.
This commit is contained in:
parent
84db5c672e
commit
5110349a6b
79 changed files with 2975 additions and 1944 deletions
|
|
@ -11,12 +11,14 @@ using Sozsoft.Platform.BlobStoring;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.Identity;
|
using Volo.Abp.Identity;
|
||||||
|
using Volo.Abp.Uow;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Messenger;
|
namespace Sozsoft.Platform.Messenger;
|
||||||
|
|
||||||
|
|
@ -29,6 +31,7 @@ public class MessengerAppService : ApplicationService
|
||||||
private readonly IIdentitySessionRepository _identitySessionRepository;
|
private readonly IIdentitySessionRepository _identitySessionRepository;
|
||||||
private readonly BlobManager _blobManager;
|
private readonly BlobManager _blobManager;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IHubContext<MessengerHub> _messengerHubContext;
|
||||||
|
|
||||||
public MessengerAppService(
|
public MessengerAppService(
|
||||||
IRepository<IdentityUser, Guid> userRepository,
|
IRepository<IdentityUser, Guid> userRepository,
|
||||||
|
|
@ -36,7 +39,8 @@ public class MessengerAppService : ApplicationService
|
||||||
IRepository<MessengerConversationMessage, Guid> messageRepository,
|
IRepository<MessengerConversationMessage, Guid> messageRepository,
|
||||||
IIdentitySessionRepository identitySessionRepository,
|
IIdentitySessionRepository identitySessionRepository,
|
||||||
BlobManager blobManager,
|
BlobManager blobManager,
|
||||||
IConfiguration configuration)
|
IConfiguration configuration,
|
||||||
|
IHubContext<MessengerHub> messengerHubContext)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_conversationRepository = conversationRepository;
|
_conversationRepository = conversationRepository;
|
||||||
|
|
@ -44,6 +48,7 @@ public class MessengerAppService : ApplicationService
|
||||||
_identitySessionRepository = identitySessionRepository;
|
_identitySessionRepository = identitySessionRepository;
|
||||||
_blobManager = blobManager;
|
_blobManager = blobManager;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_messengerHubContext = messengerHubContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MessengerContactDto>> GetContactsAsync(string? filter = null)
|
public async Task<List<MessengerContactDto>> GetContactsAsync(string? filter = null)
|
||||||
|
|
@ -133,6 +138,7 @@ public class MessengerAppService : ApplicationService
|
||||||
return MapConversation(conversation);
|
return MapConversation(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
public async Task<MessengerConversationDto> CreateConversationAsync(MessengerConversationCreateUpdateDto input)
|
public async Task<MessengerConversationDto> CreateConversationAsync(MessengerConversationCreateUpdateDto input)
|
||||||
{
|
{
|
||||||
var currentUserId = GetCurrentUserId();
|
var currentUserId = GetCurrentUserId();
|
||||||
|
|
@ -159,6 +165,7 @@ public class MessengerAppService : ApplicationService
|
||||||
return MapConversation(conversation);
|
return MapConversation(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
public async Task<MessengerConversationDto> UpdateConversationAsync(Guid id, MessengerConversationCreateUpdateDto input)
|
public async Task<MessengerConversationDto> UpdateConversationAsync(Guid id, MessengerConversationCreateUpdateDto input)
|
||||||
{
|
{
|
||||||
var conversation = await _conversationRepository.GetAsync(id);
|
var conversation = await _conversationRepository.GetAsync(id);
|
||||||
|
|
@ -178,6 +185,7 @@ public class MessengerAppService : ApplicationService
|
||||||
return MapConversation(conversation);
|
return MapConversation(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
public async Task DeleteConversationAsync(Guid id)
|
public async Task DeleteConversationAsync(Guid id)
|
||||||
{
|
{
|
||||||
var conversation = await _conversationRepository.GetAsync(id);
|
var conversation = await _conversationRepository.GetAsync(id);
|
||||||
|
|
@ -220,6 +228,8 @@ public class MessengerAppService : ApplicationService
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
|
[HttpPost("api/app/messenger/send-message")]
|
||||||
public async Task<MessengerMessageDto> SendMessageAsync(MessengerSendMessageDto input)
|
public async Task<MessengerMessageDto> SendMessageAsync(MessengerSendMessageDto input)
|
||||||
{
|
{
|
||||||
var senderId = GetCurrentUserId();
|
var senderId = GetCurrentUserId();
|
||||||
|
|
@ -271,7 +281,7 @@ public class MessengerAppService : ApplicationService
|
||||||
IsGroup = participantIds.Count > 2
|
IsGroup = participantIds.Count > 2
|
||||||
};
|
};
|
||||||
|
|
||||||
await _conversationRepository.InsertAsync(conversation, autoSave: true);
|
await _conversationRepository.InsertAsync(conversation, autoSave: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var senderName = $"{CurrentUser.Name} {CurrentUser.SurName}".Trim() ?? CurrentUser.UserName ?? "Kullanici";
|
var senderName = $"{CurrentUser.Name} {CurrentUser.SurName}".Trim() ?? CurrentUser.UserName ?? "Kullanici";
|
||||||
|
|
@ -289,17 +299,23 @@ public class MessengerAppService : ApplicationService
|
||||||
SentAt = Clock.Now
|
SentAt = Clock.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
await _messageRepository.InsertAsync(message, autoSave: true);
|
await _messageRepository.InsertAsync(message, autoSave: false);
|
||||||
|
|
||||||
conversation.LastSenderId = senderId;
|
conversation.LastSenderId = senderId;
|
||||||
conversation.LastMessagePreview = GetMessagePreview(trimmedText, input.Attachments.Count);
|
conversation.LastMessagePreview = GetMessagePreview(trimmedText, input.Attachments.Count);
|
||||||
conversation.LastMessageTime = message.SentAt;
|
conversation.LastMessageTime = message.SentAt;
|
||||||
conversation.MessageCount += 1;
|
conversation.MessageCount += 1;
|
||||||
await _conversationRepository.UpdateAsync(conversation, autoSave: true);
|
await _conversationRepository.UpdateAsync(conversation, autoSave: false);
|
||||||
|
await CurrentUnitOfWork!.SaveChangesAsync();
|
||||||
|
|
||||||
return MapMessage(message);
|
var messageDto = MapMessage(message);
|
||||||
|
await PublishMessageReceivedAsync(messageDto);
|
||||||
|
|
||||||
|
return messageDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
|
[HttpDelete("api/app/messenger/messages/{id}")]
|
||||||
public async Task<MessengerMessageDeletedDto> DeleteMessageAsync(Guid id)
|
public async Task<MessengerMessageDeletedDto> DeleteMessageAsync(Guid id)
|
||||||
{
|
{
|
||||||
var currentUserId = GetCurrentUserId();
|
var currentUserId = GetCurrentUserId();
|
||||||
|
|
@ -335,13 +351,17 @@ public class MessengerAppService : ApplicationService
|
||||||
conversation.LastMessageTime = lastMessage?.SentAt;
|
conversation.LastMessageTime = lastMessage?.SentAt;
|
||||||
await _conversationRepository.UpdateAsync(conversation, autoSave: true);
|
await _conversationRepository.UpdateAsync(conversation, autoSave: true);
|
||||||
|
|
||||||
return new MessengerMessageDeletedDto
|
var deletedMessage = new MessengerMessageDeletedDto
|
||||||
{
|
{
|
||||||
MessageId = message.Id,
|
MessageId = message.Id,
|
||||||
ConversationId = message.ConversationId,
|
ConversationId = message.ConversationId,
|
||||||
SenderId = message.SenderId,
|
SenderId = message.SenderId,
|
||||||
RecipientIds = recipientIds
|
RecipientIds = recipientIds
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await PublishMessageDeletedAsync(deletedMessage);
|
||||||
|
|
||||||
|
return deletedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("api/app/messenger/upload-attachment")]
|
[HttpPost("api/app/messenger/upload-attachment")]
|
||||||
|
|
@ -523,4 +543,26 @@ public class MessengerAppService : ApplicationService
|
||||||
|
|
||||||
return attachmentCount > 0 ? $"{attachmentCount} dosya" : string.Empty;
|
return attachmentCount > 0 ? $"{attachmentCount} dosya" : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PublishMessageReceivedAsync(MessengerMessageDto message)
|
||||||
|
{
|
||||||
|
var targetGroups = message.RecipientIds
|
||||||
|
.Append(message.SenderId)
|
||||||
|
.Distinct()
|
||||||
|
.Select(userId => MessengerHub.UserGroupName(CurrentTenant.Id, userId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await _messengerHubContext.Clients.Groups(targetGroups).SendAsync("MessengerMessageReceived", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PublishMessageDeletedAsync(MessengerMessageDeletedDto message)
|
||||||
|
{
|
||||||
|
var targetGroups = message.RecipientIds
|
||||||
|
.Append(message.SenderId)
|
||||||
|
.Distinct()
|
||||||
|
.Select(userId => MessengerHub.UserGroupName(CurrentTenant.Id, userId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await _messengerHubContext.Clients.Groups(targetGroups).SendAsync("MessengerMessageDeleted", message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,33 +51,34 @@ public class MessengerHub : Hub
|
||||||
|
|
||||||
public async Task SendMessage(MessengerSendMessageDto input)
|
public async Task SendMessage(MessengerSendMessageDto input)
|
||||||
{
|
{
|
||||||
var message = await _messengerAppService.SendMessageAsync(input);
|
try
|
||||||
|
{
|
||||||
var targetGroups = message.RecipientIds
|
await _messengerAppService.SendMessageAsync(input);
|
||||||
.Append(message.SenderId)
|
}
|
||||||
.Distinct()
|
catch (OperationCanceledException) when (Context.ConnectionAborted.IsCancellationRequested)
|
||||||
.Select(UserGroupName)
|
{
|
||||||
.ToList();
|
return;
|
||||||
|
}
|
||||||
await Clients.Groups(targetGroups).SendAsync("MessengerMessageReceived", message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteMessage(Guid messageId)
|
public async Task DeleteMessage(Guid messageId)
|
||||||
{
|
{
|
||||||
var deletedMessage = await _messengerAppService.DeleteMessageAsync(messageId);
|
try
|
||||||
|
{
|
||||||
var targetGroups = deletedMessage.RecipientIds
|
await _messengerAppService.DeleteMessageAsync(messageId);
|
||||||
.Append(deletedMessage.SenderId)
|
}
|
||||||
.Distinct()
|
catch (OperationCanceledException) when (Context.ConnectionAborted.IsCancellationRequested)
|
||||||
.Select(UserGroupName)
|
{
|
||||||
.ToList();
|
return;
|
||||||
|
}
|
||||||
await Clients.Groups(targetGroups).SendAsync("MessengerMessageDeleted", deletedMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string TenantKey => _currentTenant.Id?.ToString("N") ?? "host";
|
private string TenantKey => _currentTenant.Id?.ToString("N") ?? "host";
|
||||||
|
|
||||||
private string TenantGroupName() => $"messenger:tenant:{TenantKey}";
|
private string TenantGroupName() => $"messenger:tenant:{TenantKey}";
|
||||||
|
|
||||||
private string UserGroupName(Guid userId) => $"messenger:user:{TenantKey}:{userId:N}";
|
public static string UserGroupName(Guid? tenantId, Guid userId) =>
|
||||||
|
$"messenger:user:{tenantId?.ToString("N") ?? "host"}:{userId:N}";
|
||||||
|
|
||||||
|
private string UserGroupName(Guid userId) => UserGroupName(_currentTenant.Id, userId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1421,8 +1421,6 @@ public class PlatformDbContext :
|
||||||
b.Property(x => x.LastMessagePreview).HasMaxLength(512);
|
b.Property(x => x.LastMessagePreview).HasMaxLength(512);
|
||||||
b.Property(x => x.MessageCount).HasDefaultValue(0);
|
b.Property(x => x.MessageCount).HasDefaultValue(0);
|
||||||
|
|
||||||
b.HasIndex(x => new { x.TenantId, x.ParticipantKey }).IsUnique().HasFilter("[IsDeleted] = 0");
|
|
||||||
|
|
||||||
b.HasMany(x => x.Messages)
|
b.HasMany(x => x.Messages)
|
||||||
.WithOne(x => x.Conversation)
|
.WithOne(x => x.Conversation)
|
||||||
.HasForeignKey(x => x.ConversationId)
|
.HasForeignKey(x => x.ConversationId)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Sozsoft.Platform.Migrations
|
namespace Sozsoft.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20260624175615_Initial")]
|
[Migration("20260625183130_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -3904,10 +3904,6 @@ namespace Sozsoft.Platform.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "ParticipantKey")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("[IsDeleted] = 0");
|
|
||||||
|
|
||||||
b.ToTable("Adm_T_MessengerConversation", (string)null);
|
b.ToTable("Adm_T_MessengerConversation", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3730,13 +3730,6 @@ namespace Sozsoft.Platform.Migrations
|
||||||
unique: true,
|
unique: true,
|
||||||
filter: "[IsDeleted] = 0");
|
filter: "[IsDeleted] = 0");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Adm_T_MessengerConversation_TenantId_ParticipantKey",
|
|
||||||
table: "Adm_T_MessengerConversation",
|
|
||||||
columns: new[] { "TenantId", "ParticipantKey" },
|
|
||||||
unique: true,
|
|
||||||
filter: "[IsDeleted] = 0");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Adm_T_MessengerConversationMessage_ConversationId",
|
name: "IX_Adm_T_MessengerConversationMessage_ConversationId",
|
||||||
table: "Adm_T_MessengerConversationMessage",
|
table: "Adm_T_MessengerConversationMessage",
|
||||||
|
|
@ -3901,10 +3901,6 @@ namespace Sozsoft.Platform.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "ParticipantKey")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("[IsDeleted] = 0");
|
|
||||||
|
|
||||||
b.ToTable("Adm_T_MessengerConversation", (string)null);
|
b.ToTable("Adm_T_MessengerConversation", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import React, { useState, useEffect, useRef } from 'react'
|
||||||
import Editor from '@monaco-editor/react'
|
import Editor from '@monaco-editor/react'
|
||||||
import { ComponentDefinition } from '../../proxy/developerKit/componentInfo'
|
import { ComponentDefinition } from '../../proxy/developerKit/componentInfo'
|
||||||
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
||||||
import { FaCheck, FaCode, FaSpinner, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
import { FaCheck, FaCode, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
||||||
import { toast } from '../ui'
|
import { Button, toast } from '../ui'
|
||||||
import Notification from '../ui/Notification/Notification'
|
import Notification from '../ui/Notification/Notification'
|
||||||
|
|
||||||
interface ComponentCodeEditorProps {
|
interface ComponentCodeEditorProps {
|
||||||
|
|
@ -560,55 +560,57 @@ export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-gray-800 border-b border-gray-700 p-4 flex items-end justify-between shrink-0">
|
<div className="bg-gray-800 border-b border-gray-700 p-4 flex items-end justify-between shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={handleFormatCode}
|
onClick={handleFormatCode}
|
||||||
disabled={isFormatting}
|
loading={isFormatting}
|
||||||
className="px-3 py-2 bg-gray-700 text-gray-300 rounded-lg text-xs font-medium hover:bg-gray-600 transition-colors flex items-center gap-2 disabled:opacity-50"
|
icon={<FaCode className="w-4 h-4" />}
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{isFormatting ? (
|
|
||||||
<FaSpinner className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FaCode className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
Formatla
|
Formatla
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => setShowSettings(!showSettings)}
|
onClick={() => setShowSettings(!showSettings)}
|
||||||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 ${
|
icon={<FaCog className="w-4 h-4" />}
|
||||||
showSettings
|
variant={showSettings ? 'solid' : 'default'}
|
||||||
? 'bg-blue-600 text-white'
|
color="blue-600"
|
||||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
size="sm"
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<FaCog className="w-4 h-4" />
|
|
||||||
Ayarlar
|
Ayarlar
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={handleResetChanges}
|
onClick={handleResetChanges}
|
||||||
className="px-3 py-2 bg-red-600 text-white rounded-lg text-xs font-medium hover:bg-red-700 transition-colors flex items-center gap-2"
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
|
variant="solid"
|
||||||
|
color="red-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaTimes className="w-4 h-4" />
|
|
||||||
Sıfırla
|
Sıfırla
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={handleApplyChanges}
|
onClick={handleApplyChanges}
|
||||||
className="px-3 py-2 bg-green-600 text-white rounded-lg text-xs font-medium hover:bg-green-700 transition-colors flex items-center gap-2"
|
icon={<FaCheck className="w-4 h-4" />}
|
||||||
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaCheck className="w-4 h-4" />
|
|
||||||
Uygula
|
Uygula
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center justify-end">
|
<div className="col-span-2 flex items-center justify-end">
|
||||||
<button
|
<Button
|
||||||
onClick={onComponentSave}
|
onClick={onComponentSave}
|
||||||
className="flex items-center gap-2 bg-yellow-600 text-white px-4 py-2 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
icon={<FaSave className="w-4 h-4" />}
|
||||||
|
variant="solid"
|
||||||
|
color="yellow-600"
|
||||||
|
size="sm"
|
||||||
|
className="shadow-sm"
|
||||||
>
|
>
|
||||||
<FaSave className="w-4 h-4" />
|
|
||||||
Kaydet
|
Kaydet
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -656,14 +658,15 @@ export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">Mini Harita</label>
|
<label className="block text-sm font-medium text-gray-300 mb-2">Mini Harita</label>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setMinimap(!minimap)}
|
onClick={() => setMinimap(!minimap)}
|
||||||
className={`w-full px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
block
|
||||||
minimap ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
variant={minimap ? 'solid' : 'default'}
|
||||||
}`}
|
color="blue-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{minimap ? 'Etkin' : 'Devre Dışı'}
|
{minimap ? 'Etkin' : 'Devre Dışı'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
FaEye,
|
FaEye,
|
||||||
FaEyeSlash
|
FaEyeSlash
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
import { Button } from "../ui";
|
||||||
import { PanelState } from "./data/componentDefinitions";
|
import { PanelState } from "./data/componentDefinitions";
|
||||||
|
|
||||||
interface PanelManagerProps {
|
interface PanelManagerProps {
|
||||||
|
|
@ -37,13 +38,13 @@ export const PanelManager: React.FC<PanelManagerProps> = ({
|
||||||
<FaBars className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
<FaBars className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">Panel Manager</h2>
|
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">Panel Manager</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
|
icon={<FaTimes className="w-5 h-5" />}
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
title="Kapat"
|
title="Kapat"
|
||||||
>
|
/>
|
||||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">Customize Workspace</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">Customize Workspace</p>
|
||||||
|
|
@ -55,13 +56,20 @@ export const PanelManager: React.FC<PanelManagerProps> = ({
|
||||||
<Icon className="w-4 h-4 text-gray-600 dark:text-gray-300" />
|
<Icon className="w-4 h-4 text-gray-600 dark:text-gray-300" />
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{label}</span>
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={() => onPanelToggle(key)}
|
onClick={() => onPanelToggle(key)}
|
||||||
className={`p-1 rounded transition-colors ${panelState[key] ? "text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900" : "text-gray-400 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
|
icon={
|
||||||
|
panelState[key] ? (
|
||||||
|
<FaEye className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<FaEyeSlash className="w-4 h-4" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant={panelState[key] ? "twoTone" : "plain"}
|
||||||
|
color="blue-600"
|
||||||
|
size="xs"
|
||||||
title={panelState[key] ? "Hide" : "Show"}
|
title={panelState[key] ? "Hide" : "Show"}
|
||||||
>
|
/>
|
||||||
{panelState[key] ? <FaEye className="w-4 h-4" /> : <FaEyeSlash className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import TailwindModal from "./TailwindModal";
|
||||||
import { ComponentInfo, HookInfo, PropertyInfo } from "../../proxy/developerKit/componentInfo";
|
import { ComponentInfo, HookInfo, PropertyInfo } from "../../proxy/developerKit/componentInfo";
|
||||||
import { getComponentDefinition } from "./data/componentDefinitions";
|
import { getComponentDefinition } from "./data/componentDefinitions";
|
||||||
import { Button } from "../ui";
|
import { Button } from "../ui";
|
||||||
|
import { FaCheck, FaCode, FaTimes, FaTrash } from "react-icons/fa";
|
||||||
|
|
||||||
interface PropertyPanelProps {
|
interface PropertyPanelProps {
|
||||||
selectedComponent: ComponentInfo | null;
|
selectedComponent: ComponentInfo | null;
|
||||||
|
|
@ -284,13 +285,16 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||||
placeholder={`Enter ${property.name}`}
|
placeholder={`Enter ${property.name}`}
|
||||||
/>
|
/>
|
||||||
{isTailwindProperty && (
|
{isTailwindProperty && (
|
||||||
<button
|
<Button
|
||||||
onClick={() => openTailwindModal(property.name)}
|
onClick={() => openTailwindModal(property.name)}
|
||||||
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 dark:text-white transition-colors text-sm"
|
icon={<FaCode className="w-3 h-3" />}
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="xs"
|
||||||
title="Select Tailwind Classes"
|
title="Select Tailwind Classes"
|
||||||
>
|
>
|
||||||
TW
|
TW
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isColorProperty && (
|
{isColorProperty && (
|
||||||
<input
|
<input
|
||||||
|
|
@ -317,13 +321,16 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||||
placeholder={`Enter ${property.name}`}
|
placeholder={`Enter ${property.name}`}
|
||||||
/>
|
/>
|
||||||
{isTailwindProperty && (
|
{isTailwindProperty && (
|
||||||
<button
|
<Button
|
||||||
onClick={() => openTailwindModal(property.name)}
|
onClick={() => openTailwindModal(property.name)}
|
||||||
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 dark:text-white transition-colors text-sm"
|
icon={<FaCode className="w-3 h-3" />}
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="xs"
|
||||||
title="Select Tailwind Classes"
|
title="Select Tailwind Classes"
|
||||||
>
|
>
|
||||||
TW
|
TW
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isColorProperty && (
|
{isColorProperty && (
|
||||||
<input
|
<input
|
||||||
|
|
@ -489,33 +496,31 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-2 p-1">
|
<div className="flex gap-2 p-1">
|
||||||
<button
|
<Button
|
||||||
className={`px-3 py-1 font-medium border-b-2 transition-colors ${
|
variant={activeTab === "props" ? "twoTone" : "plain"}
|
||||||
activeTab === "props"
|
color="blue-600"
|
||||||
? "border-blue-500 text-blue-700 bg-white dark:bg-gray-900 dark:text-blue-400 dark:border-blue-400"
|
size="xs"
|
||||||
: "border-transparent text-gray-500 bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:border-transparent"
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("props")}
|
onClick={() => setActiveTab("props")}
|
||||||
>
|
>
|
||||||
Properties
|
Properties
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className={`px-3 py-1 font-medium border-b-2 transition-colors ${
|
variant={activeTab === "hooks" ? "twoTone" : "plain"}
|
||||||
activeTab === "hooks"
|
color="blue-600"
|
||||||
? "border-blue-500 text-blue-700 bg-white dark:bg-gray-900 dark:text-blue-400 dark:border-blue-400"
|
size="xs"
|
||||||
: "border-transparent text-gray-500 bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:border-transparent"
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("hooks")}
|
onClick={() => setActiveTab("hooks")}
|
||||||
>
|
>
|
||||||
Hooks
|
Hooks
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Sil Butonu */}
|
{/* Sil Butonu */}
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
size="sm"
|
color="red-600"
|
||||||
className="mr-2 px-3 py-1 rounded bg-red-500 text-white hover:bg-red-600 dark:bg-red-700 dark:hover:bg-red-800 dark:text-white transition-colors text-sm"
|
size="xs"
|
||||||
|
icon={<FaTrash className="w-4 h-4" />}
|
||||||
|
className="mr-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedComponent) {
|
if (selectedComponent) {
|
||||||
if (
|
if (
|
||||||
|
|
@ -539,23 +544,23 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
icon={<FaCheck className="w-4 h-4" />}
|
||||||
onClick={
|
onClick={
|
||||||
activeTab === "props"
|
activeTab === "props"
|
||||||
? handleApplyPropChanges
|
? handleApplyPropChanges
|
||||||
: handleApplyHookChanges
|
: handleApplyHookChanges
|
||||||
}
|
}
|
||||||
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
|
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
|
||||||
className={`flex-1 rounded-md font-medium transition-colors ${
|
className="flex-1"
|
||||||
(activeTab === "props" ? hasChanges : hasHookChanges)
|
|
||||||
? "bg-green-500 text-white hover:bg-green-600"
|
|
||||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Uygula
|
Uygula
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="solid"
|
||||||
|
color="red-600"
|
||||||
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
onClick={
|
onClick={
|
||||||
activeTab === "props"
|
activeTab === "props"
|
||||||
? handleResetChanges
|
? handleResetChanges
|
||||||
|
|
@ -565,11 +570,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
|
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
|
||||||
className={`flex-1 rounded-md font-medium transition-colors ${
|
className="flex-1"
|
||||||
(activeTab === "props" ? hasChanges : hasHookChanges)
|
|
||||||
? "bg-red-500 text-white hover:bg-red-600"
|
|
||||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
İptal
|
İptal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { searchTailwindClasses, TAILWIND_CLASSES } from './data/tailwindClasses';
|
import { searchTailwindClasses, TAILWIND_CLASSES } from './data/tailwindClasses';
|
||||||
import { Button } from '../ui';
|
import { Button } from '../ui';
|
||||||
|
import { FaTimes } from 'react-icons/fa';
|
||||||
|
|
||||||
interface TailwindModalProps {
|
interface TailwindModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -53,12 +54,13 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Tailwind CSS Classes</h2>
|
<h2 className="text-xl font-semibold text-gray-800">Tailwind CSS Classes</h2>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-500 hover:text-gray-700 text-2xl"
|
icon={<FaTimes className="w-5 h-5" />}
|
||||||
>
|
variant="plain"
|
||||||
×
|
size="xs"
|
||||||
</button>
|
title="Close"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and Filter */}
|
{/* Search and Filter */}
|
||||||
|
|
@ -108,9 +110,12 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
||||||
className="group relative"
|
className="group relative"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant='default'
|
block
|
||||||
|
variant="twoTone"
|
||||||
|
color="blue-600"
|
||||||
|
size="xs"
|
||||||
onClick={() => handleClassSelect(className)}
|
onClick={() => handleClassSelect(className)}
|
||||||
className="w-full text-left px-3 py-2 text-sm bg-gray-100 hover:bg-blue-100 rounded border hover:border-blue-300 transition-colors"
|
className="justify-start"
|
||||||
>
|
>
|
||||||
<span className="font-mono text-xs text-gray-600">
|
<span className="font-mono text-xs text-gray-600">
|
||||||
{className}
|
{className}
|
||||||
|
|
@ -129,16 +134,17 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant='default'
|
variant="default"
|
||||||
|
size="sm"
|
||||||
onClick={() => onSelectClass('')}
|
onClick={() => onSelectClass('')}
|
||||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
|
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='solid'
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { FaUpload, FaFile, FaTimes, FaRegCircle } from 'react-icons/fa'
|
import { FaUpload, FaFile, FaTimes, FaRegCircle } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface FileUploadAreaProps {
|
interface FileUploadAreaProps {
|
||||||
onFileUpload: (file: File) => void
|
onFileUpload: (file: File) => void
|
||||||
|
|
@ -149,12 +150,13 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={clearFile}
|
onClick={clearFile}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:text-gray-500 dark:hover:text-gray-300 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
>
|
variant="plain"
|
||||||
<FaTimes className="w-4 h-4" />
|
size="xs"
|
||||||
</button>
|
title={translate('::Clear')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -167,23 +169,19 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedFile && !error && (
|
{selectedFile && !error && (
|
||||||
<button
|
<Button
|
||||||
onClick={uploadFile}
|
onClick={uploadFile}
|
||||||
disabled={loading}
|
loading={loading}
|
||||||
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center space-x-2"
|
block
|
||||||
|
icon={<FaUpload className="w-4 h-4" />}
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading
|
||||||
<>
|
? translate('::App.Uploading')
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
: translate('::App.Listforms.ImportManager.UploadFile')}
|
||||||
<span>{translate('::App.Uploading')}</span>
|
</Button>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaUpload className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.Listforms.ImportManager.UploadFile')}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { ImportService } from '@/services/import.service'
|
||||||
import { GridDto } from '@/proxy/form/models'
|
import { GridDto } from '@/proxy/form/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface ImportDashboardProps {
|
interface ImportDashboardProps {
|
||||||
gridDto: GridDto
|
gridDto: GridDto
|
||||||
|
|
@ -333,20 +334,25 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
{/* Navigation Tabs */}
|
{/* Navigation Tabs */}
|
||||||
<div className="flex space-x-1 mb-4 bg-white dark:bg-gray-800 dark:border-gray-800 rounded-lg p-1 shadow-sm border border-gray-200 flex-shrink-0">
|
<div className="flex space-x-1 mb-4 bg-white dark:bg-gray-800 dark:border-gray-800 rounded-lg p-1 shadow-sm border border-gray-200 flex-shrink-0">
|
||||||
{['import', 'preview', 'history'].map((tab) => (
|
{['import', 'preview', 'history'].map((tab) => (
|
||||||
<button
|
<Button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(tab as TabNames)}
|
onClick={() => setActiveTab(tab as TabNames)}
|
||||||
className={`px-3 py-2 rounded-md font-medium transition-all duration-200 flex items-center space-x-2 ${
|
icon={
|
||||||
activeTab === tab
|
tab === 'import' ? (
|
||||||
? 'bg-blue-500 text-white shadow-md'
|
<FaUpload className="w-4 h-4" />
|
||||||
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-gray-100 dark:hover:bg-gray-800'
|
) : tab === 'preview' ? (
|
||||||
}`}
|
<FaEye className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<FaClock className="w-4 h-4" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant={activeTab === tab ? 'solid' : 'plain'}
|
||||||
|
color="blue-600"
|
||||||
|
size="sm"
|
||||||
|
className={activeTab === tab ? 'shadow-md' : ''}
|
||||||
>
|
>
|
||||||
{tab === 'import' && <FaUpload className="w-4 h-4" />}
|
|
||||||
{tab === 'preview' && <FaEye className="w-4 h-4" />}
|
|
||||||
{tab === 'history' && <FaClock className="w-4 h-4" />}
|
|
||||||
<span className="capitalize">{tab}</span>
|
<span className="capitalize">{tab}</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -368,27 +374,27 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
|
|
||||||
{/* Template Options */}
|
{/* Template Options */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => generateTemplate('excel')}
|
onClick={() => generateTemplate('excel')}
|
||||||
disabled={generating}
|
disabled={generating}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-green-200 dark:border-gray-700 rounded-md hover:border-green-300 hover:bg-green-50 dark:hover:bg-gray-800 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-900 text-xs"
|
icon={<FaFileExcel className="w-3.5 h-3.5" />}
|
||||||
|
variant="twoTone"
|
||||||
|
color="green-600"
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
|
|
||||||
<span className="font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
|
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
|
||||||
</span>
|
</Button>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => generateTemplate('csv')}
|
onClick={() => generateTemplate('csv')}
|
||||||
disabled={generating}
|
disabled={generating}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-blue-200 dark:border-gray-700 rounded-md hover:border-blue-300 hover:bg-blue-50 dark:hover:bg-gray-800 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-900 text-xs"
|
icon={<FaFileAlt className="w-3.5 h-3.5" />}
|
||||||
|
variant="twoTone"
|
||||||
|
color="blue-600"
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
|
|
||||||
<span className="font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{translate('::App.Listforms.ImportManager.CsvTemplate')}
|
{translate('::App.Listforms.ImportManager.CsvTemplate')}
|
||||||
</span>
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -567,18 +573,15 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => toggleSessionExecutes(session.id)}
|
onClick={() => toggleSessionExecutes(session.id)}
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
icon={<FaEye className="w-4 h-4" />}
|
||||||
expandedSessions.has(session.id)
|
variant={expandedSessions.has(session.id) ? 'twoTone' : 'plain'}
|
||||||
? 'text-red-500 bg-red-50 hover:text-red-600 hover:bg-red-100 dark:bg-gray-800 dark:text-red-300 dark:hover:bg-gray-700'
|
color="red-600"
|
||||||
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:text-gray-500 dark:hover:text-gray-300 dark:hover:bg-gray-800'
|
size="xs"
|
||||||
}`}
|
|
||||||
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
|
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
|
||||||
>
|
/>
|
||||||
<FaEye className="w-4 h-4" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// Execute bilgilerini manuel olarak yenile
|
// Execute bilgilerini manuel olarak yenile
|
||||||
if (sessionExecutes[session.id]) {
|
if (sessionExecutes[session.id]) {
|
||||||
|
|
@ -602,12 +605,13 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 rounded-lg transition-colors text-gray-400 hover:text-blue-500 hover:bg-blue-50 dark:text-gray-500 dark:hover:text-blue-300 dark:hover:bg-gray-800"
|
icon={<FaSync className="w-4 h-4" />}
|
||||||
|
variant="twoTone"
|
||||||
|
color="blue-600"
|
||||||
|
size="xs"
|
||||||
title={translate('::App.Listforms.ImportManager.RefreshExecutionDetails')}
|
title={translate('::App.Listforms.ImportManager.RefreshExecutionDetails')}
|
||||||
>
|
/>
|
||||||
<FaSync className="w-4 h-4" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (currentSession?.id === session.id) {
|
if (currentSession?.id === session.id) {
|
||||||
return // Don't delete if it's the current session
|
return // Don't delete if it's the current session
|
||||||
|
|
@ -616,19 +620,16 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
await loadImportHistory()
|
await loadImportHistory()
|
||||||
}}
|
}}
|
||||||
disabled={currentSession?.id === session.id}
|
disabled={currentSession?.id === session.id}
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
icon={<FaTrashAlt className="w-4 h-4" />}
|
||||||
currentSession?.id === session.id
|
variant="twoTone"
|
||||||
? 'text-gray-300 dark:text-gray-600 cursor-not-allowed'
|
color="red-600"
|
||||||
: 'text-gray-400 hover:text-red-500 hover:bg-red-50 dark:text-gray-500 dark:hover:text-red-300 dark:hover:bg-gray-800'
|
size="xs"
|
||||||
}`}
|
|
||||||
title={
|
title={
|
||||||
currentSession?.id === session.id
|
currentSession?.id === session.id
|
||||||
? translate('::App.Listforms.ImportManager.CannotDeleteActiveSession')
|
? translate('::App.Listforms.ImportManager.CannotDeleteActiveSession')
|
||||||
: translate('::App.Listforms.ImportManager.DeleteImportSession')
|
: translate('::App.Listforms.ImportManager.DeleteImportSession')
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<FaTrashAlt className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -730,20 +731,24 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
execute.status === 'failed') &&
|
execute.status === 'failed') &&
|
||||||
execute.errorRows > 0 && (
|
execute.errorRows > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => toggleErrors(execute.id)}
|
onClick={() => toggleErrors(execute.id)}
|
||||||
className="flex items-center space-x-1 text-xs text-orange-600 hover:text-orange-700 dark:text-orange-300 dark:hover:text-orange-200 font-medium"
|
icon={
|
||||||
>
|
expandedErrors.has(execute.id) ? (
|
||||||
{expandedErrors.has(execute.id) ? (
|
|
||||||
<FaChevronUp className="w-3 h-3" />
|
<FaChevronUp className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
<FaChevronDown className="w-3 h-3" />
|
<FaChevronDown className="w-3 h-3" />
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
variant="twoTone"
|
||||||
|
color="orange-600"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{execute.errorRows} hata detayı
|
{execute.errorRows} hata detayı
|
||||||
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'}
|
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{expandedErrors.has(execute.id) && (
|
{expandedErrors.has(execute.id) && (
|
||||||
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50 dark:border-gray-700 dark:bg-gray-800">
|
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ListFormImportDto } from '@/proxy/imports/models'
|
||||||
import { GridDto } from '@/proxy/form/models'
|
import { GridDto } from '@/proxy/form/models'
|
||||||
import { ImportService } from '@/services/import.service'
|
import { ImportService } from '@/services/import.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface ImportPreviewProps {
|
interface ImportPreviewProps {
|
||||||
session: ListFormImportDto
|
session: ListFormImportDto
|
||||||
|
|
@ -314,28 +315,27 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<button className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors flex items-center space-x-2">
|
<Button
|
||||||
<FaTimes className="w-4 h-4" />
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
<span>{translate('::Cancel')}</span>
|
variant="default"
|
||||||
</button>
|
size="sm"
|
||||||
|
>
|
||||||
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={handleExecute}
|
onClick={handleExecute}
|
||||||
disabled={!canExecute || loading}
|
disabled={!canExecute || loading}
|
||||||
className="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center space-x-2"
|
loading={loading}
|
||||||
|
icon={<FaPlay className="w-4 h-4" />}
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading
|
||||||
<>
|
? translate('::App.Listforms.Status.Processing')
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
: translate('::App.Listforms.ImportManager.Button.ExecuteImport')}
|
||||||
<span>{translate('::App.Listforms.Status.Processing')}</span>
|
</Button>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaPlay className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.Listforms.ImportManager.Button.ExecuteImport')}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import Demo from '@/views/public/Demo'
|
||||||
import { ScrollContext } from '@/contexts/ScrollContext'
|
import { ScrollContext } from '@/contexts/ScrollContext'
|
||||||
import useDarkMode from '@/utils/hooks/useDarkmode'
|
import useDarkMode from '@/utils/hooks/useDarkmode'
|
||||||
import { THEME_ENUM } from '@/constants/theme.constant'
|
import { THEME_ENUM } from '@/constants/theme.constant'
|
||||||
|
import { Button } from '../ui'
|
||||||
|
|
||||||
interface NavLink {
|
interface NavLink {
|
||||||
resourceKey?: string
|
resourceKey?: string
|
||||||
|
|
@ -108,13 +109,15 @@ const PublicLayout = () => {
|
||||||
const isLoginLink = (resourceKey?: string) => resourceKey === 'Public.nav.giris'
|
const isLoginLink = (resourceKey?: string) => resourceKey === 'Public.nav.giris'
|
||||||
|
|
||||||
const demoButton = (className = '', onClick?: () => void) => (
|
const demoButton = (className = '', onClick?: () => void) => (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDemoOpen(true)
|
setIsDemoOpen(true)
|
||||||
onClick?.()
|
onClick?.()
|
||||||
}}
|
}}
|
||||||
className={`group inline-flex items-center justify-center gap-1.5 text-sm font-semibold text-gray-200 hover:text-white transition-colors ${className}`}
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={`group !h-9 !w-9 !bg-transparent !px-0 !text-gray-200 hover:!bg-transparent hover:!text-white !shadow-none !overflow-visible transition-colors ${className}`}
|
||||||
>
|
>
|
||||||
<span className="relative inline-flex h-7 w-7 items-center justify-center overflow-visible rounded-full border-2 border-stone-700 bg-gradient-to-b from-red-400 via-red-600 to-red-800 shadow-[inset_0_-3px_6px_rgba(0,0,0,0.35),0_1px_3px_rgba(0,0,0,0.35)] ring-1 ring-black/60 transition-all duration-200 group-hover:scale-105 group-hover:from-red-300 group-hover:via-red-500 group-hover:to-red-700 group-hover:shadow-[inset_0_-3px_6px_rgba(0,0,0,0.3),0_2px_8px_rgba(239,68,68,0.35)]">
|
<span className="relative inline-flex h-7 w-7 items-center justify-center overflow-visible rounded-full border-2 border-stone-700 bg-gradient-to-b from-red-400 via-red-600 to-red-800 shadow-[inset_0_-3px_6px_rgba(0,0,0,0.35),0_1px_3px_rgba(0,0,0,0.35)] ring-1 ring-black/60 transition-all duration-200 group-hover:scale-105 group-hover:from-red-300 group-hover:via-red-500 group-hover:to-red-700 group-hover:shadow-[inset_0_-3px_6px_rgba(0,0,0,0.3),0_2px_8px_rgba(239,68,68,0.35)]">
|
||||||
<span className="absolute inset-x-1 top-1 h-2 rounded-full bg-white/35 blur-[1px] transition-colors group-hover:bg-white/50" />
|
<span className="absolute inset-x-1 top-1 h-2 rounded-full bg-white/35 blur-[1px] transition-colors group-hover:bg-white/50" />
|
||||||
|
|
@ -122,34 +125,40 @@ const PublicLayout = () => {
|
||||||
{translate('::App.Demos')}
|
{translate('::App.Demos')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
const themeToggle = (
|
const themeToggle = (
|
||||||
<div className="inline-flex items-center rounded-lg p-0.5">
|
<div className="inline-flex h-9 items-center rounded-lg p-0.5">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
|
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
|
||||||
aria-pressed={!isDarkMode}
|
aria-pressed={!isDarkMode}
|
||||||
title="Light mode"
|
title="Light mode"
|
||||||
className={`inline-flex h-8 w-8 xl:w-auto items-center justify-center gap-1.5 px-0 xl:px-2.5 text-xs font-semibold transition-colors rounded-l-md ${
|
icon={<LuSun size={15} strokeWidth={1.8} />}
|
||||||
!isDarkMode ? 'bg-white text-gray-950' : 'text-gray-300 hover:text-white'
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
className={`!h-8 !w-8 xl:!w-auto !px-0 xl:!px-2.5 !rounded-l-md !rounded-r-none !border-transparent !shadow-none ${
|
||||||
|
!isDarkMode
|
||||||
|
? '!bg-white !text-gray-950'
|
||||||
|
: '!bg-transparent !text-gray-300 hover:!bg-transparent hover:!text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
/>
|
||||||
<LuSun size={15} strokeWidth={1.8} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
|
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
|
||||||
aria-pressed={isDarkMode}
|
aria-pressed={isDarkMode}
|
||||||
title="Dark mode"
|
title="Dark mode"
|
||||||
className={`inline-flex h-8 w-8 xl:w-auto items-center justify-center gap-1.5 px-0 xl:px-2.5 text-xs font-semibold transition-colors rounded-r-md ${
|
icon={<LuMoon size={15} strokeWidth={1.8} />}
|
||||||
isDarkMode ? 'bg-white text-gray-950' : 'text-gray-300 hover:text-white'
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
className={`!h-8 !w-8 xl:!w-auto !px-0 xl:!px-2.5 !rounded-r-md !rounded-l-none !border-transparent !shadow-none ${
|
||||||
|
isDarkMode
|
||||||
|
? '!bg-white !text-gray-950'
|
||||||
|
: '!bg-transparent !text-gray-300 hover:!bg-transparent hover:!text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
/>
|
||||||
<LuMoon size={15} strokeWidth={1.8} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -207,9 +216,11 @@ const PublicLayout = () => {
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
key={link.name}
|
key={link.name}
|
||||||
onClick={link.action}
|
onClick={link.action}
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
className={`${baseClass} ${activeClass}`}
|
className={`${baseClass} ${activeClass}`}
|
||||||
>
|
>
|
||||||
{link.icon && (
|
{link.icon && (
|
||||||
|
|
@ -221,20 +232,21 @@ const PublicLayout = () => {
|
||||||
)}
|
)}
|
||||||
{link.name}
|
{link.name}
|
||||||
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
|
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Right side: Language + Login */}
|
{/* Right side: Language + Login */}
|
||||||
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-2 xl:gap-3">
|
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
|
||||||
<div className="inline-flex items-center gap-1">
|
{demoButton('rounded-lg')}
|
||||||
{demoButton('h-9 px-3 rounded-lg')}
|
<div className="h-5 w-px bg-white/20" />
|
||||||
<div className="w-px h-5 bg-white/20"></div>
|
<div className="flex h-9 w-9 items-center justify-center">
|
||||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-5 w-px bg-white/20" />
|
||||||
{themeToggle}
|
{themeToggle}
|
||||||
<div className="w-px h-5 bg-white/20" />
|
<div className="h-5 w-px bg-white/20" />
|
||||||
{navLinks
|
{navLinks
|
||||||
.filter((l) => isLoginLink(l.resourceKey))
|
.filter((l) => isLoginLink(l.resourceKey))
|
||||||
.map((link) =>
|
.map((link) =>
|
||||||
|
|
@ -242,7 +254,7 @@ const PublicLayout = () => {
|
||||||
<Link
|
<Link
|
||||||
key={link.path}
|
key={link.path}
|
||||||
to={link.path}
|
to={link.path}
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-all duration-200 hover:shadow-blue-700/40 hover:-translate-y-px"
|
className="inline-flex h-9 items-center gap-1.5 px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-all duration-200 hover:shadow-blue-700/40 hover:-translate-y-px"
|
||||||
>
|
>
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -251,14 +263,21 @@ const PublicLayout = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<button
|
<Button
|
||||||
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
|
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
icon={
|
||||||
{isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />}
|
isOpen ? (
|
||||||
</button>
|
<LuX size={20} strokeWidth={2} />
|
||||||
|
) : (
|
||||||
|
<LuMenu size={20} strokeWidth={2} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Navigation */}
|
{/* Mobile Navigation */}
|
||||||
|
|
@ -309,26 +328,28 @@ const PublicLayout = () => {
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
key={link.name}
|
key={link.name}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
link.action?.()
|
link.action?.()
|
||||||
toggleMenu()
|
toggleMenu()
|
||||||
}}
|
}}
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
|
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
|
||||||
>
|
>
|
||||||
{link.icon && (
|
{link.icon && (
|
||||||
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
|
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
|
||||||
)}
|
)}
|
||||||
{link.name}
|
{link.name}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
|
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{demoButton('h-10 px-3 rounded-lg', toggleMenu)}
|
{demoButton('rounded-lg', toggleMenu)}
|
||||||
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
|
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
|
||||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { BillingCycle } from '@/proxy/order/models'
|
||||||
import { CartState } from '@/utils/cartUtils'
|
import { CartState } from '@/utils/cartUtils'
|
||||||
import { useScroll } from '@/contexts/ScrollContext'
|
import { useScroll } from '@/contexts/ScrollContext'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface BillingControlsProps {
|
interface BillingControlsProps {
|
||||||
globalBillingCycle: BillingCycle
|
globalBillingCycle: BillingCycle
|
||||||
|
|
@ -48,38 +49,32 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => !hasCartItems && setGlobalBillingCycle('monthly')}
|
onClick={() => !hasCartItems && setGlobalBillingCycle('monthly')}
|
||||||
disabled={hasCartItems}
|
disabled={hasCartItems}
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'}
|
||||||
globalBillingCycle === 'monthly'
|
color="blue-600"
|
||||||
? 'bg-blue-600 text-white shadow-md'
|
size="sm"
|
||||||
: hasCartItems
|
className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''}
|
||||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-500'
|
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
title={
|
title={
|
||||||
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{translate('::Public.products.billingcycle.monthly')}
|
{translate('::Public.products.billingcycle.monthly')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => !hasCartItems && setGlobalBillingCycle('yearly')}
|
onClick={() => !hasCartItems && setGlobalBillingCycle('yearly')}
|
||||||
disabled={hasCartItems}
|
disabled={hasCartItems}
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'}
|
||||||
globalBillingCycle === 'yearly'
|
color="blue-600"
|
||||||
? 'bg-blue-600 text-white shadow-md'
|
size="sm"
|
||||||
: hasCartItems
|
className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''}
|
||||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-500'
|
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
title={
|
title={
|
||||||
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{translate('::Public.products.billingcycle.yearly')}
|
{translate('::Public.products.billingcycle.yearly')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -93,20 +88,16 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => !hasCartItems && setGlobalPeriod(Math.max(1, globalPeriod - 1))}
|
onClick={() => !hasCartItems && setGlobalPeriod(Math.max(1, globalPeriod - 1))}
|
||||||
className={`p-2 rounded-md border border-gray-300 transition-colors dark:border-gray-700 dark:text-gray-200 ${
|
|
||||||
globalPeriod <= 1 || hasCartItems
|
|
||||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-500'
|
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
disabled={globalPeriod <= 1 || hasCartItems}
|
disabled={globalPeriod <= 1 || hasCartItems}
|
||||||
|
icon={<FaMinus className="w-4 h-4" />}
|
||||||
|
variant="default"
|
||||||
|
size="xs"
|
||||||
title={hasCartItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
title={hasCartItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
||||||
>
|
/>
|
||||||
<FaMinus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 bg-gray-50 px-4 py-2 rounded-lg dark:bg-gray-800">
|
<div className="flex items-center space-x-2 bg-gray-50 px-2 py-1 rounded-lg dark:bg-gray-800">
|
||||||
<span className="font-bold text-lg text-blue-600">{globalPeriod}</span>
|
<span className="font-bold text-lg text-blue-600">{globalPeriod}</span>
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{globalBillingCycle === 'monthly'
|
{globalBillingCycle === 'monthly'
|
||||||
|
|
@ -115,26 +106,25 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => !hasCartItems && setGlobalPeriod(globalPeriod + 1)}
|
onClick={() => !hasCartItems && setGlobalPeriod(globalPeriod + 1)}
|
||||||
className={`p-2 rounded-md border border-gray-300 transition-colors dark:border-gray-700 dark:text-gray-200 ${
|
|
||||||
hasCartItems
|
|
||||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-500'
|
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
disabled={hasCartItems}
|
disabled={hasCartItems}
|
||||||
|
icon={<FaPlus className="w-4 h-4" />}
|
||||||
|
variant="default"
|
||||||
|
size="xs"
|
||||||
title={hasCartItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
title={hasCartItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
||||||
>
|
/>
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={onCartClick}
|
onClick={onCartClick}
|
||||||
className="relative flex items-center gap-2 p-2 bg-green-600 text-white hover:bg-green-700 transition-colors rounded-lg shadow-md"
|
icon={<FaShoppingCart className="w-5 h-5" />}
|
||||||
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
size="sm"
|
||||||
|
className="relative shadow-md"
|
||||||
>
|
>
|
||||||
<FaShoppingCart className="w-5 h-5" />
|
|
||||||
<span className="font-medium text-sm">{translate('::Public.nav.basket')}</span>
|
<span className="font-medium text-sm">{translate('::Public.nav.basket')}</span>
|
||||||
|
|
||||||
{cartItemsCount > 0 && (
|
{cartItemsCount > 0 && (
|
||||||
|
|
@ -142,7 +132,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
||||||
{cartItemsCount}
|
{cartItemsCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import { FaTimes, FaMinus, FaPlus, FaShoppingBag, FaTrash } from 'react-icons/fa'
|
import { FaTimes, FaMinus, FaPlus, FaShoppingBag, FaTrash } from 'react-icons/fa'
|
||||||
import { BillingCycle, BasketItem } from '@/proxy/order/models'
|
import { BillingCycle, BasketItem } from '@/proxy/order/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface CartState {
|
interface CartState {
|
||||||
items: BasketItem[]
|
items: BasketItem[]
|
||||||
|
|
@ -59,20 +60,21 @@ export const Cart: React.FC<CartProps> = ({
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{cartState.items.length > 0 && (
|
{cartState.items.length > 0 && (
|
||||||
<button
|
<Button
|
||||||
onClick={handleClearCart}
|
onClick={handleClearCart}
|
||||||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors dark:hover:bg-red-950/40"
|
icon={<FaTrash className="w-3 h-3" />}
|
||||||
|
variant="twoTone"
|
||||||
|
color="red-600"
|
||||||
|
size="xs"
|
||||||
title={translate('::Public.cart.clear')}
|
title={translate('::Public.cart.clear')}
|
||||||
>
|
/>
|
||||||
<FaTrash className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors dark:text-gray-200 dark:hover:bg-gray-800"
|
icon={<FaTimes className="w-5 h-5" />}
|
||||||
>
|
variant="plain"
|
||||||
<FaTimes className="w-5 h-5" />
|
size="xs"
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -94,12 +96,14 @@ export const Cart: React.FC<CartProps> = ({
|
||||||
<h4 className="font-medium text-gray-900 text-sm leading-tight dark:text-gray-100">
|
<h4 className="font-medium text-gray-900 text-sm leading-tight dark:text-gray-100">
|
||||||
{translate('::' + item.product.name)}
|
{translate('::' + item.product.name)}
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<Button
|
||||||
onClick={() => removeItem(item.product.id)}
|
onClick={() => removeItem(item.product.id)}
|
||||||
className="text-red-500 hover:text-red-700 ml-2"
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
>
|
variant="plain"
|
||||||
<FaTimes className="w-4 h-4" />
|
color="red-600"
|
||||||
</button>
|
size="xs"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -125,19 +129,19 @@ export const Cart: React.FC<CartProps> = ({
|
||||||
|
|
||||||
{item.product.isQuantityBased && (
|
{item.product.isQuantityBased && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
|
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
|
||||||
className="p-1 rounded border border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
icon={<FaMinus className="w-3 h-3" />}
|
||||||
>
|
variant="default"
|
||||||
<FaMinus className="w-3 h-3" />
|
size="xs"
|
||||||
</button>
|
/>
|
||||||
<span className="w-8 text-center text-sm dark:text-gray-100">{item.quantity}</span>
|
<span className="w-8 text-center text-sm dark:text-gray-100">{item.quantity}</span>
|
||||||
<button
|
<Button
|
||||||
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
|
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
|
||||||
className="p-1 rounded border border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
icon={<FaPlus className="w-3 h-3" />}
|
||||||
>
|
variant="default"
|
||||||
<FaPlus className="w-3 h-3" />
|
size="xs"
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -162,12 +166,15 @@ export const Cart: React.FC<CartProps> = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={onCheckout}
|
onClick={onCheckout}
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200"
|
block
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
{translate('::Public.cart.checkout')}
|
{translate('::Public.cart.checkout')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { FaCheckCircle, FaArrowLeft } from 'react-icons/fa'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface OrderSuccessProps {
|
interface OrderSuccessProps {
|
||||||
orderId: string
|
orderId: string
|
||||||
|
|
@ -40,13 +41,14 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, onBackToSho
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 items-center justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 items-center justify-center">
|
||||||
<button
|
<Button
|
||||||
onClick={() => navigate(ROUTES_ENUM.public.home)}
|
onClick={() => navigate(ROUTES_ENUM.public.home)}
|
||||||
className="flex items-center justify-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
icon={<FaArrowLeft className="w-4 h-4" />}
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
{translate('::Public.notFound.button')}
|
{translate('::Public.notFound.button')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
import { OrderService } from '@/services/order.service'
|
import { OrderService } from '@/services/order.service'
|
||||||
import { CustomTenantDto } from '@/proxy/config/models'
|
import { CustomTenantDto } from '@/proxy/config/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface CartState {
|
interface CartState {
|
||||||
items: BasketItem[]
|
items: BasketItem[]
|
||||||
|
|
@ -443,23 +444,26 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
|
||||||
|
|
||||||
{/* Butonlar */}
|
{/* Butonlar */}
|
||||||
<div className="flex justify-between items-center mt-6">
|
<div className="flex justify-between items-center mt-6">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 dark:text-gray-300 rounded-lg"
|
icon={<FaArrowLeft className="w-4 h-4" />}
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
{translate('::Back')}
|
{translate('::Back')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg"
|
icon={<FaCreditCard className="w-5 h-5" />}
|
||||||
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaCreditCard className="w-5 h-5 mr-2" />
|
|
||||||
{selectedPaymentMethod === 'bank-transfer'
|
{selectedPaymentMethod === 'bank-transfer'
|
||||||
? translate('::Public.payment.buttons.completeOrder')
|
? translate('::Public.payment.buttons.completeOrder')
|
||||||
: translate('::Public.payment.buttons.pay')}
|
: translate('::Public.payment.buttons.pay')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { FaPlus, FaMinus } from 'react-icons/fa'
|
||||||
import { BillingCycle, Product, ProductDto } from '@/proxy/order/models'
|
import { BillingCycle, Product, ProductDto } from '@/proxy/order/models'
|
||||||
import { CartState } from '@/utils/cartUtils'
|
import { CartState } from '@/utils/cartUtils'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
product: ProductDto
|
product: ProductDto
|
||||||
|
|
@ -119,19 +120,19 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
{translate('::Public.products.quantity')}
|
{translate('::Public.products.quantity')}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
className="p-1 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
icon={<FaMinus className="w-4 h-4" />}
|
||||||
>
|
variant="default"
|
||||||
<FaMinus className="w-4 h-4" />
|
size="xs"
|
||||||
</button>
|
/>
|
||||||
<span className="w-12 text-center font-medium dark:text-gray-100">{quantity}</span>
|
<span className="w-12 text-center font-sm dark:text-gray-100">{quantity}</span>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setQuantity(quantity + 1)}
|
onClick={() => setQuantity(quantity + 1)}
|
||||||
className="p-1 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
icon={<FaPlus className="w-4 h-4" />}
|
||||||
>
|
variant="default"
|
||||||
<FaPlus className="w-4 h-4" />
|
size="xs"
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -165,17 +166,17 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
const isDisabled = isNonQuantityProduct && isInCart
|
const isDisabled = isNonQuantityProduct && isInCart
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
onClick={handleAddToCart}
|
onClick={handleAddToCart}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={`w-full font-medium py-3 px-4 rounded-lg transition-colors duration-200 transform dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white ${
|
block
|
||||||
isDisabled
|
variant="solid"
|
||||||
? 'bg-gray-400 text-gray-700 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400'
|
color="blue-600"
|
||||||
: 'bg-blue-600 hover:bg-blue-700 text-white hover:scale-[1.02] active:scale-[0.98]'
|
size="md"
|
||||||
}`}
|
className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''}
|
||||||
>
|
>
|
||||||
{isDisabled ? translate('::Public.products.inCart') : translate('::Public.products.addToCart')}
|
{isDisabled ? translate('::Public.products.inCart') : translate('::Public.products.addToCart')}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { BillingCycle, ProductDto } from '@/proxy/order/models'
|
||||||
import { OrderService } from '@/services/order.service'
|
import { OrderService } from '@/services/order.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Loading } from '../shared'
|
import { Loading } from '../shared'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface ProductCatalogProps {
|
interface ProductCatalogProps {
|
||||||
searchQuery: string
|
searchQuery: string
|
||||||
|
|
@ -88,7 +89,7 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
<aside className="lg:w-64 flex-shrink-0">
|
<aside className="lg:w-64 flex-shrink-0">
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 sticky top-40 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-2 sticky top-40 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<FaFilter className="w-5 h-5 text-gray-600 dark:text-gray-300" />
|
<FaFilter className="w-5 h-5 text-gray-600 dark:text-gray-300" />
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
|
@ -97,22 +98,23 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<button
|
<Button
|
||||||
key={category}
|
key={category}
|
||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
className={`w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center justify-between ${
|
block
|
||||||
selectedCategory === category
|
variant={selectedCategory === category ? 'twoTone' : 'plain'}
|
||||||
? 'bg-blue-100 text-blue-800 border border-blue-200'
|
color="blue-600"
|
||||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100'
|
size="md"
|
||||||
}`}
|
className="justify-start text-left"
|
||||||
>
|
>
|
||||||
<span>
|
<span className="flex w-full items-center justify-between gap-3">
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
{category === 'all'
|
{category === 'all'
|
||||||
? translate('::Public.products.categories.all')
|
? translate('::Public.products.categories.all')
|
||||||
: translate('::' + category)}
|
: translate('::' + category)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-xs px-2 py-1 rounded-full ${
|
className={`shrink-0 rounded-full px-2 py-0.5 text-xs ${
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-blue-200 text-blue-800'
|
? 'bg-blue-200 text-blue-800'
|
||||||
: 'bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-300'
|
: 'bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-300'
|
||||||
|
|
@ -120,7 +122,8 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
|
||||||
>
|
>
|
||||||
{getCategoryCount(category)}
|
{getCategoryCount(category)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</span>
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import React, { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface TenantFormProps {
|
interface TenantFormProps {
|
||||||
onSubmit: (tenant: CustomTenantDto) => void
|
onSubmit: (tenant: CustomTenantDto) => void
|
||||||
|
|
@ -84,9 +85,10 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsExisting(true)}
|
onClick={() => setIsExisting(true)}
|
||||||
className={`p-4 border-2 rounded-xl transition-all text-left select-none ${
|
variant="plain"
|
||||||
|
className={`block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none ${
|
||||||
isExisting === true
|
isExisting === true
|
||||||
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
|
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
|
||||||
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
|
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
|
||||||
|
|
@ -98,11 +100,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{translate('::Public.products.tenantForm.existing.desc')}
|
{translate('::Public.products.tenantForm.existing.desc')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsExisting(false)}
|
onClick={() => setIsExisting(false)}
|
||||||
className={`p-4 border-2 rounded-xl transition-all text-left select-none ${
|
variant="plain"
|
||||||
|
className={`block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none ${
|
||||||
isExisting === false
|
isExisting === false
|
||||||
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
|
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
|
||||||
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
|
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
|
||||||
|
|
@ -114,7 +117,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{translate('::Public.products.tenantForm.new.desc')}
|
{translate('::Public.products.tenantForm.new.desc')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,7 +130,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.payment.customer.code')}
|
{translate('::Public.payment.customer.code')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative flex flex-col sm:flex-row sm:items-stretch">
|
<div className="relative flex flex-col sm:flex-row sm:items-stretch gap-3">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
|
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||||
<input
|
<input
|
||||||
|
|
@ -143,17 +146,20 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
await getTenantInfo()
|
await getTenantInfo()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 sm:rounded-l-lg sm:rounded-r-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
className="h-[40px] w-full pl-10 pr-4 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 sm:rounded-l-lg sm:rounded-r-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={getTenantInfo}
|
onClick={getTenantInfo}
|
||||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 text-white rounded-lg sm:rounded-r-lg sm:rounded-l-none hover:bg-blue-700 transition-colors min-w-[180px]"
|
icon={<FaSearch className="w-4 h-4" />}
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
size="sm"
|
||||||
|
className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2"
|
||||||
>
|
>
|
||||||
<FaSearch className="w-4 h-4" />
|
|
||||||
{translate('::Public.products.tenantForm.searchOrg')}
|
{translate('::Public.products.tenantForm.searchOrg')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.organizationName && (
|
{formData.organizationName && (
|
||||||
|
|
@ -276,12 +282,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="Enter your organization name"
|
placeholder="Enter your organization name"
|
||||||
value={formData.organizationName || ''}
|
value={formData.organizationName || ''}
|
||||||
onChange={(e) => handleInputChange('organizationName', e.target.value)}
|
onChange={(e) => handleInputChange('organizationName', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::LoginPanel.Profil')}
|
{translate('::LoginPanel.Profil')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -292,7 +298,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="Enter founder's name"
|
placeholder="Enter founder's name"
|
||||||
value={formData.founder || ''}
|
value={formData.founder || ''}
|
||||||
onChange={(e) => handleInputChange('founder', e.target.value)}
|
onChange={(e) => handleInputChange('founder', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -300,7 +306,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
|
{translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -311,12 +317,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="+90 (___) ___-____"
|
placeholder="+90 (___) ___-____"
|
||||||
value={formData.phoneNumber || ''}
|
value={formData.phoneNumber || ''}
|
||||||
onChange={(e) => handleInputChange('phoneNumber', e.target.value)}
|
onChange={(e) => handleInputChange('phoneNumber', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Abp.Account.EmailAddress')}
|
{translate('::Abp.Account.EmailAddress')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -327,14 +333,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="sample@email.com"
|
placeholder="sample@email.com"
|
||||||
value={formData.email || ''}
|
value={formData.email || ''}
|
||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::App.Address')}
|
{translate('::App.Address')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -345,14 +351,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
value={formData.address1 || ''}
|
value={formData.address1 || ''}
|
||||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.payment.customer.country')}
|
{translate('::Public.payment.customer.country')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -362,12 +368,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="Türkiye"
|
placeholder="Türkiye"
|
||||||
value={formData.country || ''}
|
value={formData.country || ''}
|
||||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
onChange={(e) => handleInputChange('country', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.common.city')}
|
{translate('::Public.common.city')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -378,7 +384,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="İstanbul"
|
placeholder="İstanbul"
|
||||||
value={formData.city || ''}
|
value={formData.city || ''}
|
||||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -386,7 +392,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.payment.customer.district')}
|
{translate('::Public.payment.customer.district')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -397,12 +403,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="Çekmeköy"
|
placeholder="Çekmeköy"
|
||||||
value={formData.district || ''}
|
value={formData.district || ''}
|
||||||
onChange={(e) => handleInputChange('district', e.target.value)}
|
onChange={(e) => handleInputChange('district', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.payment.customer.postalCode')}
|
{translate('::Public.payment.customer.postalCode')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -413,7 +419,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="34782"
|
placeholder="34782"
|
||||||
value={formData.postalCode || ''}
|
value={formData.postalCode || ''}
|
||||||
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -421,7 +427,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.contact.taxOffice')}
|
{translate('::Public.contact.taxOffice')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -432,12 +438,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="Kozyatağı"
|
placeholder="Kozyatağı"
|
||||||
value={formData.taxOffice}
|
value={formData.taxOffice}
|
||||||
onChange={(e) => handleInputChange('taxOffice', e.target.value)}
|
onChange={(e) => handleInputChange('taxOffice', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.contact.taxNumber')}
|
{translate('::Public.contact.taxNumber')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -448,14 +454,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder="1234567890"
|
placeholder="1234567890"
|
||||||
value={formData.vknTckn}
|
value={formData.vknTckn}
|
||||||
onChange={(e) => handleInputChange('vknTckn', e.target.value)}
|
onChange={(e) => handleInputChange('vknTckn', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{translate('::Public.payment.customer.reference')}
|
{translate('::Public.payment.customer.reference')}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -465,7 +471,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
placeholder={translate('::Public.payment.customer.reference')}
|
placeholder={translate('::Public.payment.customer.reference')}
|
||||||
value={formData.reference || ''}
|
value={formData.reference || ''}
|
||||||
onChange={(e) => handleInputChange('reference', e.target.value)}
|
onChange={(e) => handleInputChange('reference', e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -473,21 +479,24 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-6">
|
<div className="flex justify-between items-center mt-6">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(ROUTES_ENUM.public.products)}
|
onClick={() => navigate(ROUTES_ENUM.public.products)}
|
||||||
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 dark:text-gray-300 rounded-lg"
|
icon={<FaArrowLeft className="w-4 h-4" />}
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
{translate('::Back')}
|
{translate('::Back')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg"
|
icon={<FaArrowRight className="w-5 h-5" />}
|
||||||
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaArrowRight className="w-5 h-5 mr-2" />
|
|
||||||
{translate('::Abp.Account.ResetPassword.Continue')}
|
{translate('::Abp.Account.ResetPassword.Continue')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { MigrateLogEntry } from '@/proxy/setup/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface DbMigrateLogPanelProps {
|
interface DbMigrateLogPanelProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
|
@ -50,12 +51,14 @@ function DbMigrateLogPanel({ onClose }: DbMigrateLogPanelProps) {
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
||||||
<span className="text-white font-semibold text-sm">DB Migration Logs</span>
|
<span className="text-white font-semibold text-sm">DB Migration Logs</span>
|
||||||
{done && (
|
{done && (
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-white text-xs px-3 py-1 rounded border border-gray-600 hover:border-gray-400 transition-colors"
|
variant="default"
|
||||||
|
size="xs"
|
||||||
|
className="border-gray-600 text-gray-400 hover:border-gray-400 hover:text-white"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-3 font-mono text-xs leading-relaxed">
|
<div className="flex-1 overflow-y-auto p-3 font-mono text-xs leading-relaxed">
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ import Tooltip from '@/components/ui/Tooltip'
|
||||||
import { toast } from '@/components/ui'
|
import { toast } from '@/components/ui'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
import {
|
import {
|
||||||
|
deleteMessengerMessage,
|
||||||
getMessengerContacts,
|
getMessengerContacts,
|
||||||
getMessengerMessages,
|
getMessengerMessages,
|
||||||
MessengerAttachmentDto,
|
MessengerAttachmentDto,
|
||||||
MessengerContactDto,
|
MessengerContactDto,
|
||||||
MessengerMessageDto,
|
MessengerMessageDto,
|
||||||
|
sendMessengerMessage,
|
||||||
uploadMessengerAttachment,
|
uploadMessengerAttachment,
|
||||||
} from '@/services/messenger.service'
|
} from '@/services/messenger.service'
|
||||||
import { messengerSignalR } from '@/services/messenger.signalr'
|
import { messengerSignalR } from '@/services/messenger.signalr'
|
||||||
|
|
@ -43,6 +45,25 @@ const formatFileSize = (size: number) => {
|
||||||
const canDeleteMessage = (message: MessengerMessageDto, userId: string) =>
|
const canDeleteMessage = (message: MessengerMessageDto, userId: string) =>
|
||||||
message.senderId === userId && dayjs().diff(dayjs(message.sentAt), 'minute', true) < 10
|
message.senderId === userId && dayjs().diff(dayjs(message.sentAt), 'minute', true) < 10
|
||||||
|
|
||||||
|
const mergeMessages = (
|
||||||
|
currentMessages: MessengerMessageDto[],
|
||||||
|
nextMessages: MessengerMessageDto[],
|
||||||
|
) => {
|
||||||
|
const messagesById = new Map<string, MessengerMessageDto>()
|
||||||
|
|
||||||
|
currentMessages.forEach((message) => {
|
||||||
|
messagesById.set(message.id, message)
|
||||||
|
})
|
||||||
|
|
||||||
|
nextMessages.forEach((message) => {
|
||||||
|
messagesById.set(message.id, message)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(messagesById.values()).sort(
|
||||||
|
(first, second) => dayjs(first.sentAt).valueOf() - dayjs(second.sentAt).valueOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const MessengerWidget = () => {
|
const MessengerWidget = () => {
|
||||||
const auth = useStoreState((state) => state.auth)
|
const auth = useStoreState((state) => state.auth)
|
||||||
const tenantId = auth.user.tenantId || auth.tenant?.tenantId
|
const tenantId = auth.user.tenantId || auth.tenant?.tenantId
|
||||||
|
|
@ -64,6 +85,7 @@ const MessengerWidget = () => {
|
||||||
const [multiSelect, setMultiSelect] = useState(false)
|
const [multiSelect, setMultiSelect] = useState(false)
|
||||||
const [maximized, setMaximized] = useState(false)
|
const [maximized, setMaximized] = useState(false)
|
||||||
const [unreadByContact, setUnreadByContact] = useState<Record<string, number>>({})
|
const [unreadByContact, setUnreadByContact] = useState<Record<string, number>>({})
|
||||||
|
const [reachableContactIds, setReachableContactIds] = useState<string[]>([])
|
||||||
|
|
||||||
const unread = useMemo(
|
const unread = useMemo(
|
||||||
() => Object.values(unreadByContact).reduce((total, count) => total + count, 0),
|
() => Object.values(unreadByContact).reduce((total, count) => total + count, 0),
|
||||||
|
|
@ -74,9 +96,17 @@ const MessengerWidget = () => {
|
||||||
if (!auth.session.signedIn) return
|
if (!auth.session.signedIn) return
|
||||||
|
|
||||||
const unsubscribeMessage = messengerSignalR.onMessage((message) => {
|
const unsubscribeMessage = messengerSignalR.onMessage((message) => {
|
||||||
setMessages((prev) =>
|
setMessages((prev) => mergeMessages(prev, [message]))
|
||||||
prev.some((item) => item.id === message.id) ? prev : [...prev, message],
|
if (message.senderId !== auth.user.id) {
|
||||||
|
setReachableContactIds((prev) =>
|
||||||
|
prev.includes(message.senderId) ? prev : [...prev, message.senderId],
|
||||||
)
|
)
|
||||||
|
setContacts((prev) =>
|
||||||
|
prev.map((contact) =>
|
||||||
|
contact.id === message.senderId ? { ...contact, isOnline: true } : contact,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (message.senderId !== auth.user.id && (!open || !selectedIds.includes(message.senderId))) {
|
if (message.senderId !== auth.user.id && (!open || !selectedIds.includes(message.senderId))) {
|
||||||
setUnreadByContact((current) => ({
|
setUnreadByContact((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
|
@ -148,8 +178,8 @@ const MessengerWidget = () => {
|
||||||
maxResultCount: 50,
|
maxResultCount: 50,
|
||||||
sorting: 'sentAt desc',
|
sorting: 'sentAt desc',
|
||||||
})
|
})
|
||||||
.then((response) => setMessages(response.data))
|
.then((response) => setMessages((prev) => mergeMessages(prev, response.data)))
|
||||||
.catch(() => setMessages([]))
|
.catch(() => undefined)
|
||||||
}, [open, selectedIds])
|
}, [open, selectedIds])
|
||||||
|
|
||||||
const selectedContacts = useMemo(
|
const selectedContacts = useMemo(
|
||||||
|
|
@ -158,7 +188,11 @@ const MessengerWidget = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const canSendToSelectedContacts =
|
const canSendToSelectedContacts =
|
||||||
selectedContacts.length > 0 && selectedContacts.every((contact) => contact.isOnline)
|
selectedIds.length > 0 &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const contact = contacts.find((item) => item.id === id)
|
||||||
|
return contact?.isOnline || reachableContactIds.includes(id)
|
||||||
|
})
|
||||||
|
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
if (selectedIds.length === 0) return []
|
if (selectedIds.length === 0) return []
|
||||||
|
|
@ -175,7 +209,8 @@ const MessengerWidget = () => {
|
||||||
|
|
||||||
const toggleContact = (id: string) => {
|
const toggleContact = (id: string) => {
|
||||||
const contact = contacts.find((item) => item.id === id)
|
const contact = contacts.find((item) => item.id === id)
|
||||||
if (!contact?.isOnline) {
|
const canReachContact = contact?.isOnline || reachableContactIds.includes(id)
|
||||||
|
if (!canReachContact) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification
|
<Notification
|
||||||
title={translate('::MessengerWidget.OfflineUsersCannotReceiveMessages')}
|
title={translate('::MessengerWidget.OfflineUsersCannotReceiveMessages')}
|
||||||
|
|
@ -219,7 +254,6 @@ const MessengerWidget = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
setMaximized(false)
|
setMaximized(false)
|
||||||
setSelectedIds([])
|
setSelectedIds([])
|
||||||
setMessages([])
|
|
||||||
setText('')
|
setText('')
|
||||||
setAttachments([])
|
setAttachments([])
|
||||||
setShowEmoji(false)
|
setShowEmoji(false)
|
||||||
|
|
@ -277,11 +311,12 @@ const MessengerWidget = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await messengerSignalR.sendMessage({
|
const response = await sendMessengerMessage({
|
||||||
recipientIds: selectedIds,
|
recipientIds: selectedIds,
|
||||||
text: trimmedText,
|
text: trimmedText,
|
||||||
attachments,
|
attachments,
|
||||||
})
|
})
|
||||||
|
setMessages((prev) => mergeMessages(prev, [response.data]))
|
||||||
setText('')
|
setText('')
|
||||||
setAttachments([])
|
setAttachments([])
|
||||||
setShowEmoji(false)
|
setShowEmoji(false)
|
||||||
|
|
@ -300,7 +335,8 @@ const MessengerWidget = () => {
|
||||||
if (!window.confirm(translate('::MessengerWidget.DeleteMessageConfirmation'))) return
|
if (!window.confirm(translate('::MessengerWidget.DeleteMessageConfirmation'))) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await messengerSignalR.deleteMessage(messageId)
|
const response = await deleteMessengerMessage(messageId)
|
||||||
|
setMessages((prev) => prev.filter((item) => item.id !== response.data.messageId))
|
||||||
} catch {
|
} catch {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification title={translate('::MessengerWidget.MessageDeleteFailed')} type="danger" />,
|
<Notification title={translate('::MessengerWidget.MessageDeleteFailed')} type="danger" />,
|
||||||
|
|
@ -325,10 +361,12 @@ const MessengerWidget = () => {
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={translate('::MessengerWidget.Minimize')}
|
aria-label={translate('::MessengerWidget.Minimize')}
|
||||||
className="fixed inset-0 z-50 cursor-default bg-slate-950/25 backdrop-blur-[1px] dark:bg-black/55"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="fixed inset-0 z-50 !h-auto cursor-default bg-slate-950/25 !px-0 backdrop-blur-[1px] hover:!bg-slate-950/25 active:!bg-slate-950/25 dark:bg-black/55 dark:hover:!bg-black/55 dark:active:!bg-black/55"
|
||||||
onClick={closeWidget}
|
onClick={closeWidget}
|
||||||
/>
|
/>
|
||||||
<div className={panelClassName} role="dialog" aria-modal="true">
|
<div className={panelClassName} role="dialog" aria-modal="true">
|
||||||
|
|
@ -420,16 +458,20 @@ const MessengerWidget = () => {
|
||||||
{contacts.map((contact) => {
|
{contacts.map((contact) => {
|
||||||
const active = selectedIds.includes(contact.id)
|
const active = selectedIds.includes(contact.id)
|
||||||
const contactUnread = unreadByContact[contact.id] || 0
|
const contactUnread = unreadByContact[contact.id] || 0
|
||||||
|
const isReachable = contact.isOnline || reachableContactIds.includes(contact.id)
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={contact.id}
|
key={contact.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleContact(contact.id)}
|
onClick={() => toggleContact(contact.id)}
|
||||||
disabled={!contact.isOnline}
|
disabled={!isReachable}
|
||||||
className={`mb-1 flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
block
|
||||||
|
className={`mb-1 !flex !h-auto items-center gap-3 !rounded-md !px-2 !py-2 text-left transition ${
|
||||||
active
|
active
|
||||||
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-100'
|
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-100'
|
||||||
: contact.isOnline
|
: isReachable
|
||||||
? 'hover:bg-white dark:hover:bg-gray-800'
|
? 'hover:bg-white dark:hover:bg-gray-800'
|
||||||
: 'cursor-not-allowed opacity-60'
|
: 'cursor-not-allowed opacity-60'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -445,15 +487,15 @@ const MessengerWidget = () => {
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center gap-1 text-xs font-medium ${
|
className={`inline-flex items-center gap-1 text-xs font-medium ${
|
||||||
contact.isOnline ? 'text-emerald-600' : 'text-red-500'
|
isReachable ? 'text-emerald-600' : 'text-red-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`h-2 w-2 rounded-full ${
|
className={`h-2 w-2 rounded-full ${
|
||||||
contact.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
isReachable ? 'bg-emerald-500' : 'bg-red-500'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{contact.isOnline
|
{isReachable
|
||||||
? translate('::MessengerWidget.Online')
|
? translate('::MessengerWidget.Online')
|
||||||
: translate('::MessengerWidget.Offline')}
|
: translate('::MessengerWidget.Offline')}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -466,7 +508,7 @@ const MessengerWidget = () => {
|
||||||
{active && contactUnread === 0 && (
|
{active && contactUnread === 0 && (
|
||||||
<IoCheckmarkDone className="shrink-0 text-lg" />
|
<IoCheckmarkDone className="shrink-0 text-lg" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -628,14 +670,16 @@ const MessengerWidget = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{own && deletable && (
|
{own && deletable && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title={translate('::MessengerWidget.DeleteMessage')}
|
title={translate('::MessengerWidget.DeleteMessage')}
|
||||||
className="mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] text-gray-400 opacity-0 transition hover:bg-red-500 hover:text-white group-hover:opacity-100 focus:opacity-100"
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
className="mt-1 !h-5 !w-5 shrink-0 !px-0 text-[10px] text-gray-400 opacity-0 transition hover:!bg-red-500 hover:text-white group-hover:opacity-100 focus:opacity-100"
|
||||||
onClick={() => deleteMessage(message.id)}
|
onClick={() => deleteMessage(message.id)}
|
||||||
>
|
/>
|
||||||
<FaTrash />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -652,16 +696,19 @@ const MessengerWidget = () => {
|
||||||
className="inline-flex max-w-[220px] items-center gap-2 rounded-md bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
|
className="inline-flex max-w-[220px] items-center gap-2 rounded-md bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<span className="truncate">{file.fileName}</span>
|
<span className="truncate">{file.fileName}</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
className="!h-5 !w-5 shrink-0 !px-0 text-gray-500 hover:text-red-500"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAttachments((prev) =>
|
setAttachments((prev) =>
|
||||||
prev.filter((item) => item.savedFileName !== file.savedFileName),
|
prev.filter((item) => item.savedFileName !== file.savedFileName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<FaTrash />
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -746,18 +793,23 @@ const MessengerWidget = () => {
|
||||||
|
|
||||||
<div className="fixed bottom-5 right-5 z-[52]">
|
<div className="fixed bottom-5 right-5 z-[52]">
|
||||||
<Tooltip title={translate('::MessengerWidget.Title')}>
|
<Tooltip title={translate('::MessengerWidget.Title')}>
|
||||||
<button
|
<span className="pointer-events-auto relative inline-flex">
|
||||||
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
variant="solid"
|
||||||
|
color="emerald-600"
|
||||||
|
shape="circle"
|
||||||
|
icon={<IoChatbubbleEllipsesOutline />}
|
||||||
onClick={() => (open ? closeWidget() : setOpen(true))}
|
onClick={() => (open ? closeWidget() : setOpen(true))}
|
||||||
className="pointer-events-auto relative flex h-12 w-12 items-center justify-center rounded-full bg-emerald-600 text-2xl text-white shadow-xl transition hover:bg-emerald-700"
|
className="!h-12 !w-12 !px-0 text-2xl shadow-xl transition hover:!bg-emerald-700"
|
||||||
>
|
/>
|
||||||
<IoChatbubbleEllipsesOutline />
|
|
||||||
{unread > 0 && (
|
{unread > 0 && (
|
||||||
<span className="absolute -right-1 -top-1 flex min-h-[20px] min-w-[20px] items-center justify-center rounded-full bg-red-500 px-1.5 text-xs font-semibold leading-none text-white ring-2 ring-white dark:ring-gray-800">
|
<span className="absolute -right-1 -top-1 flex min-h-[20px] min-w-[20px] items-center justify-center rounded-full bg-red-500 px-1.5 text-xs font-semibold leading-none text-white ring-2 ring-white dark:ring-gray-800">
|
||||||
{unread > 99 ? '99+' : unread}
|
{unread > 99 ? '99+' : unread}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,13 @@ export interface MessengerGetMessagesInput {
|
||||||
sorting?: string
|
sorting?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MessengerSendMessageDto {
|
||||||
|
conversationId?: string
|
||||||
|
recipientIds: string[]
|
||||||
|
text?: string
|
||||||
|
attachments: MessengerAttachmentDto[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessengerMessageDto {
|
export interface MessengerMessageDto {
|
||||||
id: string
|
id: string
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
|
|
@ -62,6 +69,19 @@ export const getMessengerMessages = (data: MessengerGetMessagesInput) =>
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const sendMessengerMessage = (data: MessengerSendMessageDto) =>
|
||||||
|
apiService.fetchData<MessengerMessageDto>({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/app/messenger/send-message',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteMessengerMessage = (messageId: string) =>
|
||||||
|
apiService.fetchData<MessengerMessageDeletedDto>({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/api/app/messenger/messages/${messageId}`,
|
||||||
|
})
|
||||||
|
|
||||||
export const uploadMessengerAttachment = (file: File) => {
|
export const uploadMessengerAttachment = (file: File) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { store } from '@/store/store'
|
import { store } from '@/store/store'
|
||||||
import * as signalR from '@microsoft/signalr'
|
import * as signalR from '@microsoft/signalr'
|
||||||
import { MessengerAttachmentDto, MessengerMessageDeletedDto, MessengerMessageDto } from './messenger.service'
|
import {
|
||||||
|
MessengerMessageDeletedDto,
|
||||||
export interface MessengerSendMessageDto {
|
MessengerMessageDto,
|
||||||
conversationId?: string
|
MessengerSendMessageDto,
|
||||||
recipientIds: string[]
|
} from './messenger.service'
|
||||||
text?: string
|
|
||||||
attachments: MessengerAttachmentDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageHandler = (message: MessengerMessageDto) => void
|
type MessageHandler = (message: MessengerMessageDto) => void
|
||||||
type MessageDeletedHandler = (message: MessengerMessageDeletedDto) => void
|
type MessageDeletedHandler = (message: MessengerMessageDeletedDto) => void
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import Loading from '@/components/shared/Loading'
|
import Loading from '@/components/shared/Loading'
|
||||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
||||||
import { Avatar } from '@/components/ui'
|
import { Avatar, Button } from '@/components/ui'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
import { useStoreState } from '@/store'
|
import { useStoreState } from '@/store'
|
||||||
import UserProfileCard from '@/views/intranet/SocialWall/UserProfileCard'
|
import UserProfileCard from '@/views/intranet/SocialWall/UserProfileCard'
|
||||||
|
|
@ -59,7 +59,14 @@ function loadImg(src: string): Promise<HTMLImageElement | null> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRR(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
function drawRR(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
r: number,
|
||||||
|
) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x + r, y)
|
ctx.moveTo(x + r, y)
|
||||||
ctx.lineTo(x + w - r, y)
|
ctx.lineTo(x + w - r, y)
|
||||||
|
|
@ -73,7 +80,14 @@ function drawRR(ctx: CanvasRenderingContext2D, x: number, y: number, w: number,
|
||||||
ctx.closePath()
|
ctx.closePath()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTopRR(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
function drawTopRR(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
r: number,
|
||||||
|
) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x + r, y)
|
ctx.moveTo(x + r, y)
|
||||||
ctx.lineTo(x + w - r, y)
|
ctx.lineTo(x + w - r, y)
|
||||||
|
|
@ -233,7 +247,13 @@ async function captureAsJpeg(element: HTMLElement, filename: string, zoom: numbe
|
||||||
} else {
|
} else {
|
||||||
const nameSpan = row.querySelector('[data-user-name]') as HTMLElement | null
|
const nameSpan = row.querySelector('[data-user-name]') as HTMLElement | null
|
||||||
const nm = nameSpan?.textContent?.trim() || ''
|
const nm = nameSpan?.textContent?.trim() || ''
|
||||||
const initials = nm.split(' ').map((p) => p[0]).filter(Boolean).slice(0, 2).join('').toUpperCase()
|
const initials = nm
|
||||||
|
.split(' ')
|
||||||
|
.map((p) => p[0])
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2)
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.font = `bold 8px sans-serif`
|
ctx.font = `bold 8px sans-serif`
|
||||||
ctx.fillStyle = '#4f46e5'
|
ctx.fillStyle = '#4f46e5'
|
||||||
|
|
@ -477,7 +497,11 @@ function OrgChartNode({
|
||||||
nodeRefs.current[node.id] = el
|
nodeRefs.current[node.id] = el
|
||||||
}}
|
}}
|
||||||
className="flex flex-col items-center select-none"
|
className="flex flex-col items-center select-none"
|
||||||
style={applySelfTransform ? { transform: `translate(${position.x}px, ${position.y}px)` } : undefined}
|
style={
|
||||||
|
applySelfTransform
|
||||||
|
? { transform: `translate(${position.x}px, ${position.y}px)` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Node card */}
|
{/* Node card */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -492,35 +516,42 @@ function OrgChartNode({
|
||||||
style={{ cursor: dragging ? 'grabbing' : 'grab' }}
|
style={{ cursor: dragging ? 'grabbing' : 'grab' }}
|
||||||
>
|
>
|
||||||
{/* Header bar */}
|
{/* Header bar */}
|
||||||
<div data-header="" className={`${headerBg} rounded-t-xl px-3 py-2 flex items-center gap-2 dark:bg-gray-900 dark:text-gray-100`}>
|
<div
|
||||||
|
data-header=""
|
||||||
|
className={`${headerBg} rounded-t-xl px-3 py-2 flex items-center gap-2 dark:bg-gray-900 dark:text-gray-100`}
|
||||||
|
>
|
||||||
{mode === 'department' ? (
|
{mode === 'department' ? (
|
||||||
<FaBuilding className="w-3 h-3 text-white opacity-80 flex-shrink-0" />
|
<FaBuilding className="w-3 h-3 text-white opacity-80 flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<FaBriefcase className="w-3 h-3 text-white opacity-80 flex-shrink-0" />
|
<FaBriefcase className="w-3 h-3 text-white opacity-80 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span data-node-name="" className="text-white font-semibold text-xs truncate flex-1 dark:bg-gray-900 dark:text-gray-100">
|
<span
|
||||||
|
data-node-name=""
|
||||||
|
className="text-white font-semibold text-xs truncate flex-1 dark:bg-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
{node.name}
|
{node.name}
|
||||||
</span>
|
</span>
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<button
|
<Button
|
||||||
data-stop-drag="true"
|
data-stop-drag="true"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCollapsed((c) => !c)
|
setCollapsed((c) => !c)
|
||||||
requestRepaint()
|
requestRepaint()
|
||||||
}}
|
}}
|
||||||
className="text-white opacity-70 hover:opacity-100 transition-opacity ml-1 flex-shrink-0"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
{collapsed ? (
|
icon={
|
||||||
<FaChevronRight className="w-2.5 h-2.5" />
|
collapsed ? (
|
||||||
|
<FaChevronRight className="h-2.5 w-2.5" />
|
||||||
) : (
|
) : (
|
||||||
<FaChevronDown className="w-2.5 h-2.5" />
|
<FaChevronDown className="h-2.5 w-2.5" />
|
||||||
)}
|
)
|
||||||
</button>
|
}
|
||||||
|
className="ml-1 !h-5 !w-5 flex-shrink-0 !px-0 text-white opacity-70 transition-opacity hover:opacity-100"
|
||||||
|
></Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Users section */}
|
{/* Users section */}
|
||||||
{showUsers && (
|
{showUsers && (
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
|
|
@ -531,7 +562,9 @@ function OrgChartNode({
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
user={user}
|
user={user}
|
||||||
tenantId={tenantId}
|
tenantId={tenantId}
|
||||||
jobPosition={node.jobPositionName || (mode === 'jobPosition' ? node.name : undefined)}
|
jobPosition={
|
||||||
|
node.jobPositionName || (mode === 'jobPosition' ? node.name : undefined)
|
||||||
|
}
|
||||||
department={mode === 'jobPosition' ? node.departmentName : node.name}
|
department={mode === 'jobPosition' ? node.departmentName : node.name}
|
||||||
/>
|
/>
|
||||||
<span data-user-name="" className="text-xs text-slate-600 truncate">
|
<span data-user-name="" className="text-xs text-slate-600 truncate">
|
||||||
|
|
@ -848,28 +881,38 @@ const OrgChart = () => {
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setMode('department')}
|
onClick={() => setMode('department')}
|
||||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaBuilding className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
|
aria-pressed={mode === 'department'}
|
||||||
|
className={`!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-md !px-2 !py-1.5 text-xs font-medium transition-colors sm:text-sm ${
|
||||||
mode === 'department'
|
mode === 'department'
|
||||||
? 'bg-blue-600 text-white shadow-sm'
|
? '!bg-blue-600 !text-white shadow-sm ring-1 ring-blue-300/70 dark:ring-blue-400/70'
|
||||||
: 'text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white'
|
: '!bg-transparent text-slate-600 hover:!bg-white hover:text-slate-800 dark:text-gray-300 dark:hover:!bg-gray-700 dark:hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaBuilding className="w-3.5 h-3.5 flex-shrink-0" />
|
<span className="hidden sm:inline">
|
||||||
<span className="hidden sm:inline">{translate('::App.Hr.Department') || 'Departman'}</span>
|
{translate('::App.Hr.Department') || 'Departman'}
|
||||||
</button>
|
</span>
|
||||||
<button
|
</Button>
|
||||||
|
<Button
|
||||||
onClick={() => setMode('jobPosition')}
|
onClick={() => setMode('jobPosition')}
|
||||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaBriefcase className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
|
aria-pressed={mode === 'jobPosition'}
|
||||||
|
className={`!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-md !px-2 !py-1.5 text-xs font-medium transition-colors sm:text-sm ${
|
||||||
mode === 'jobPosition'
|
mode === 'jobPosition'
|
||||||
? 'bg-purple-600 text-white shadow-sm'
|
? '!bg-purple-600 !text-white shadow-sm ring-1 ring-purple-300/70 dark:ring-purple-400/70'
|
||||||
: 'text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white'
|
: '!bg-transparent text-slate-600 hover:!bg-white hover:text-slate-800 dark:text-gray-300 dark:hover:!bg-gray-700 dark:hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaBriefcase className="w-3.5 h-3.5 flex-shrink-0" />
|
<span className="hidden sm:inline">
|
||||||
<span className="hidden sm:inline">{translate('::App.Hr.JobPosition') || 'Pozisyon'}</span>
|
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
|
||||||
</button>
|
</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
||||||
|
|
@ -880,46 +923,53 @@ const OrgChart = () => {
|
||||||
onChange={(e) => setShowUsers(e.target.checked)}
|
onChange={(e) => setShowUsers(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-slate-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
className="h-4 w-4 rounded border-slate-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<span className="hidden sm:inline">{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}</span>
|
<span className="hidden sm:inline">
|
||||||
|
{translate('::App.Definitions.OrgChart.ShowUsers') || 'Kullanıcılar'}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
||||||
<button
|
<Button
|
||||||
onClick={handleZoomOut}
|
onClick={handleZoomOut}
|
||||||
className="p-1.5 sm:p-2 rounded-md text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white hover:bg-white dark:hover:bg-gray-700"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaSearchMinus className="h-3.5 w-3.5" />}
|
||||||
|
className="!h-7 !w-7 !rounded-md !px-0 text-slate-600 hover:!bg-white hover:text-slate-800 dark:text-gray-300 dark:hover:!bg-gray-700 dark:hover:text-white sm:!h-8 sm:!w-8"
|
||||||
title="Zoom Out"
|
title="Zoom Out"
|
||||||
>
|
/>
|
||||||
<FaSearchMinus className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<span className="text-xs font-medium text-slate-600 dark:text-gray-300 px-1 min-w-[36px] sm:min-w-[46px] text-center">
|
<span className="text-xs font-medium text-slate-600 dark:text-gray-300 px-1 min-w-[36px] sm:min-w-[46px] text-center">
|
||||||
{Math.round(zoom * 100)}%
|
{Math.round(zoom * 100)}%
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
onClick={handleZoomIn}
|
onClick={handleZoomIn}
|
||||||
className="p-1.5 sm:p-2 rounded-md text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white hover:bg-white dark:hover:bg-gray-700"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaSearchPlus className="h-3.5 w-3.5" />}
|
||||||
|
className="!h-7 !w-7 !rounded-md !px-0 text-slate-600 hover:!bg-white hover:text-slate-800 dark:text-gray-300 dark:hover:!bg-gray-700 dark:hover:text-white sm:!h-8 sm:!w-8"
|
||||||
title="Zoom In"
|
title="Zoom In"
|
||||||
>
|
/>
|
||||||
<FaSearchPlus className="w-3.5 h-3.5" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleZoomReset}
|
onClick={handleZoomReset}
|
||||||
className="p-1.5 sm:p-2 rounded-md text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white hover:bg-white dark:hover:bg-gray-700"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaUndo className="h-3.5 w-3.5" />}
|
||||||
|
className="!h-7 !w-7 !rounded-md !px-0 text-slate-600 hover:!bg-white hover:text-slate-800 dark:text-gray-300 dark:hover:!bg-gray-700 dark:hover:text-white sm:!h-8 sm:!w-8"
|
||||||
title="Reset Zoom"
|
title="Reset Zoom"
|
||||||
>
|
/>
|
||||||
<FaUndo className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={handleExportJpg}
|
onClick={handleExportJpg}
|
||||||
disabled={exporting || loading}
|
disabled={exporting || loading}
|
||||||
className="flex items-center gap-1.5 p-1.5 sm:p-2 rounded-lg text-xs sm:text-sm font-medium bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-gray-300 hover:bg-slate-200 dark:hover:bg-gray-700 disabled:opacity-50 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaFileImage className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-1.5 !rounded-lg bg-slate-100 !p-1.5 text-xs font-medium text-slate-600 transition-colors hover:!bg-slate-200 disabled:opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700 sm:!p-2 sm:text-sm"
|
||||||
title="JPG olarak indir"
|
title="JPG olarak indir"
|
||||||
>
|
>
|
||||||
<FaFileImage className="w-3.5 h-3.5 flex-shrink-0" />
|
|
||||||
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
|
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,14 @@ function TablePickerModal({
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{step === 'columns' && (
|
{step === 'columns' && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep('table')}
|
onClick={() => setStep('table')}
|
||||||
className="text-gray-400 hover:text-indigo-500 transition-colors"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaArrowLeft className="text-xs" />
|
icon={<FaArrowLeft className="text-xs" />}
|
||||||
</button>
|
className="!h-6 !w-6 !px-0 text-gray-400 transition-colors hover:!bg-transparent hover:text-indigo-500"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||||
{step === 'table'
|
{step === 'table'
|
||||||
|
|
@ -82,13 +83,14 @@ function TablePickerModal({
|
||||||
: (pickerTable?.tableName ?? '')}
|
: (pickerTable?.tableName ?? '')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes />
|
icon={<FaTimes />}
|
||||||
</button>
|
className="!h-7 !w-7 !px-0 text-gray-400 hover:!bg-transparent hover:text-gray-600 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 1: Table list */}
|
{/* Step 1: Table list */}
|
||||||
|
|
@ -112,7 +114,7 @@ function TablePickerModal({
|
||||||
dbObjects.tables
|
dbObjects.tables
|
||||||
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
|
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
|
||||||
.map((t) => (
|
.map((t) => (
|
||||||
<button
|
<Button
|
||||||
key={t.fullName}
|
key={t.fullName}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|
@ -135,11 +137,13 @@ function TablePickerModal({
|
||||||
setIsLoadingColumns(false)
|
setIsLoadingColumns(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full text-left text-xs px-3 py-2 rounded hover:bg-indigo-50 dark:hover:bg-indigo-900/30 text-gray-700 dark:text-gray-200 font-mono transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded !px-3 !py-2 text-left font-mono text-xs text-gray-700 transition-colors hover:!bg-indigo-50 dark:text-gray-200 dark:hover:!bg-indigo-900/30"
|
||||||
>
|
>
|
||||||
<span className="text-gray-400 mr-1">{t.schemaName}.</span>
|
<span className="text-gray-400 mr-1">{t.schemaName}.</span>
|
||||||
{t.tableName}
|
{t.tableName}
|
||||||
</button>
|
</Button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,7 +203,7 @@ function TablePickerModal({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!keyCol || !nameCol}
|
disabled={!keyCol || !nameCol}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -212,10 +216,13 @@ function TablePickerModal({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
className="mt-1 w-full py-1.5 text-xs font-semibold rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
block
|
||||||
|
variant="solid"
|
||||||
|
color="indigo-600"
|
||||||
|
className="mt-1 !h-auto !rounded !py-1.5 text-xs font-semibold text-white transition-colors hover:!bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Tamam
|
Tamam
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -310,14 +317,16 @@ function FormFieldTabLookup({
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')}
|
label={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')}
|
||||||
extra={
|
extra={
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openTablePicker}
|
onClick={openTablePicker}
|
||||||
className="ml-2 flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-800/40 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaPlus className="text-[8px]" />}
|
||||||
|
className="ml-2 !h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40"
|
||||||
>
|
>
|
||||||
<FaPlus className="text-[8px]" />
|
|
||||||
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
|
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
|
||||||
</button>
|
</Button>
|
||||||
}
|
}
|
||||||
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
|
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
|
||||||
errorMessage={errors.lookupDto?.lookupQuery}
|
errorMessage={errors.lookupDto?.lookupQuery}
|
||||||
|
|
|
||||||
|
|
@ -182,52 +182,60 @@ function TreeNode({
|
||||||
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onSaveEdit(node)
|
onSaveEdit(node)
|
||||||
}}
|
}}
|
||||||
className="p-1 text-green-500 hover:text-green-600 disabled:opacity-40"
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaCheck className="text-xs" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaCheck className="text-xs" />}
|
||||||
<button
|
className="!h-6 !w-6 !px-0 text-green-500 hover:!bg-transparent hover:text-green-600 disabled:opacity-40"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onCancelEdit()
|
onCancelEdit()
|
||||||
}}
|
}}
|
||||||
className="p-1 text-gray-400 hover:text-gray-600"
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaTimes className="text-xs" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTimes className="text-xs" />}
|
||||||
|
className="!h-6 !w-6 !px-0 text-gray-400 hover:!bg-transparent hover:text-gray-600"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<span className="shrink-0 flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title={translate('::ListForms.Wizard.Step1.Rename')}
|
title={translate('::ListForms.Wizard.Step1.Rename')}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onStartEdit(node.code, node.displayName)
|
onStartEdit(node.code, node.displayName)
|
||||||
}}
|
}}
|
||||||
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaEdit className="text-xs" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaEdit className="text-xs" />}
|
||||||
<button
|
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title={translate('::ListForms.Wizard.Step1.Delete')}
|
title={translate('::ListForms.Wizard.Step1.Delete')}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onDelete(node)
|
onDelete(node)
|
||||||
}}
|
}}
|
||||||
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaTrash className="text-xs" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTrash className="text-xs" />}
|
||||||
|
className={`!h-6 !w-6 !px-0 hover:!bg-transparent ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -343,7 +351,12 @@ function MenuTreeInline({
|
||||||
|
|
||||||
const handleDelete = async (node: MenuTreeNode & { id?: string }) => {
|
const handleDelete = async (node: MenuTreeNode & { id?: string }) => {
|
||||||
if (!node.id) return
|
if (!node.id) return
|
||||||
if (!window.confirm(`"${node.displayName}" ${translate('::ListForms.Wizard.Step1.DeleteMenuConfirm') || 'menüsünü silmek istediğinize emin misiniz?'}`)) return
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
`"${node.displayName}" ${translate('::ListForms.Wizard.Step1.DeleteMenuConfirm') || 'menüsünü silmek istediğinize emin misiniz?'}`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await menuService.delete(node.id)
|
await menuService.delete(node.id)
|
||||||
|
|
@ -383,7 +396,9 @@ function MenuTreeInline({
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||||
) : enrichedNodes.length === 0 ? (
|
) : enrichedNodes.length === 0 ? (
|
||||||
<div className="px-4 py-3 text-sm text-gray-400">{translate('::ListForms.Wizard.Step1.NoMenusAvailable') || 'No menus available'}</div>
|
<div className="px-4 py-3 text-sm text-gray-400">
|
||||||
|
{translate('::ListForms.Wizard.Step1.NoMenusAvailable') || 'No menus available'}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
enrichedNodes.map((node) => (
|
enrichedNodes.map((node) => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
|
|
@ -476,7 +491,8 @@ const WizardStep1 = ({
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
extra={
|
extra={
|
||||||
<span className="text-xs ml-2 text-gray-400">
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
{translate('::ListForms.Wizard.Step1.WizardNameHint') || 'Used to generate ListForm Code and Menu Code'}
|
{translate('::ListForms.Wizard.Step1.WizardNameHint') ||
|
||||||
|
'Used to generate ListForm Code and Menu Code'}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -501,7 +517,7 @@ const WizardStep1 = ({
|
||||||
errorMessage={undefined}
|
errorMessage={undefined}
|
||||||
extra={
|
extra={
|
||||||
<div className="flex items-center gap-2 ml-3">
|
<div className="flex items-center gap-2 ml-3">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMenuDialogParentCode(
|
setMenuDialogParentCode(
|
||||||
|
|
@ -511,22 +527,31 @@ const WizardStep1 = ({
|
||||||
)
|
)
|
||||||
setMenuDialogOpen(true)
|
setMenuDialogOpen(true)
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600"
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
color="green-500"
|
||||||
|
icon={<FaPlus className="text-xs" />}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-white whitespace-nowrap hover:!bg-green-600"
|
||||||
>
|
>
|
||||||
<FaPlus className="text-xs" /> {translate('::ListForms.Wizard.Add') || 'Ekle'}
|
<span className="whitespace-nowrap">
|
||||||
</button>
|
{translate('::ListForms.Wizard.Add') || 'Ekle'}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
{values.menuParentCode && (
|
{values.menuParentCode && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onClearMenuParent()
|
onClearMenuParent()
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
icon={<FaTimes className="text-xs" />}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-1 !rounded !px-2 !py-0.5 text-xs text-gray-500 whitespace-nowrap hover:border-red-400 hover:text-red-500 dark:border-gray-600"
|
||||||
>
|
>
|
||||||
<FaTimes className="text-xs" /> {translate('::ListForms.Wizard.ClearSelection') || 'Seçimi Kaldır'}
|
{translate('::ListForms.Wizard.ClearSelection') || 'Seçimi Kaldır'}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -541,7 +566,11 @@ const WizardStep1 = ({
|
||||||
isLoading={isLoadingMenu}
|
isLoading={isLoadingMenu}
|
||||||
invalid={false}
|
invalid={false}
|
||||||
onReload={onReloadMenu}
|
onReload={onReloadMenu}
|
||||||
initialExpanded={values.menuParentCode ? getAncestorCodes(rawMenuItems, values.menuParentCode) : undefined}
|
initialExpanded={
|
||||||
|
values.menuParentCode
|
||||||
|
? getAncestorCodes(rawMenuItems, values.menuParentCode)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
@ -565,7 +594,11 @@ const WizardStep1 = ({
|
||||||
invalid={!!(errors.menuCode && touched.menuCode)}
|
invalid={!!(errors.menuCode && touched.menuCode)}
|
||||||
errorMessage={errors.menuCode}
|
errorMessage={errors.menuCode}
|
||||||
asterisk={true}
|
asterisk={true}
|
||||||
extra={<span className="text-xs ml-2 text-gray-400">{translate('::ListForms.Wizard.Step1.MenuCodeHint') || 'Auto-derived, editable'}</span>}
|
extra={
|
||||||
|
<span className="text-xs ml-2 text-gray-400">
|
||||||
|
{translate('::ListForms.Wizard.Step1.MenuCodeHint') || 'Auto-derived, editable'}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -606,7 +639,9 @@ const WizardStep1 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextMenuEn"
|
name="languageTextMenuEn"
|
||||||
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'}
|
placeholder={
|
||||||
|
translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'
|
||||||
|
}
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -622,7 +657,9 @@ const WizardStep1 = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="languageTextMenuTr"
|
name="languageTextMenuTr"
|
||||||
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'}
|
placeholder={
|
||||||
|
translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'
|
||||||
|
}
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
|
||||||
|
|
@ -238,26 +238,28 @@ function SortableItem({
|
||||||
>
|
>
|
||||||
{/* Top row: drag handle + field name + remove */}
|
{/* Top row: drag handle + field name + remove */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
className="cursor-grab text-gray-300 hover:text-gray-500 shrink-0"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaGripVertical className="text-sm" />}
|
||||||
|
className="!h-6 !w-6 !px-0 cursor-grab text-gray-300 hover:!bg-transparent hover:text-gray-500 shrink-0"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
/>
|
||||||
<FaGripVertical className="text-sm" />
|
|
||||||
</button>
|
|
||||||
<span className="flex-1 text-xs font-semibold text-indigo-600 dark:text-indigo-400 truncate">
|
<span className="flex-1 text-xs font-semibold text-indigo-600 dark:text-indigo-400 truncate">
|
||||||
{item.dataField}
|
{item.dataField}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
className="opacity-0 group-hover/item:opacity-100 p-0.5 text-gray-300 hover:text-red-500 shrink-0 transition-opacity"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTimes className="text-[10px]" />}
|
||||||
|
className="!h-5 !w-5 !px-0 opacity-0 group-hover/item:opacity-100 text-gray-300 hover:!bg-transparent hover:text-red-500 shrink-0 transition-opacity"
|
||||||
title={translate('::ListForms.Wizard.Step3.Remove')}
|
title={translate('::ListForms.Wizard.Step3.Remove')}
|
||||||
>
|
/>
|
||||||
<FaTimes className="text-[10px]" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Language Key + Turkish Caption + English Caption */}
|
{/* Language Key + Turkish Caption + English Caption */}
|
||||||
|
|
@ -362,14 +364,16 @@ function SortableItem({
|
||||||
<span className="text-[10px] text-gray-400 font-medium flex-1">
|
<span className="text-[10px] text-gray-400 font-medium flex-1">
|
||||||
{translate('::ListForms.Wizard.Step3.LookupLookupQuery')}
|
{translate('::ListForms.Wizard.Step3.LookupLookupQuery')}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsTablePickerOpen(true)}
|
onClick={() => setIsTablePickerOpen(true)}
|
||||||
className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-800/40 transition-colors shrink-0"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaPlus className="text-[8px]" />}
|
||||||
|
className="!h-auto !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-1.5 !py-0.5 text-[10px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-800/40 shrink-0"
|
||||||
>
|
>
|
||||||
<FaPlus className="text-[8px]" />
|
|
||||||
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
|
{translate('::ListForms.Wizard.Step3.GenerateFromTable') || 'Tablodan Oluştur'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
|
|
@ -393,13 +397,14 @@ function SortableItem({
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{pickerStep === 'columns' && (
|
{pickerStep === 'columns' && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPickerStep('table')}
|
onClick={() => setPickerStep('table')}
|
||||||
className="text-gray-400 hover:text-indigo-500 transition-colors"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaArrowLeft className="text-xs" />
|
icon={<FaArrowLeft className="text-xs" />}
|
||||||
</button>
|
className="!h-6 !w-6 !px-0 text-gray-400 transition-colors hover:!bg-transparent hover:text-indigo-500"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||||
{pickerStep === 'table'
|
{pickerStep === 'table'
|
||||||
|
|
@ -407,13 +412,14 @@ function SortableItem({
|
||||||
: (pickerTable?.tableName ?? '')}
|
: (pickerTable?.tableName ?? '')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsTablePickerOpen(false)}
|
onClick={() => setIsTablePickerOpen(false)}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes />
|
icon={<FaTimes />}
|
||||||
</button>
|
className="!h-7 !w-7 !px-0 text-gray-400 hover:!bg-transparent hover:text-gray-600 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 1: Table list */}
|
{/* Step 1: Table list */}
|
||||||
|
|
@ -438,7 +444,7 @@ function SortableItem({
|
||||||
dbObjects.tables
|
dbObjects.tables
|
||||||
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
|
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
|
||||||
.map((t) => (
|
.map((t) => (
|
||||||
<button
|
<Button
|
||||||
key={t.fullName}
|
key={t.fullName}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|
@ -461,11 +467,13 @@ function SortableItem({
|
||||||
setIsLoadingPickerColumns(false)
|
setIsLoadingPickerColumns(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full text-left text-xs px-3 py-2 rounded hover:bg-indigo-50 dark:hover:bg-indigo-900/30 text-gray-700 dark:text-gray-200 font-mono transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded !px-3 !py-2 text-left font-mono text-xs text-gray-700 transition-colors hover:!bg-indigo-50 dark:text-gray-200 dark:hover:!bg-indigo-900/30"
|
||||||
>
|
>
|
||||||
<span className="text-gray-400 mr-1">{t.schemaName}.</span>
|
<span className="text-gray-400 mr-1">{t.schemaName}.</span>
|
||||||
{t.tableName}
|
{t.tableName}
|
||||||
</button>
|
</Button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -525,7 +533,7 @@ function SortableItem({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!pickerKeyCol || !pickerNameCol}
|
disabled={!pickerKeyCol || !pickerNameCol}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -538,10 +546,13 @@ function SortableItem({
|
||||||
onLookupQueryChange(q)
|
onLookupQueryChange(q)
|
||||||
setIsTablePickerOpen(false)
|
setIsTablePickerOpen(false)
|
||||||
}}
|
}}
|
||||||
className="mt-1 w-full py-1.5 text-xs font-semibold rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
block
|
||||||
|
variant="solid"
|
||||||
|
color="indigo-600"
|
||||||
|
className="mt-1 !h-auto !rounded !py-1.5 text-xs font-semibold text-white transition-colors hover:!bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Tamam
|
Tamam
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -679,42 +690,48 @@ function GroupCard({
|
||||||
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}
|
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}
|
||||||
</span>
|
</span>
|
||||||
{[1, 2, 3].map((n) => (
|
{[1, 2, 3].map((n) => (
|
||||||
<button
|
<Button
|
||||||
key={n}
|
key={n}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onColCountChange(n)}
|
onClick={() => onColCountChange(n)}
|
||||||
className={`w-6 h-6 text-xs rounded font-medium transition-colors ${
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!h-6 !w-6 !rounded !px-0 text-xs font-medium transition-colors ${
|
||||||
group.colCount === n
|
group.colCount === n
|
||||||
? 'bg-indigo-500 text-white'
|
? '!bg-indigo-500 !text-white'
|
||||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/40'
|
: 'bg-gray-200 text-gray-600 hover:!bg-indigo-100 dark:bg-gray-700 dark:text-gray-300 dark:hover:!bg-indigo-900/40'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{hasAvailable && (
|
{hasAvailable && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAddAll}
|
onClick={onAddAll}
|
||||||
className="flex items-center gap-1 h-6 px-2 text-[11px] font-medium rounded border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 transition-colors shrink-0"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaArrowRight className="text-[9px]" />}
|
||||||
|
className="!h-6 !items-center gap-1 !rounded border border-indigo-200 bg-indigo-50 !px-2 text-[11px] font-medium text-indigo-600 transition-colors hover:!bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 dark:hover:!bg-indigo-900/40 shrink-0"
|
||||||
title={
|
title={
|
||||||
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
|
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
|
||||||
'Tüm mevcut sütunları bu gruba ekle'
|
'Tüm mevcut sütunları bu gruba ekle'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaArrowRight className="text-[9px]" />
|
|
||||||
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
|
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDeleteGroup}
|
onClick={onDeleteGroup}
|
||||||
className="p-1.5 text-gray-300 hover:text-red-500 rounded transition-colors"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTrash className="text-xs" />}
|
||||||
|
className="!h-7 !w-7 !px-0 text-gray-300 transition-colors hover:!bg-transparent hover:text-red-500"
|
||||||
title={translate('::ListForms.Wizard.Step3.DeleteGroup') || 'Delete group'}
|
title={translate('::ListForms.Wizard.Step3.DeleteGroup') || 'Delete group'}
|
||||||
>
|
/>
|
||||||
<FaTrash className="text-xs" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items drop zone */}
|
{/* Items drop zone */}
|
||||||
|
|
@ -1083,14 +1100,16 @@ const WizardStep3 = ({
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add Group */}
|
{/* Add Group */}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addGroup}
|
onClick={addGroup}
|
||||||
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700 text-sm text-gray-400 dark:text-gray-500 hover:border-indigo-400 hover:text-indigo-500 dark:hover:border-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaPlus className="text-xs" />}
|
||||||
|
className="w-full !h-auto !items-center !justify-center gap-2 !rounded-xl border-2 border-dashed border-gray-200 !py-2.5 text-sm text-gray-400 transition-colors hover:!bg-transparent hover:border-indigo-400 hover:text-indigo-500 dark:border-gray-700 dark:text-gray-500 dark:hover:border-indigo-600 dark:hover:text-indigo-400"
|
||||||
>
|
>
|
||||||
<FaPlus className="text-xs" />
|
|
||||||
{translate('::ListForms.Wizard.Step3.AddGroup') || 'Grup Ekle'}
|
{translate('::ListForms.Wizard.Step3.AddGroup') || 'Grup Ekle'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,12 @@ function Section({ title, badge, children, defaultOpen = true }: SectionProps) {
|
||||||
const [open, setOpen] = useState(defaultOpen)
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between px-4 py-2.5 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !items-center !justify-between !rounded-none bg-gray-50 !px-4 !py-2.5 transition-colors hover:!bg-gray-100 dark:bg-gray-800 dark:hover:!bg-gray-750"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{open ? (
|
{open ? (
|
||||||
|
|
@ -134,7 +136,7 @@ function Section({ title, badge, children, defaultOpen = true }: SectionProps) {
|
||||||
{badge}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
{open && <div className="px-4 py-3 bg-white dark:bg-gray-900">{children}</div>}
|
{open && <div className="px-4 py-3 bg-white dark:bg-gray-900">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -533,7 +535,10 @@ const WizardStep7 = ({
|
||||||
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
|
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
|
||||||
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
|
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
|
||||||
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
|
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
|
||||||
<Row label="Is Filter User Name?" value={workflow.isFilterUserName === true ? 'Yes' : 'No'} />
|
<Row
|
||||||
|
label="Is Filter User Name?"
|
||||||
|
value={workflow.isFilterUserName === true ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{workflowItems.length > 0 && (
|
{workflowItems.length > 0 && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import type { KeyboardEvent, MouseEvent, RefObject } from 'react'
|
import type { KeyboardEvent, MouseEvent, RefObject } from 'react'
|
||||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
type PendingLink = {
|
type PendingLink = {
|
||||||
sourceId: string
|
sourceId: string
|
||||||
|
|
@ -289,16 +290,18 @@ function FlowNode({
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute z-40 grid h-32 w-44 touch-none content-start justify-items-start gap-1 rounded-lg border-2 border-[#667085] bg-white p-2.5 text-left text-slate-700 shadow-lg dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200',
|
'absolute z-40 grid !h-32 !w-44 touch-none content-start justify-items-start gap-1 rounded-lg border-2 border-[#667085] !bg-white !p-2.5 text-left text-slate-700 shadow-lg hover:!bg-white active:!bg-white dark:border-gray-600 dark:!bg-gray-900 dark:text-gray-200 dark:hover:!bg-gray-900 dark:active:!bg-gray-900',
|
||||||
{
|
{
|
||||||
'border-blue-600 outline outline-[3px] outline-blue-600/20': selected,
|
'border-blue-600 outline outline-[3px] outline-blue-600/20': selected,
|
||||||
'border-green-600 bg-green-50 shadow-[0_0_0_4px_rgba(22,163,74,0.18),0_10px_24px_rgba(22,101,52,0.14)] dark:bg-green-900/20':
|
'border-green-600 !bg-green-50 shadow-[0_0_0_4px_rgba(22,163,74,0.18),0_10px_24px_rgba(22,101,52,0.14)] hover:!bg-green-50 active:!bg-green-50 dark:!bg-green-900/20 dark:hover:!bg-green-900/20 dark:active:!bg-green-900/20':
|
||||||
active,
|
active,
|
||||||
'h-[158px] border-amber-600': item.kind === 'Compare',
|
'!h-[158px] border-amber-600': item.kind === 'Compare',
|
||||||
'border-violet-600': item.kind === 'Approval',
|
'border-violet-600': item.kind === 'Approval',
|
||||||
'border-green-600': item.kind === 'Inform',
|
'border-green-600': item.kind === 'Inform',
|
||||||
'border-red-600': item.kind === 'End',
|
'border-red-600': item.kind === 'End',
|
||||||
|
|
@ -425,7 +428,7 @@ function FlowNode({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { FiMaximize2, FiRefreshCw, FiZoomIn, FiZoomOut } from 'react-icons/fi'
|
||||||
import { kindIcon, kindOptions } from '@/utils/workflow/workflowConstants'
|
import { kindIcon, kindOptions } from '@/utils/workflow/workflowConstants'
|
||||||
import { WorkflowCriteria } from './WorkflowCriteria'
|
import { WorkflowCriteria } from './WorkflowCriteria'
|
||||||
import { WorkflowCanvas } from './WorkflowCanvas'
|
import { WorkflowCanvas } from './WorkflowCanvas'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
import type { FormEvent, RefObject } from 'react'
|
import type { FormEvent, RefObject } from 'react'
|
||||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||||
import { SelectBoxOption } from '@/types/shared'
|
import { SelectBoxOption } from '@/types/shared'
|
||||||
|
|
@ -43,12 +44,12 @@ type WorkflowDesignerProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const designerButtonClass =
|
const designerButtonClass =
|
||||||
'inline-flex min-h-9 items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50'
|
'!inline-flex !h-auto min-h-9 items-center justify-center gap-1.5 rounded-md border !px-3 py-1.5 text-sm font-medium leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50'
|
||||||
|
|
||||||
const designerIconButtonClass =
|
const designerIconButtonClass =
|
||||||
'inline-flex min-h-9 w-9 items-center justify-center rounded-md border p-0 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50'
|
'!inline-flex !h-auto min-h-9 !w-9 items-center justify-center rounded-md border !p-0 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50'
|
||||||
|
|
||||||
const designerTabClass = 'min-h-8 rounded-md border px-3 py-1.5 transition-colors'
|
const designerTabClass = '!h-auto min-h-8 rounded-md border !px-3 py-1.5 transition-colors'
|
||||||
|
|
||||||
export function WorkflowDesigner({
|
export function WorkflowDesigner({
|
||||||
busy,
|
busy,
|
||||||
|
|
@ -174,11 +175,13 @@ function DesignerToolbar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap justify-end gap-2">
|
<div className="flex flex-wrap justify-end gap-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerButtonClass,
|
designerButtonClass,
|
||||||
'border-gray-300 bg-white text-slate-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700',
|
'border-gray-300 !bg-white text-slate-700 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-200 dark:hover:!bg-gray-700',
|
||||||
)}
|
)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
title="Demo akışı yükle"
|
title="Demo akışı yükle"
|
||||||
|
|
@ -186,12 +189,14 @@ function DesignerToolbar({
|
||||||
>
|
>
|
||||||
<FiRefreshCw />
|
<FiRefreshCw />
|
||||||
Demo
|
Demo
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerButtonClass,
|
designerButtonClass,
|
||||||
'border-blue-600 bg-white text-blue-600 hover:bg-blue-50 dark:border-blue-500 dark:bg-gray-800 dark:text-blue-300 dark:hover:bg-blue-900/30',
|
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
||||||
)}
|
)}
|
||||||
disabled={busy || currentCriteria.length === 0}
|
disabled={busy || currentCriteria.length === 0}
|
||||||
title="Düğümleri okunabilir şekilde yerleştir"
|
title="Düğümleri okunabilir şekilde yerleştir"
|
||||||
|
|
@ -199,48 +204,54 @@ function DesignerToolbar({
|
||||||
>
|
>
|
||||||
<FiMaximize2 />
|
<FiMaximize2 />
|
||||||
Fit
|
Fit
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerIconButtonClass,
|
designerIconButtonClass,
|
||||||
'border-blue-600 bg-white text-blue-600 hover:bg-blue-50 dark:border-blue-500 dark:bg-gray-800 dark:text-blue-300 dark:hover:bg-blue-900/30',
|
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
||||||
)}
|
)}
|
||||||
title="Yakınlaştır"
|
title="Yakınlaştır"
|
||||||
onClick={onZoomIn}
|
onClick={onZoomIn}
|
||||||
>
|
>
|
||||||
<FiZoomIn />
|
<FiZoomIn />
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerIconButtonClass,
|
designerIconButtonClass,
|
||||||
'border-blue-600 bg-white text-blue-600 hover:bg-blue-50 dark:border-blue-500 dark:bg-gray-800 dark:text-blue-300 dark:hover:bg-blue-900/30',
|
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
||||||
)}
|
)}
|
||||||
title="Uzaklaştır"
|
title="Uzaklaştır"
|
||||||
onClick={onZoomOut}
|
onClick={onZoomOut}
|
||||||
>
|
>
|
||||||
<FiZoomOut />
|
<FiZoomOut />
|
||||||
</button>
|
</Button>
|
||||||
<span className="inline-flex min-w-12 items-center justify-center text-[13px] font-bold text-slate-500 dark:text-gray-400">
|
<span className="inline-flex min-w-12 items-center justify-center text-[13px] font-bold text-slate-500 dark:text-gray-400">
|
||||||
{Math.round(zoom * 100)}%
|
{Math.round(zoom * 100)}%
|
||||||
</span>
|
</span>
|
||||||
{kindOptions.map((option) => {
|
{kindOptions.map((option) => {
|
||||||
const Icon = kindIcon[option.value]
|
const Icon = kindIcon[option.value]
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerButtonClass,
|
designerButtonClass,
|
||||||
'border-blue-600 bg-white text-blue-600 hover:bg-blue-50 dark:border-blue-500 dark:bg-gray-800 dark:text-blue-300 dark:hover:bg-blue-900/30',
|
'border-blue-600 !bg-white text-blue-600 hover:!bg-blue-50 dark:border-blue-500 dark:!bg-gray-800 dark:text-blue-300 dark:hover:!bg-blue-900/30',
|
||||||
)}
|
)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => onAddCriteria(option.value)}
|
onClick={() => onAddCriteria(option.value)}
|
||||||
>
|
>
|
||||||
<Icon />
|
<Icon />
|
||||||
{translate(`::ListForms.ListFormEdit.Workflow.Criteria${option.value}`)}
|
{translate(`::ListForms.ListFormEdit.Workflow.Criteria${option.value}`)}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -258,32 +269,36 @@ function DesignerTabs({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
role="tab"
|
role="tab"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerTabClass,
|
designerTabClass,
|
||||||
activeTab === 'flow'
|
activeTab === 'flow'
|
||||||
? 'border-blue-700 bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:bg-blue-600'
|
? 'border-blue-700 !bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600'
|
||||||
: 'border-gray-200 bg-white text-slate-600 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
|
: 'border-gray-200 !bg-white text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700',
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange('flow')}
|
onClick={() => onChange('flow')}
|
||||||
>
|
>
|
||||||
{translate('::ListForms.ListFormEdit.TabWorkflow')}
|
{translate('::ListForms.ListFormEdit.TabWorkflow')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
role="tab"
|
role="tab"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
designerTabClass,
|
designerTabClass,
|
||||||
activeTab === 'criteria'
|
activeTab === 'criteria'
|
||||||
? 'border-blue-700 bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:bg-blue-600'
|
? 'border-blue-700 !bg-blue-700 text-white shadow-sm dark:border-blue-500 dark:!bg-blue-600'
|
||||||
: 'border-gray-200 bg-white text-slate-600 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
|
: 'border-gray-200 !bg-white text-slate-600 hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700',
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange('criteria')}
|
onClick={() => onChange('criteria')}
|
||||||
>
|
>
|
||||||
{translate('::ListForms.ListFormEdit.Workflow.Criteria')}
|
{translate('::ListForms.ListFormEdit.Workflow.Criteria')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,19 +127,22 @@ function PermissionDialogContent({
|
||||||
<div key={permission.name} className={`ml-${permission.level * 4} group`}>
|
<div key={permission.name} className={`ml-${permission.level * 4} group`}>
|
||||||
<div className="flex items-center gap-2 px-2 py-0.5 rounded-md hover:bg-gray-50 transition-all dark:hover:bg-gray-700">
|
<div className="flex items-center gap-2 px-2 py-0.5 rounded-md hover:bg-gray-50 transition-all dark:hover:bg-gray-700">
|
||||||
{isParentPerm ? (
|
{isParentPerm ? (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
togglePermission(permKey)
|
togglePermission(permKey)
|
||||||
}}
|
}}
|
||||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition"
|
variant="default"
|
||||||
|
shape="none"
|
||||||
|
className="!h-5 !w-5 !rounded !border-transparent !bg-transparent !p-0 transition hover:!bg-transparent active:!bg-transparent dark:hover:!bg-transparent dark:active:!bg-transparent"
|
||||||
>
|
>
|
||||||
{openPermissions[permKey] || searchTerm ? (
|
{openPermissions[permKey] || searchTerm ? (
|
||||||
<FaChevronDown className="text-gray-500 text-xs" />
|
<FaChevronDown className="text-gray-500 text-xs" />
|
||||||
) : (
|
) : (
|
||||||
<FaChevronRight className="text-gray-500 text-xs" />
|
<FaChevronRight className="text-gray-500 text-xs" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-5" />
|
<div className="w-5" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -121,19 +121,22 @@ function UserPermissionDialogContent({
|
||||||
<div key={permission.name} className={`ml-${permission.level * 4} group`}>
|
<div key={permission.name} className={`ml-${permission.level * 4} group`}>
|
||||||
<div className="flex items-center gap-2 px-2 py-0.5 rounded-md hover:bg-gray-50 transition-all dark:hover:bg-gray-700">
|
<div className="flex items-center gap-2 px-2 py-0.5 rounded-md hover:bg-gray-50 transition-all dark:hover:bg-gray-700">
|
||||||
{isParentPerm ? (
|
{isParentPerm ? (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
togglePermission(permKey)
|
togglePermission(permKey)
|
||||||
}}
|
}}
|
||||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition"
|
variant="default"
|
||||||
|
shape="none"
|
||||||
|
className="!h-5 !w-5 !rounded !border-transparent !bg-transparent !p-0 transition hover:!bg-transparent active:!bg-transparent dark:hover:!bg-transparent dark:active:!bg-transparent"
|
||||||
>
|
>
|
||||||
{openPermissions[permKey] || searchTerm ? (
|
{openPermissions[permKey] || searchTerm ? (
|
||||||
<FaChevronDown className="text-gray-500 text-xs" />
|
<FaChevronDown className="text-gray-500 text-xs" />
|
||||||
) : (
|
) : (
|
||||||
<FaChevronRight className="text-gray-500 text-xs" />
|
<FaChevronRight className="text-gray-500 text-xs" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-5" />
|
<div className="w-5" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from '@/proxy/videoroom/models'
|
} from '@/proxy/videoroom/models'
|
||||||
import React, { useRef, useEffect } from 'react'
|
import React, { useRef, useEffect } from 'react'
|
||||||
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
user: { id: string; name: string; role: string }
|
user: { id: string; name: string; role: string }
|
||||||
|
|
@ -51,57 +52,72 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-3 sm:p-4 border-b border-gray-200 flex justify-between items-center">
|
<div className="p-3 sm:p-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
<h3 className="text-base sm:text-lg font-semibold">Sohbet</h3>
|
<h3 className="text-base sm:text-lg font-semibold">Sohbet</h3>
|
||||||
<button onClick={onClose}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!inline-flex !h-auto items-center justify-center !bg-transparent !p-1 hover:!bg-gray-100 active:!bg-gray-100"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
<FaTimes className="text-gray-500" size={16} />
|
<FaTimes className="text-gray-500" size={16} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mesaj Modu */}
|
{/* Mesaj Modu */}
|
||||||
<div className="p-3 border-b border-gray-200 bg-gray-50">
|
<div className="p-3 border-b border-gray-200 bg-gray-50">
|
||||||
<div className="flex space-x-2 mb-2">
|
<div className="flex space-x-2 mb-2">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMessageMode('public')
|
setMessageMode('public')
|
||||||
setSelectedRecipient(null)
|
setSelectedRecipient(null)
|
||||||
}}
|
}}
|
||||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
className={`!inline-flex !h-auto items-center gap-1 rounded-full !px-3 py-1 text-xs ${
|
||||||
messageMode === 'public'
|
messageMode === 'public'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 text-white'
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
: '!bg-gray-200 text-gray-700 hover:!bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaUsers size={12} />
|
<FaUsers size={12} />
|
||||||
<span>Herkese</span>
|
<span>Herkese</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{classSettings.allowPrivateMessages && (
|
{classSettings.allowPrivateMessages && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => setMessageMode('private')}
|
onClick={() => setMessageMode('private')}
|
||||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
className={`!inline-flex !h-auto items-center gap-1 rounded-full !px-3 py-1 text-xs ${
|
||||||
messageMode === 'private'
|
messageMode === 'private'
|
||||||
? 'bg-green-600 text-white'
|
? '!bg-green-600 text-white'
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
: '!bg-gray-200 text-gray-700 hover:!bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaUser size={12} />
|
<FaUser size={12} />
|
||||||
<span>Özel</span>
|
<span>Özel</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{user.role === 'teacher' && (
|
{user.role === 'teacher' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMessageMode('announcement')
|
setMessageMode('announcement')
|
||||||
setSelectedRecipient(null)
|
setSelectedRecipient(null)
|
||||||
}}
|
}}
|
||||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
className={`!inline-flex !h-auto items-center gap-1 rounded-full !px-3 py-1 text-xs ${
|
||||||
messageMode === 'announcement'
|
messageMode === 'announcement'
|
||||||
? 'bg-red-600 text-white'
|
? '!bg-red-600 text-white'
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
: '!bg-gray-200 text-gray-700 hover:!bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaBullhorn size={12} />
|
<FaBullhorn size={12} />
|
||||||
<span>Duyuru</span>
|
<span>Duyuru</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -180,13 +196,15 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
||||||
placeholder="Mesaj yaz..."
|
placeholder="Mesaj yaz..."
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
disabled={!newMessage.trim()}
|
disabled={!newMessage.trim()}
|
||||||
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-blue-600 !px-3 py-2 text-white hover:!bg-blue-700 disabled:!bg-gray-400"
|
||||||
>
|
>
|
||||||
<FaPaperPlane size={16} />
|
<FaPaperPlane size={16} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { VideoroomDocumentDto } from '@/proxy/videoroom/models';
|
import { VideoroomDocumentDto } from '@/proxy/videoroom/models';
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa'
|
import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface DocumentsPanelProps {
|
interface DocumentsPanelProps {
|
||||||
user: { role: string; name: string }
|
user: { role: string; name: string }
|
||||||
|
|
@ -47,9 +48,15 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Sınıf Dokümanları</h3>
|
<h3 className="text-lg font-semibold text-gray-900">Sınıf Dokümanları</h3>
|
||||||
<button onClick={onClose}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!inline-flex !h-auto items-center justify-center !bg-transparent !p-1 hover:!bg-gray-100 active:!bg-gray-100"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
<FaTimes className="text-gray-500" />
|
<FaTimes className="text-gray-500" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -71,12 +78,15 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||||
<FaFile size={32} className="mx-auto text-gray-400 mb-2" />
|
<FaFile size={32} className="mx-auto text-gray-400 mb-2" />
|
||||||
<p className="text-sm font-medium text-gray-700 mb-2">Doküman Yükle</p>
|
<p className="text-sm font-medium text-gray-700 mb-2">Doküman Yükle</p>
|
||||||
<p className="text-xs text-gray-500 mb-3">Dosyaları buraya sürükleyin veya seçin</p>
|
<p className="text-xs text-gray-500 mb-3">Dosyaları buraya sürükleyin veya seçin</p>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded !bg-blue-600 !px-3 py-1 text-sm text-white transition-colors hover:!bg-blue-700"
|
||||||
>
|
>
|
||||||
Dosya Seç
|
Dosya Seç
|
||||||
</button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
|
@ -114,13 +124,16 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 flex-shrink-0">
|
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onView(doc)}
|
onClick={() => onView(doc)}
|
||||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded !bg-transparent !p-1 text-blue-600 transition-colors hover:!bg-blue-50"
|
||||||
title="Görüntüle"
|
title="Görüntüle"
|
||||||
>
|
>
|
||||||
<FaEye size={12} />
|
<FaEye size={12} />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={doc.url}
|
href={doc.url}
|
||||||
|
|
@ -132,13 +145,16 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{user.role === 'teacher' && (
|
{user.role === 'teacher' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onDelete(doc.id)}
|
onClick={() => onDelete(doc.id)}
|
||||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded !bg-transparent !p-1 text-red-600 transition-colors hover:!bg-red-50"
|
||||||
title="Sil"
|
title="Sil"
|
||||||
>
|
>
|
||||||
<FaTrash size={12} />
|
<FaTrash size={12} />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { FaUserTimes, FaExclamationTriangle } from 'react-icons/fa'
|
import { FaUserTimes, FaExclamationTriangle } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface KickParticipantModalProps {
|
interface KickParticipantModalProps {
|
||||||
participant: { id: string; name: string } | null
|
participant: { id: string; name: string } | null
|
||||||
|
|
@ -61,19 +62,25 @@ export const KickParticipantModal: React.FC<KickParticipantModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end space-x-4">
|
<div className="flex items-center justify-end space-x-4">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded-lg border !border-gray-300 !bg-white !px-4 py-2 text-gray-700 transition-colors hover:!bg-gray-50"
|
||||||
>
|
>
|
||||||
İptal
|
İptal
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2"
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-red-600 !px-4 py-2 text-white transition-colors hover:!bg-red-700"
|
||||||
>
|
>
|
||||||
<FaUserTimes size={16} />
|
<FaUserTimes size={16} />
|
||||||
<span>Çıkar</span>
|
<span>Çıkar</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
import { VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa'
|
import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface LayoutPanelProps {
|
interface LayoutPanelProps {
|
||||||
layouts: VideoroomLayoutDto[]
|
layouts: VideoroomLayoutDto[]
|
||||||
|
|
@ -37,22 +38,31 @@ const LayoutPanel: React.FC<LayoutPanelProps> = ({
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Video Layout Seçin</h3>
|
<h3 className="text-lg font-semibold text-gray-900">Video Layout Seçin</h3>
|
||||||
<button onClick={onClose}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!inline-flex !h-auto items-center justify-center !bg-transparent !p-1 hover:!bg-gray-100 active:!bg-gray-100"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
<FaTimes className="text-gray-500" />
|
<FaTimes className="text-gray-500" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-2">
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{layouts.map((layout) => (
|
{layouts.map((layout) => (
|
||||||
<button
|
<Button
|
||||||
key={layout.id}
|
key={layout.id}
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onChangeLayout(layout)}
|
onClick={() => onChangeLayout(layout)}
|
||||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
className={`!block !h-auto !w-full rounded-lg border-2 !p-4 text-left transition-all ${
|
||||||
currentLayout.id === layout.id
|
currentLayout.id === layout.id
|
||||||
? 'border-blue-500 bg-blue-50'
|
? 'border-blue-500 !bg-blue-50'
|
||||||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50'
|
: 'border-gray-200 !bg-white hover:border-blue-300 hover:!bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
|
|
@ -101,7 +111,7 @@ const LayoutPanel: React.FC<LayoutPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
FaVideoSlash,
|
FaVideoSlash,
|
||||||
FaUserTimes,
|
FaUserTimes,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface ParticipantsPanelProps {
|
interface ParticipantsPanelProps {
|
||||||
user: { id: string; name: string; role: string }
|
user: { id: string; name: string; role: string }
|
||||||
|
|
@ -49,9 +50,15 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
Katılımcılar ({participants.length + 1})
|
Katılımcılar ({participants.length + 1})
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={onClose}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!inline-flex !h-auto items-center justify-center !bg-transparent !p-1 hover:!bg-gray-100 active:!bg-gray-100"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
<FaTimes className="text-gray-500" />
|
<FaTimes className="text-gray-500" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* El kaldıranlar göstergesi */}
|
{/* El kaldıranlar göstergesi */}
|
||||||
|
|
@ -69,30 +76,36 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => setActiveTab('participants')}
|
onClick={() => setActiveTab('participants')}
|
||||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
className={`!inline-flex !h-auto flex-1 items-center justify-center gap-1 rounded-md !px-3 py-2 text-sm font-medium transition-all ${
|
||||||
activeTab === 'participants'
|
activeTab === 'participants'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? '!bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaUsers className="inline mr-1" size={14} />
|
<FaUsers size={14} />
|
||||||
Katılımcılar
|
Katılımcılar
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{user.role === 'teacher' && (
|
{user.role === 'teacher' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => setActiveTab('attendance')}
|
onClick={() => setActiveTab('attendance')}
|
||||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
className={`!inline-flex !h-auto flex-1 items-center justify-center gap-1 rounded-md !px-3 py-2 text-sm font-medium transition-all ${
|
||||||
activeTab === 'attendance'
|
activeTab === 'attendance'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? '!bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaClipboardList className="inline mr-1" size={14} />
|
<FaClipboardList size={14} />
|
||||||
Katılım Raporu
|
Katılım Raporu
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,13 +144,16 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||||
{/* Hand Raise Indicator & Teacher Control */}
|
{/* Hand Raise Indicator & Teacher Control */}
|
||||||
{participant.isHandRaised &&
|
{participant.isHandRaised &&
|
||||||
(user.role === 'teacher' && !participant.isTeacher ? (
|
(user.role === 'teacher' && !participant.isTeacher ? (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onDismissHandRaise(participant.id)}
|
onClick={() => onDismissHandRaise(participant.id)}
|
||||||
className="ml-2 p-1 rounded bg-yellow-100 hover:bg-yellow-200"
|
className="ml-2 !inline-flex !h-auto items-center justify-center rounded !bg-yellow-100 !p-1 hover:!bg-yellow-200"
|
||||||
title="El kaldırmayı kaldır"
|
title="El kaldırmayı kaldır"
|
||||||
>
|
>
|
||||||
<FaHandPaper className="text-yellow-600" />
|
<FaHandPaper className="text-yellow-600" />
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<FaHandPaper className="text-yellow-600 ml-2" title="Parmak kaldırdı" />
|
<FaHandPaper className="text-yellow-600 ml-2" title="Parmak kaldırdı" />
|
||||||
))}
|
))}
|
||||||
|
|
@ -148,19 +164,22 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||||
|
|
||||||
{/* Mute / Unmute Button */}
|
{/* Mute / Unmute Button */}
|
||||||
{user.role === 'teacher' && !participant.isTeacher && (
|
{user.role === 'teacher' && !participant.isTeacher && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onMuteParticipant(participant.id, !participant.isAudioMuted, true)
|
onMuteParticipant(participant.id, !participant.isAudioMuted, true)
|
||||||
}
|
}
|
||||||
className={`p-1 rounded transition-colors ${
|
className={`!inline-flex !h-auto items-center justify-center rounded !bg-transparent !p-1 transition-colors ${
|
||||||
participant.isAudioMuted
|
participant.isAudioMuted
|
||||||
? 'text-green-600 hover:bg-green-50'
|
? 'text-green-600 hover:!bg-green-50'
|
||||||
: 'text-yellow-600 hover:bg-yellow-50'
|
: 'text-yellow-600 hover:!bg-yellow-50'
|
||||||
}`}
|
}`}
|
||||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||||
>
|
>
|
||||||
{participant.isAudioMuted ? <FaMicrophone /> : <FaMicrophoneSlash />}
|
{participant.isAudioMuted ? <FaMicrophone /> : <FaMicrophoneSlash />}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Video muted indicator */}
|
{/* Video muted indicator */}
|
||||||
|
|
@ -174,13 +193,16 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||||
|
|
||||||
{/* Kick Button (Teacher Only) */}
|
{/* Kick Button (Teacher Only) */}
|
||||||
{user.role === 'teacher' && !participant.isTeacher && (
|
{user.role === 'teacher' && !participant.isTeacher && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onKickParticipant(participant.id, participant.name)}
|
onClick={() => onKickParticipant(participant.id, participant.name)}
|
||||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded !bg-transparent !p-1 text-red-600 transition-colors hover:!bg-red-50"
|
||||||
title="Sınıftan Çıkar"
|
title="Sınıftan Çıkar"
|
||||||
>
|
>
|
||||||
<FaUserTimes size={12} />
|
<FaUserTimes size={12} />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import { ScreenSharePanel } from '@/views/admin/videoroom/ScreenSharePanel'
|
||||||
import { RoomParticipant } from '@/views/admin/videoroom/RoomParticipant'
|
import { RoomParticipant } from '@/views/admin/videoroom/RoomParticipant'
|
||||||
import toast from '@/components/ui/toast/toast'
|
import toast from '@/components/ui/toast/toast'
|
||||||
import Notification from '@/components/ui/Notification'
|
import Notification from '@/components/ui/Notification'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
VideoroomDocumentDto,
|
VideoroomDocumentDto,
|
||||||
VideoroomAttendanceDto,
|
VideoroomAttendanceDto,
|
||||||
|
|
@ -942,84 +943,102 @@ const RoomDetail: React.FC = () => {
|
||||||
<div className="flex items-center space-x-1 sm:space-x-2">
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
||||||
{/* Audio Control */}
|
{/* Audio Control */}
|
||||||
{user.role !== 'observer' && (
|
{user.role !== 'observer' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleToggleAudio}
|
onClick={handleToggleAudio}
|
||||||
className={`p-2 sm:p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
||||||
isAudioEnabled
|
isAudioEnabled
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
? '!bg-gray-700 hover:!bg-gray-600'
|
||||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
: '!bg-red-600 hover:!bg-red-700'
|
||||||
}`}
|
}`}
|
||||||
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
||||||
>
|
>
|
||||||
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Video Control */}
|
{/* Video Control */}
|
||||||
{user.role !== 'observer' && (
|
{user.role !== 'observer' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleToggleVideo}
|
onClick={handleToggleVideo}
|
||||||
className={`p-2 sm:p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
||||||
isVideoEnabled
|
isVideoEnabled
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
? '!bg-gray-700 hover:!bg-gray-600'
|
||||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
: '!bg-red-600 hover:!bg-red-700'
|
||||||
}`}
|
}`}
|
||||||
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
||||||
>
|
>
|
||||||
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Screen Share */}
|
{/* Screen Share */}
|
||||||
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
|
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
|
||||||
className={`p-2 sm:p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
||||||
isScreenSharing
|
isScreenSharing
|
||||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
? '!bg-blue-600 hover:!bg-blue-700'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
: '!bg-gray-700 hover:!bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
||||||
>
|
>
|
||||||
<FaDesktop size={16} />
|
<FaDesktop size={16} />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hand Raise (Students) */}
|
{/* Hand Raise (Students) */}
|
||||||
{user.role === 'student' && classSettings.allowHandRaise && (
|
{user.role === 'student' && classSettings.allowHandRaise && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleRaiseHand}
|
onClick={handleRaiseHand}
|
||||||
disabled={hasRaisedHand}
|
disabled={hasRaisedHand}
|
||||||
className={`p-2 sm:p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-2 text-white transition-all sm:!p-3 ${
|
||||||
hasRaisedHand
|
hasRaisedHand
|
||||||
? 'bg-yellow-600 text-white cursor-not-allowed'
|
? '!bg-yellow-600 cursor-not-allowed'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
|
: '!bg-gray-700 hover:!bg-yellow-600'
|
||||||
}`}
|
}`}
|
||||||
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
||||||
>
|
>
|
||||||
<FaHandPaper size={16} />
|
<FaHandPaper size={16} />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Leave Call */}
|
{/* Leave Call */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleLeaveCall}
|
onClick={handleLeaveCall}
|
||||||
className="p-2 sm:p-3 rounded-full bg-red-600 hover:bg-red-700 text-white transition-all"
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-2 text-white transition-all hover:!bg-red-700 sm:!p-3"
|
||||||
title="Aramayı Sonlandır"
|
title="Aramayı Sonlandır"
|
||||||
>
|
>
|
||||||
<FaPhone size={16} />
|
<FaPhone size={16} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side - Panel Controls */}
|
{/* Right Side - Panel Controls */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<Button
|
||||||
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-gray-700 !p-2 text-white hover:!bg-gray-600"
|
||||||
onClick={() => setMobileMenuOpen(true)}
|
onClick={() => setMobileMenuOpen(true)}
|
||||||
aria-label="Menüyü Aç"
|
aria-label="Menüyü Aç"
|
||||||
>
|
>
|
||||||
<FaBars size={20} />
|
<FaBars size={20} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hamburger Menu Modal */}
|
{/* Hamburger Menu Modal */}
|
||||||
|
|
@ -1040,20 +1059,26 @@ const RoomDetail: React.FC = () => {
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-4 border-b">
|
<div className="flex items-center justify-between px-4 py-4 border-b">
|
||||||
<span className="font-semibold text-gray-800 text-lg">Menü</span>
|
<span className="font-semibold text-gray-800 text-lg">Menü</span>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className="p-2 rounded-full hover:bg-gray-100"
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-transparent !p-2 hover:!bg-gray-100"
|
||||||
>
|
>
|
||||||
<FaTimes size={22} />
|
<FaTimes size={22} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col space-y-1 px-2 py-2 overflow-y-auto">
|
<div className="flex-1 flex flex-col space-y-1 px-2 py-2 overflow-y-auto">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
setTimeout(() => toggleSidePanel('chat'), 200)
|
setTimeout(() => toggleSidePanel('chat'), 200)
|
||||||
}}
|
}}
|
||||||
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'chat' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all ${activeSidePanel === 'chat' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
<FaComments /> <span>Sohbet</span>
|
<FaComments /> <span>Sohbet</span>
|
||||||
{chatMessages.length > 0 && (
|
{chatMessages.length > 0 && (
|
||||||
|
|
@ -1061,13 +1086,16 @@ const RoomDetail: React.FC = () => {
|
||||||
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
setTimeout(() => toggleSidePanel('participants'), 200)
|
setTimeout(() => toggleSidePanel('participants'), 200)
|
||||||
}}
|
}}
|
||||||
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base relative ${activeSidePanel === 'participants' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all relative ${activeSidePanel === 'participants' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
<FaUserFriends />
|
<FaUserFriends />
|
||||||
<span>Katılımcılar</span>
|
<span>Katılımcılar</span>
|
||||||
|
|
@ -1077,47 +1105,59 @@ const RoomDetail: React.FC = () => {
|
||||||
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
setTimeout(() => toggleSidePanel('documents'), 200)
|
setTimeout(() => toggleSidePanel('documents'), 200)
|
||||||
}}
|
}}
|
||||||
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'documents' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all ${activeSidePanel === 'documents' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
<FaFile /> <span>Dokümanlar</span>
|
<FaFile /> <span>Dokümanlar</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
setTimeout(() => toggleSidePanel('layout'), 200)
|
setTimeout(() => toggleSidePanel('layout'), 200)
|
||||||
}}
|
}}
|
||||||
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'layout' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
|
className={`!inline-flex !h-auto items-center gap-2 rounded-lg !p-3 text-base transition-all ${activeSidePanel === 'layout' ? '!bg-blue-100 text-blue-700' : '!bg-transparent text-gray-700 hover:!bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
<FaLayerGroup /> <span>Görünüm</span>
|
<FaLayerGroup /> <span>Görünüm</span>
|
||||||
</button>
|
</Button>
|
||||||
{user.role === 'teacher' && (
|
{user.role === 'teacher' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
setTimeout(() => handleMuteAll(), 200)
|
setTimeout(() => handleMuteAll(), 200)
|
||||||
}}
|
}}
|
||||||
className="flex items-center space-x-2 p-3 rounded-lg transition-all hover:bg-gray-100 text-gray-700 text-base"
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-transparent !p-3 text-base text-gray-700 transition-all hover:!bg-gray-100"
|
||||||
>
|
>
|
||||||
{isAllMuted ? <FaVolumeUp /> : <FaVolumeMute />}{' '}
|
{isAllMuted ? <FaVolumeUp /> : <FaVolumeMute />}{' '}
|
||||||
<span>{isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}</span>
|
<span>{isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
setTimeout(() => toggleFullscreen(), 200)
|
setTimeout(() => toggleFullscreen(), 200)
|
||||||
}}
|
}}
|
||||||
className="flex items-center space-x-2 p-3 rounded-lg transition-all hover:bg-gray-100 text-gray-700 text-base"
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-transparent !p-3 text-base text-gray-700 transition-all hover:!bg-gray-100"
|
||||||
>
|
>
|
||||||
{isFullscreen ? <FaCompress /> : <FaExpand />}{' '}
|
{isFullscreen ? <FaCompress /> : <FaExpand />}{' '}
|
||||||
<span>{isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}</span>
|
<span>{isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -1141,95 +1181,116 @@ const RoomDetail: React.FC = () => {
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{/* Audio Control */}
|
{/* Audio Control */}
|
||||||
{user.role !== 'observer' && (
|
{user.role !== 'observer' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleToggleAudio}
|
onClick={handleToggleAudio}
|
||||||
className={`p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
||||||
isAudioEnabled
|
isAudioEnabled
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
? '!bg-gray-700 hover:!bg-gray-600'
|
||||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
: '!bg-red-600 hover:!bg-red-700'
|
||||||
}`}
|
}`}
|
||||||
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
||||||
>
|
>
|
||||||
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Video Control */}
|
{/* Video Control */}
|
||||||
{user.role !== 'observer' && (
|
{user.role !== 'observer' && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleToggleVideo}
|
onClick={handleToggleVideo}
|
||||||
className={`p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
||||||
isVideoEnabled
|
isVideoEnabled
|
||||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
? '!bg-gray-700 hover:!bg-gray-600'
|
||||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
: '!bg-red-600 hover:!bg-red-700'
|
||||||
}`}
|
}`}
|
||||||
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
||||||
>
|
>
|
||||||
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Screen Share */}
|
{/* Screen Share */}
|
||||||
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
|
onClick={isScreenSharing ? handleStopScreenShare : handleStartScreenShare}
|
||||||
className={`p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
||||||
isScreenSharing
|
isScreenSharing
|
||||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
? '!bg-blue-600 hover:!bg-blue-700'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
: '!bg-gray-700 hover:!bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
||||||
>
|
>
|
||||||
<FaDesktop size={16} />
|
<FaDesktop size={16} />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hand Raise (Students) */}
|
{/* Hand Raise (Students) */}
|
||||||
{user.role === 'student' && classSettings.allowHandRaise && (
|
{user.role === 'student' && classSettings.allowHandRaise && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleRaiseHand}
|
onClick={handleRaiseHand}
|
||||||
disabled={hasRaisedHand}
|
disabled={hasRaisedHand}
|
||||||
className={`p-3 rounded-full transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-3 text-white transition-all ${
|
||||||
hasRaisedHand
|
hasRaisedHand
|
||||||
? 'bg-yellow-600 text-white cursor-not-allowed'
|
? '!bg-yellow-600 cursor-not-allowed'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
|
: '!bg-gray-700 hover:!bg-yellow-600'
|
||||||
}`}
|
}`}
|
||||||
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
||||||
>
|
>
|
||||||
<FaHandPaper size={16} />
|
<FaHandPaper size={16} />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Leave Call */}
|
{/* Leave Call */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={handleLeaveCall}
|
onClick={handleLeaveCall}
|
||||||
className="p-3 rounded-full bg-red-600 hover:bg-red-700 text-white transition-all"
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-3 text-white transition-all hover:!bg-red-700"
|
||||||
title="Aramayı Sonlandır"
|
title="Aramayı Sonlandır"
|
||||||
>
|
>
|
||||||
<FaPhone size={16} />
|
<FaPhone size={16} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side - Panel Controls */}
|
{/* Right Side - Panel Controls */}
|
||||||
<div className="flex items-center space-x-2 absolute right-0">
|
<div className="flex items-center space-x-2 absolute right-0">
|
||||||
{/* Fullscreen Toggle */}
|
{/* Fullscreen Toggle */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white transition-all"
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-gray-700 !p-2 text-white transition-all hover:!bg-gray-600"
|
||||||
title={isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}
|
title={isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}
|
||||||
>
|
>
|
||||||
{isFullscreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
|
{isFullscreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Chat */}
|
{/* Chat */}
|
||||||
{((user.role !== 'observer' && classSettings.allowStudentChat) ||
|
{((user.role !== 'observer' && classSettings.allowStudentChat) ||
|
||||||
user.role === 'teacher') && (
|
user.role === 'teacher') && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => toggleSidePanel('chat')}
|
onClick={() => toggleSidePanel('chat')}
|
||||||
className={`p-2 rounded-lg transition-all relative ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all relative ${
|
||||||
activeSidePanel === 'chat'
|
activeSidePanel === 'chat'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 text-white'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||||
}`}
|
}`}
|
||||||
title="Sohbet"
|
title="Sohbet"
|
||||||
>
|
>
|
||||||
|
|
@ -1239,16 +1300,19 @@ const RoomDetail: React.FC = () => {
|
||||||
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Participants */}
|
{/* Participants */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => toggleSidePanel('participants')}
|
onClick={() => toggleSidePanel('participants')}
|
||||||
className={`p-2 rounded-lg transition-all relative ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all relative ${
|
||||||
activeSidePanel === 'participants'
|
activeSidePanel === 'participants'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 text-white'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||||
}`}
|
}`}
|
||||||
title="Katılımcılar"
|
title="Katılımcılar"
|
||||||
>
|
>
|
||||||
|
|
@ -1259,47 +1323,56 @@ const RoomDetail: React.FC = () => {
|
||||||
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Teacher Only Options */}
|
{/* Teacher Only Options */}
|
||||||
{user.role === 'teacher' && (
|
{user.role === 'teacher' && (
|
||||||
<>
|
<>
|
||||||
{/* Documents Button */}
|
{/* Documents Button */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => toggleSidePanel('documents')}
|
onClick={() => toggleSidePanel('documents')}
|
||||||
className={`p-2 rounded-lg transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all ${
|
||||||
activeSidePanel === 'documents'
|
activeSidePanel === 'documents'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 text-white'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||||
}`}
|
}`}
|
||||||
title="Dokümanlar"
|
title="Dokümanlar"
|
||||||
>
|
>
|
||||||
<FaFile size={14} />
|
<FaFile size={14} />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Mute All Button */}
|
{/* Mute All Button */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={handleMuteAll}
|
onClick={handleMuteAll}
|
||||||
className="p-2 rounded-lg transition-all bg-gray-700 hover:bg-gray-600 text-white"
|
className="!inline-flex !h-auto items-center justify-center rounded-lg !bg-gray-700 !p-2 text-white transition-all hover:!bg-gray-600"
|
||||||
title={isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}
|
title={isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}
|
||||||
>
|
>
|
||||||
{isAllMuted ? <FaVolumeUp size={14} /> : <FaVolumeMute size={14} />}
|
{isAllMuted ? <FaVolumeUp size={14} /> : <FaVolumeMute size={14} />}
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Layout Button */}
|
{/* Layout Button */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => toggleSidePanel('layout')}
|
onClick={() => toggleSidePanel('layout')}
|
||||||
className={`p-2 rounded-lg transition-all ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-lg !p-2 transition-all ${
|
||||||
activeSidePanel === 'layout'
|
activeSidePanel === 'layout'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 text-white'
|
||||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||||
}`}
|
}`}
|
||||||
title="Layout"
|
title="Layout"
|
||||||
>
|
>
|
||||||
<FaLayerGroup size={14} />
|
<FaLayerGroup size={14} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@ const RoomList: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scheduled Classes */}
|
{/* Scheduled Classes */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
<div className="dark:bg-gray-900 rounded-lg">
|
||||||
{videoList.length === 0 ? (
|
{videoList.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
<FaCalendarAlt size={48} className="mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
||||||
|
|
@ -481,7 +481,7 @@ const RoomList: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card Footer — Meta Info */}
|
{/* Card Footer — Meta Info */}
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2.5 bg-gray-50 rounded-b-xl border-t border-gray-100 text-xs text-gray-500">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2.5 bg-gray-50 rounded-b-xl border-t border-gray-100 text-xs text-gray-500 dark:bg-gray-800">
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<FaCalendarAlt size={11} className="text-blue-400" />
|
<FaCalendarAlt size={11} className="text-blue-400" />
|
||||||
{showDbDateAsIs(classSession.scheduledStartTime)}
|
{showDbDateAsIs(classSession.scheduledStartTime)}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
|
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
|
||||||
import { VideoPlayer } from './VideoPlayer'
|
import { VideoPlayer } from './VideoPlayer'
|
||||||
import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface RoomParticipantProps {
|
interface RoomParticipantProps {
|
||||||
participants: VideoroomParticipantDto[]
|
participants: VideoroomParticipantDto[]
|
||||||
|
|
@ -240,28 +241,34 @@ export const RoomParticipant: React.FC<RoomParticipantProps> = ({
|
||||||
{/* Teacher controls for students */}
|
{/* Teacher controls for students */}
|
||||||
{isTeacher && participant.id !== currentUserId && (
|
{isTeacher && participant.id !== currentUserId && (
|
||||||
<div className="absolute top-2 left-2 flex space-x-1 z-10">
|
<div className="absolute top-2 left-2 flex space-x-1 z-10">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
|
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
|
||||||
}}
|
}}
|
||||||
className={`p-1 rounded-full text-white text-xs ${
|
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-1 text-xs text-white ${
|
||||||
participant.isAudioMuted ? 'bg-red-600' : 'bg-gray-600 hover:bg-gray-700'
|
participant.isAudioMuted ? '!bg-red-600' : '!bg-gray-600 hover:!bg-gray-700'
|
||||||
} transition-colors`}
|
} transition-colors`}
|
||||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||||
>
|
>
|
||||||
<FaMicrophoneSlash size={12} />
|
<FaMicrophoneSlash size={12} />
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onKickParticipant?.(participant.id)
|
onKickParticipant?.(participant.id)
|
||||||
}}
|
}}
|
||||||
className="p-1 rounded-full bg-red-600 hover:bg-red-700 text-white text-xs transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-1 text-xs text-white transition-colors hover:!bg-red-700"
|
||||||
title="Sınıftan Çıkar"
|
title="Sınıftan Çıkar"
|
||||||
>
|
>
|
||||||
<FaUserTimes size={12} />
|
<FaUserTimes size={12} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FaDesktop, FaStop, FaPlay } from 'react-icons/fa';
|
import { FaDesktop, FaStop, FaPlay } from 'react-icons/fa';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
|
||||||
interface ScreenSharePanelProps {
|
interface ScreenSharePanelProps {
|
||||||
isSharing: boolean;
|
isSharing: boolean;
|
||||||
|
|
@ -25,21 +26,27 @@ export const ScreenSharePanel: React.FC<ScreenSharePanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSharing ? (
|
{isSharing ? (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={onStopShare}
|
onClick={onStopShare}
|
||||||
className="flex items-center space-x-2 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-red-600 !px-3 py-2 text-white transition-colors hover:!bg-red-700"
|
||||||
>
|
>
|
||||||
<FaStop size={16} />
|
<FaStop size={16} />
|
||||||
<span>Paylaşımı Durdur</span>
|
<span>Paylaşımı Durdur</span>
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={onStartShare}
|
onClick={onStartShare}
|
||||||
className="flex items-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
className="!inline-flex !h-auto items-center gap-2 rounded-lg !bg-blue-600 !px-3 py-2 text-white transition-colors hover:!bg-blue-700"
|
||||||
>
|
>
|
||||||
<FaPlay size={16} />
|
<FaPlay size={16} />
|
||||||
<span>Ekranı Paylaş</span>
|
<span>Ekranı Paylaş</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -593,14 +593,16 @@ const Assistant = () => {
|
||||||
<div className="mt-3 max-h-[calc(100vh-140px)] overflow-y-auto overscroll-contain">
|
<div className="mt-3 max-h-[calc(100vh-140px)] overflow-y-auto overscroll-contain">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{conversations.map((conversation) => (
|
{conversations.map((conversation) => (
|
||||||
<button
|
<Button
|
||||||
key={conversation.id}
|
key={conversation.id}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => handleSelectConversation(conversation.id)}
|
onClick={() => handleSelectConversation(conversation.id)}
|
||||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-3 text-left text-sm transition-colors ${
|
className={`!inline-flex !h-auto !w-full items-center gap-2 rounded-lg !px-3 py-3 text-left text-sm transition-colors ${
|
||||||
conversation.id === activeConversationId
|
conversation.id === activeConversationId
|
||||||
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200'
|
? '!bg-blue-50 text-blue-700 dark:!bg-blue-900/40 dark:text-blue-200'
|
||||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700'
|
: '!bg-transparent text-gray-700 hover:!bg-gray-100 dark:text-gray-200 dark:hover:!bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaComment className="shrink-0" />
|
<FaComment className="shrink-0" />
|
||||||
|
|
@ -617,7 +619,7 @@ const Assistant = () => {
|
||||||
>
|
>
|
||||||
<FaTrash className="text-xs" />
|
<FaTrash className="text-xs" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -631,14 +633,16 @@ const Assistant = () => {
|
||||||
<ScrollBar autoHide className="mt-3 flex-1 min-h-0">
|
<ScrollBar autoHide className="mt-3 flex-1 min-h-0">
|
||||||
<div className="space-y-1 pr-2">
|
<div className="space-y-1 pr-2">
|
||||||
{conversations.map((conversation) => (
|
{conversations.map((conversation) => (
|
||||||
<button
|
<Button
|
||||||
key={conversation.id}
|
key={conversation.id}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => handleSelectConversation(conversation.id)}
|
onClick={() => handleSelectConversation(conversation.id)}
|
||||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors ${
|
className={`!inline-flex !h-auto !w-full items-center gap-2 rounded-lg !px-3 py-2 text-left text-sm transition-colors ${
|
||||||
conversation.id === activeConversationId
|
conversation.id === activeConversationId
|
||||||
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200'
|
? '!bg-blue-50 text-blue-700 dark:!bg-blue-900/40 dark:text-blue-200'
|
||||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700'
|
: '!bg-transparent text-gray-700 hover:!bg-gray-100 dark:text-gray-200 dark:hover:!bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaComment className="shrink-0" />
|
<FaComment className="shrink-0" />
|
||||||
|
|
@ -663,7 +667,7 @@ const Assistant = () => {
|
||||||
>
|
>
|
||||||
<FaTrash className="text-xs" />
|
<FaTrash className="text-xs" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ScrollBar>
|
</ScrollBar>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import PropertyPanel from '../../components/codeLayout/PropertyPanel'
|
||||||
import ComponentSelector from '../../components/codeLayout/ComponentSelector'
|
import ComponentSelector from '../../components/codeLayout/ComponentSelector'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useComponents } from '../../contexts/ComponentContext'
|
import { useComponents } from '../../contexts/ComponentContext'
|
||||||
import { toast } from '../../components/ui'
|
import { Button, toast } from '../../components/ui'
|
||||||
import Notification from '../../components/ui/Notification/Notification'
|
import Notification from '../../components/ui/Notification/Notification'
|
||||||
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
|
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
|
||||||
import { ComponentCodeEditor } from '@/components/codeLayout/ComponentCodeEditor'
|
import { ComponentCodeEditor } from '@/components/codeLayout/ComponentCodeEditor'
|
||||||
|
|
@ -627,15 +627,17 @@ function ComponentCodeLayout() {
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{dependencies.join(', ')}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">{dependencies.join(', ')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center">
|
||||||
<button
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
onClick={() => setShowPanelManager(true)}
|
onClick={() => setShowPanelManager(true)}
|
||||||
className="flex items-center space-x-2 px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
className="!inline-flex items-center gap-2 rounded-lg border border-gray-300 !bg-white transition-colors hover:!bg-gray-50 dark:border-gray-700 dark:!bg-gray-900 dark:hover:!bg-gray-800"
|
||||||
title="Panel Manager"
|
title="Panel Manager"
|
||||||
>
|
>
|
||||||
<FaThLarge className="w-4 h-4 text-gray-600 dark:text-gray-300" />
|
<FaThLarge className="text-gray-600 dark:text-gray-300" />
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">Panels</span>
|
<span className="text-sm text-gray-600 dark:text-gray-300">Panels</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
import { Formik, Form, Field, FieldProps } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
||||||
|
|
||||||
// Error tipini tanımla
|
// Error tipini tanımla
|
||||||
interface ValidationError {
|
interface ValidationError {
|
||||||
|
|
@ -202,7 +202,8 @@ export default ${pascalCaseName}Component;`
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
|
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Enhanced Header */}
|
{/* Enhanced Header */}
|
||||||
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10">
|
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10">
|
||||||
|
|
@ -237,17 +238,20 @@ export default ${pascalCaseName}Component;`
|
||||||
</Link>
|
</Link>
|
||||||
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant='solid'
|
||||||
|
size="sm"
|
||||||
|
color='green'
|
||||||
onClick={submitForm}
|
onClick={submitForm}
|
||||||
disabled={isSubmitting || !values.name.trim() || !isValid}
|
disabled={isSubmitting || !values.name.trim() || !isValid}
|
||||||
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:hover:bg-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="flex items-center gap-2 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:hover:bg-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
<FaRegSave className="w-4 h-4" />
|
<FaRegSave className="w-4 h-4" />
|
||||||
{isSubmitting
|
{isSubmitting
|
||||||
? translate('::App.DeveloperKit.ComponentEditor.Saving')
|
? translate('::App.DeveloperKit.ComponentEditor.Saving')
|
||||||
: translate('::App.DeveloperKit.ComponentEditor.Save')}
|
: translate('::App.DeveloperKit.ComponentEditor.Save')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -286,8 +290,7 @@ export default ${pascalCaseName}Component;`
|
||||||
if (!isEditing && newName.trim()) {
|
if (!isEditing && newName.trim()) {
|
||||||
const currentCode = values.code.trim()
|
const currentCode = values.code.trim()
|
||||||
const isCodeEmpty = !currentCode
|
const isCodeEmpty = !currentCode
|
||||||
const isCodeDefaultTemplate =
|
const isCodeDefaultTemplate = currentCode.includes('Component = ({') &&
|
||||||
currentCode.includes('Component = ({') &&
|
|
||||||
currentCode.includes('export default') &&
|
currentCode.includes('export default') &&
|
||||||
currentCode.includes('<span>{title}</span>')
|
currentCode.includes('<span>{title}</span>')
|
||||||
|
|
||||||
|
|
@ -297,8 +300,7 @@ export default ${pascalCaseName}Component;`
|
||||||
parseAndUpdateComponents(template)
|
parseAndUpdateComponents(template)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
} } />
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -310,8 +312,7 @@ export default ${pascalCaseName}Component;`
|
||||||
name="description"
|
name="description"
|
||||||
type="text"
|
type="text"
|
||||||
component={Input}
|
component={Input}
|
||||||
placeholder="Brief description of the component"
|
placeholder="Brief description of the component" />
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -328,8 +329,7 @@ export default ${pascalCaseName}Component;`
|
||||||
rows={10}
|
rows={10}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setFieldValue('code', e.target.value)
|
setFieldValue('code', e.target.value)
|
||||||
}}
|
} } />
|
||||||
/>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -342,17 +342,14 @@ export default ${pascalCaseName}Component;`
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={(values.dependencies || []).join(', ')}
|
value={(values.dependencies || []).join(', ')}
|
||||||
onChange={(e) =>
|
onChange={(e) => setFieldValue(
|
||||||
setFieldValue(
|
|
||||||
'dependencies',
|
'dependencies',
|
||||||
e.target.value
|
e.target.value
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean)
|
||||||
)
|
)}
|
||||||
}
|
placeholder="MyComponent, AnotherComponent, etc." />
|
||||||
placeholder="MyComponent, AnotherComponent, etc."
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -422,7 +419,8 @@ export default ${pascalCaseName}Component;`
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Loading } from '../../components/shared'
|
import { Loading } from '../../components/shared'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
const ComponentManager: React.FC = () => {
|
const ComponentManager: React.FC = () => {
|
||||||
const { components, loading, updateComponent, deleteComponent } = useComponents()
|
const { components, loading, updateComponent, deleteComponent } = useComponents()
|
||||||
|
|
@ -208,12 +209,15 @@ const ComponentManager: React.FC = () => {
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-gray-700">
|
<div className="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-gray-700">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => handleToggleActive(component.id, !component.isActive)}
|
onClick={() => handleToggleActive(component.id, !component.isActive)}
|
||||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors ${
|
className={`!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors ${
|
||||||
component.isActive
|
component.isActive
|
||||||
? 'bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-800'
|
? '!bg-green-100 text-green-700 hover:!bg-green-200 dark:!bg-green-900/20 dark:text-green-400 dark:hover:!bg-green-800'
|
||||||
: 'bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-gray-400 hover:bg-slate-200 dark:hover:bg-gray-700'
|
: '!bg-slate-100 text-slate-600 hover:!bg-slate-200 dark:!bg-gray-800 dark:text-gray-400 dark:hover:!bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{component.isActive ? (
|
{component.isActive ? (
|
||||||
|
|
@ -227,7 +231,7 @@ const ComponentManager: React.FC = () => {
|
||||||
{translate('::App.DeveloperKit.Component.Inactive')}
|
{translate('::App.DeveloperKit.Component.Inactive')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -251,13 +255,16 @@ const ComponentManager: React.FC = () => {
|
||||||
>
|
>
|
||||||
<FaEye className="w-4 h-4" />
|
<FaEye className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => handleDelete(component.id, component.name)}
|
onClick={() => handleDelete(component.id, component.name)}
|
||||||
className="p-2 text-slate-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900 rounded transition-colors"
|
className="!inline-flex !h-auto items-center justify-center rounded !bg-transparent !p-2 text-slate-600 transition-colors hover:!bg-red-50 hover:text-red-600 dark:text-gray-400 dark:hover:!bg-red-900 dark:hover:text-red-400"
|
||||||
title={translate('::App.DeveloperKit.Component.Delete')}
|
title={translate('::App.DeveloperKit.Component.Delete')}
|
||||||
>
|
>
|
||||||
<FaTrashAlt className="w-4 h-4" />
|
<FaTrashAlt className="w-4 h-4" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
|
||||||
import type { CrudEndpoint } from '@/proxy/developerKit/models'
|
import type { CrudEndpoint } from '@/proxy/developerKit/models'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
interface TestResult {
|
interface TestResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
|
|
@ -379,7 +380,9 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{ds.code}
|
{ds.code}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
{dataSources.length === 0 && <option value="">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</option>}
|
{dataSources.length === 0 && (
|
||||||
|
<option value="">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</option>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -404,21 +407,24 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
}
|
}
|
||||||
const active = crudFilter === f
|
const active = crudFilter === f
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
key={f}
|
key={f}
|
||||||
onClick={() => setCrudFilter(f)}
|
onClick={() => setCrudFilter(f)}
|
||||||
className={`flex-1 py-1.5 transition-colors ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`flex-1 !h-auto !rounded-none !px-0 !py-1.5 transition-colors ${
|
||||||
active
|
active
|
||||||
? f === 'with'
|
? f === 'with'
|
||||||
? 'bg-green-500 text-white'
|
? '!bg-green-500 text-white'
|
||||||
: f === 'without'
|
: f === 'without'
|
||||||
? 'bg-slate-500 text-white'
|
? '!bg-slate-500 text-white'
|
||||||
: 'bg-blue-500 text-white'
|
: '!bg-blue-500 text-white'
|
||||||
: 'bg-white dark:bg-gray-900 text-slate-500 dark:text-gray-400 hover:bg-slate-50 dark:hover:bg-gray-800'
|
: '!bg-white text-slate-500 hover:!bg-slate-50 dark:!bg-gray-900 dark:text-gray-400 dark:hover:!bg-gray-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{labels[f]}
|
{labels[f]}
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -429,11 +435,15 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{loadingTables ? (
|
{loadingTables ? (
|
||||||
<div className="flex items-center justify-center p-8 text-slate-400">
|
<div className="flex items-center justify-center p-8 text-slate-400">
|
||||||
<FaSyncAlt className="animate-spin mr-2" />
|
<FaSyncAlt className="animate-spin mr-2" />
|
||||||
<span className="text-sm">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</span>
|
<span className="text-sm">
|
||||||
|
{translate('::App.DeveloperKit.CrudEndpoints.Loading')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : filteredTables.length === 0 ? (
|
) : filteredTables.length === 0 ? (
|
||||||
<div className="p-6 text-center text-slate-400 text-sm">
|
<div className="p-6 text-center text-slate-400 text-sm">
|
||||||
{selectedDataSource ? translate('::App.DeveloperKit.CrudEndpoints.NoTablesFound') : translate('::App.DeveloperKit.CrudEndpoints.SelectDataSource')}
|
{selectedDataSource
|
||||||
|
? translate('::App.DeveloperKit.CrudEndpoints.NoTablesFound')
|
||||||
|
: translate('::App.DeveloperKit.CrudEndpoints.SelectDataSource')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(tablesBySchema).map(([schema, tables]) => (
|
Object.entries(tablesBySchema).map(([schema, tables]) => (
|
||||||
|
|
@ -447,36 +457,47 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
const isSelected = selectedTable?.fullName === table.fullName
|
const isSelected = selectedTable?.fullName === table.fullName
|
||||||
const hasEndpoints = total > 0
|
const hasEndpoints = total > 0
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
key={table.fullName}
|
key={table.fullName}
|
||||||
onClick={() => setSelectedTable(table)}
|
onClick={() => setSelectedTable(table)}
|
||||||
className={`w-full flex items-center justify-between px-3 py-1 text-left hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors border-b border-slate-50 dark:border-gray-800 ${
|
variant="plain"
|
||||||
isSelected ? 'bg-blue-50 dark:bg-blue-900/20 border-l-2 border-l-blue-500 dark:border-l-blue-400' : ''
|
shape="none"
|
||||||
|
className={`w-full !h-auto !items-center !justify-between !rounded-none border-b border-slate-50 !px-3 !py-1 text-left transition-colors hover:!bg-slate-50 dark:border-gray-800 dark:hover:!bg-gray-800 ${
|
||||||
|
isSelected
|
||||||
|
? '!bg-blue-50 border-l-2 border-l-blue-500 dark:!bg-blue-900/20 dark:border-l-blue-400'
|
||||||
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<span className="flex w-full min-w-0 items-center justify-between gap-2">
|
||||||
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<FaTable
|
<FaTable
|
||||||
className={`flex-shrink-0 text-xs ${
|
className={`flex-shrink-0 text-xs ${
|
||||||
hasEndpoints ? 'text-green-500' : 'text-slate-300'
|
hasEndpoints ? 'text-green-500' : 'text-slate-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-slate-700 dark:text-gray-100 truncate">{table.tableName}</span>
|
<span className="truncate text-sm text-slate-700 dark:text-gray-100">
|
||||||
</div>
|
{table.tableName}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-shrink-0 items-center gap-1">
|
||||||
{hasEndpoints && (
|
{hasEndpoints && (
|
||||||
<span
|
<span
|
||||||
className={`flex-shrink-0 text-xs px-1.5 py-0.5 rounded-full font-medium ${
|
className={`rounded-full px-1.5 py-0.5 text-xs font-medium ${
|
||||||
active > 0
|
active > 0
|
||||||
? 'bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-400'
|
? 'bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400'
|
||||||
: 'bg-slate-100 dark:bg-gray-800 text-slate-500 dark:text-gray-400'
|
: 'bg-slate-100 text-slate-500 dark:bg-gray-800 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{active}/{total}
|
{active}/{total}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<FaChevronRight className="flex-shrink-0 text-blue-400 text-xs ml-1" />
|
<FaChevronRight className="flex-shrink-0 text-xs text-blue-400" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -490,7 +511,9 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{!selectedTable ? (
|
{!selectedTable ? (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 dark:text-gray-500 p-8">
|
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 dark:text-gray-500 p-8">
|
||||||
<FaDatabase className="text-4xl mb-3 text-slate-200 dark:text-gray-700" />
|
<FaDatabase className="text-4xl mb-3 text-slate-200 dark:text-gray-700" />
|
||||||
<p className="text-base font-medium dark:text-gray-300">{translate('::App.DeveloperKit.CrudEndpoints.SelectTablePrompt')}</p>
|
<p className="text-base font-medium dark:text-gray-300">
|
||||||
|
{translate('::App.DeveloperKit.CrudEndpoints.SelectTablePrompt')}
|
||||||
|
</p>
|
||||||
<p className="text-sm mt-1 dark:text-gray-400">
|
<p className="text-sm mt-1 dark:text-gray-400">
|
||||||
{translate('::App.DeveloperKit.CrudEndpoints.SelectTableDescription')}
|
{translate('::App.DeveloperKit.CrudEndpoints.SelectTableDescription')}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -504,38 +527,50 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<FaTable />
|
<FaTable />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-slate-900 dark:text-gray-100">{selectedTable.schemaName}.{selectedTable.tableName}</h4>
|
<h4 className="text-slate-900 dark:text-gray-100">
|
||||||
|
{selectedTable.schemaName}.{selectedTable.tableName}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{selectedTableEndpoints.length > 0 && (
|
{selectedTableEndpoints.length > 0 && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => handleDeleteAll(selectedTable)}
|
onClick={() => handleDeleteAll(selectedTable)}
|
||||||
disabled={deletingAll === selectedTable.fullName}
|
disabled={deletingAll === selectedTable.fullName}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 text-sm text-red-600 border border-red-200 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50 transition-colors"
|
variant="plain"
|
||||||
>
|
shape="none"
|
||||||
{deletingAll === selectedTable.fullName ? (
|
icon={
|
||||||
|
deletingAll === selectedTable.fullName ? (
|
||||||
<FaSyncAlt className="animate-spin" />
|
<FaSyncAlt className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg border border-red-200 !px-3 !py-1.5 text-sm text-red-600 transition-colors hover:!bg-red-50 disabled:opacity-50 dark:border-red-700 dark:hover:!bg-red-900/20"
|
||||||
|
>
|
||||||
{translate('::App.DeveloperKit.CrudEndpoints.DeleteAll')}
|
{translate('::App.DeveloperKit.CrudEndpoints.DeleteAll')}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => handleGenerate(selectedTable)}
|
onClick={() => handleGenerate(selectedTable)}
|
||||||
disabled={generatingFor === selectedTable.fullName}
|
disabled={generatingFor === selectedTable.fullName}
|
||||||
className="flex items-center gap-2 px-4 py-1.5 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800 disabled:opacity-50 transition-colors"
|
variant="solid"
|
||||||
>
|
color="blue-600"
|
||||||
{generatingFor === selectedTable.fullName ? (
|
icon={
|
||||||
|
generatingFor === selectedTable.fullName ? (
|
||||||
<FaSyncAlt className="animate-spin" />
|
<FaSyncAlt className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<FaBolt />
|
<FaBolt />
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg !px-4 !py-1.5 text-sm font-medium text-white transition-colors hover:!bg-blue-700 disabled:opacity-50 dark:!bg-blue-700 dark:hover:!bg-blue-800"
|
||||||
|
>
|
||||||
{selectedTableEndpoints.length > 0
|
{selectedTableEndpoints.length > 0
|
||||||
? translate('::App.DeveloperKit.CrudEndpoints.Regenerate')
|
? translate('::App.DeveloperKit.CrudEndpoints.Regenerate')
|
||||||
: translate('::App.DeveloperKit.CrudEndpoints.CreateCrudEndpoint')}
|
: translate('::App.DeveloperKit.CrudEndpoints.CreateCrudEndpoint')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -544,7 +579,9 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{selectedTableEndpoints.length === 0 ? (
|
{selectedTableEndpoints.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-slate-400 dark:text-gray-500">
|
<div className="flex flex-col items-center justify-center py-16 text-slate-400 dark:text-gray-500">
|
||||||
<FaBolt className="text-3xl mb-3 text-slate-200 dark:text-gray-700" />
|
<FaBolt className="text-3xl mb-3 text-slate-200 dark:text-gray-700" />
|
||||||
<p className="font-medium dark:text-gray-300">{translate('::App.DeveloperKit.CrudEndpoints.NoEndpointsYet')}</p>
|
<p className="font-medium dark:text-gray-300">
|
||||||
|
{translate('::App.DeveloperKit.CrudEndpoints.NoEndpointsYet')}
|
||||||
|
</p>
|
||||||
<p className="text-sm mt-1 dark:text-gray-400">
|
<p className="text-sm mt-1 dark:text-gray-400">
|
||||||
{translate('::App.DeveloperKit.CrudEndpoints.ClickToCreate')}
|
{translate('::App.DeveloperKit.CrudEndpoints.ClickToCreate')}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -562,11 +599,18 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{/* Endpoint row */}
|
{/* Endpoint row */}
|
||||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-900 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors">
|
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-900 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors">
|
||||||
{/* Toggle */}
|
{/* Toggle */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => handleToggle(ep.id)}
|
onClick={() => handleToggle(ep.id)}
|
||||||
disabled={togglingId === ep.id}
|
disabled={togglingId === ep.id}
|
||||||
title={ep.isActive ? translate('::App.DeveloperKit.CrudEndpoints.Disable') : translate('::App.DeveloperKit.CrudEndpoints.Enable')}
|
title={
|
||||||
className={`flex-shrink-0 text-xl transition-colors ${
|
ep.isActive
|
||||||
|
? translate('::App.DeveloperKit.CrudEndpoints.Disable')
|
||||||
|
: translate('::App.DeveloperKit.CrudEndpoints.Enable')
|
||||||
|
}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!h-8 !w-8 flex-shrink-0 !rounded !p-0 text-xl transition-colors hover:!bg-transparent ${
|
||||||
ep.isActive
|
ep.isActive
|
||||||
? 'text-green-500 hover:text-green-700'
|
? 'text-green-500 hover:text-green-700'
|
||||||
: 'text-slate-300 hover:text-slate-500'
|
: 'text-slate-300 hover:text-slate-500'
|
||||||
|
|
@ -579,7 +623,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<FaToggleOff />
|
<FaToggleOff />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Method badge */}
|
{/* Method badge */}
|
||||||
<span
|
<span
|
||||||
|
|
@ -602,9 +646,12 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expand */}
|
{/* Expand */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}
|
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}
|
||||||
className="p-1.5 text-slate-400 dark:text-gray-400 hover:text-slate-700 dark:hover:text-gray-200 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-7 !w-7 !rounded !p-0 text-slate-400 transition-colors hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
title={translate('::App.DeveloperKit.CrudEndpoints.TestDetails')}
|
title={translate('::App.DeveloperKit.CrudEndpoints.TestDetails')}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
|
|
@ -612,7 +659,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<FaChevronRight className="text-xs" />
|
<FaChevronRight className="text-xs" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expanded detail + test */}
|
{/* Expanded detail + test */}
|
||||||
|
|
@ -680,20 +727,28 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
|
|
||||||
{/* Test button */}
|
{/* Test button */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => testEndpoint(ep)}
|
onClick={() => testEndpoint(ep)}
|
||||||
disabled={loadingEndpoints.has(ep.id)}
|
disabled={loadingEndpoints.has(ep.id)}
|
||||||
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-1.5 text-sm rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
variant="solid"
|
||||||
>
|
color="blue-600"
|
||||||
{loadingEndpoints.has(ep.id) ? (
|
icon={
|
||||||
|
loadingEndpoints.has(ep.id) ? (
|
||||||
<FaSyncAlt className="animate-spin" />
|
<FaSyncAlt className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<FaPaperPlane />
|
<FaPaperPlane />
|
||||||
)}
|
)
|
||||||
{loadingEndpoints.has(ep.id) ? translate('::App.DeveloperKit.CrudEndpoints.Sending') : translate('::App.DeveloperKit.CrudEndpoints.Test')}
|
}
|
||||||
</button>
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg !px-4 !py-1.5 text-sm text-white transition-colors hover:!bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loadingEndpoints.has(ep.id)
|
||||||
|
? translate('::App.DeveloperKit.CrudEndpoints.Sending')
|
||||||
|
: translate('::App.DeveloperKit.CrudEndpoints.Test')}
|
||||||
|
</Button>
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setTestResults((prev) => {
|
setTestResults((prev) => {
|
||||||
const n = { ...prev }
|
const n = { ...prev }
|
||||||
|
|
@ -701,10 +756,12 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
return n
|
return n
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="text-xs text-slate-500 hover:text-slate-700 px-2 py-1.5"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !rounded !px-2 !py-1.5 text-xs text-slate-500 hover:!bg-transparent hover:text-slate-700"
|
||||||
>
|
>
|
||||||
{translate('::App.DeveloperKit.CrudEndpoints.Clear')}
|
{translate('::App.DeveloperKit.CrudEndpoints.Clear')}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -738,7 +795,8 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
2,
|
2,
|
||||||
)}
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
|
|
@ -748,10 +806,11 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="absolute top-1.5 right-1.5 p-1 text-slate-400 dark:text-gray-400 hover:text-slate-700 dark:hover:text-gray-200"
|
variant="plain"
|
||||||
>
|
shape="none"
|
||||||
<FaCopy className="text-xs" />
|
icon={<FaCopy className="text-xs" />}
|
||||||
</button>
|
className="absolute right-1.5 top-1.5 !h-6 !w-6 !rounded !p-0 text-slate-400 hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -760,13 +819,19 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{ep.csharpCode && (
|
{ep.csharpCode && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<p className="text-xs font-semibold text-slate-600 dark:text-gray-300">{translate('::App.DeveloperKit.CrudEndpoints.CsharpCode')}</p>
|
<p className="text-xs font-semibold text-slate-600 dark:text-gray-300">
|
||||||
<button
|
{translate('::App.DeveloperKit.CrudEndpoints.CsharpCode')}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => navigator.clipboard.writeText(ep.csharpCode)}
|
onClick={() => navigator.clipboard.writeText(ep.csharpCode)}
|
||||||
className="text-xs text-slate-400 dark:text-gray-400 hover:text-slate-700 dark:hover:text-gray-200 flex items-center gap-1"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaCopy />}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-1 whitespace-nowrap !rounded-none !px-0 !py-0 text-xs text-slate-400 hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
>
|
>
|
||||||
<FaCopy /> {translate('::App.DeveloperKit.CrudEndpoints.Copy')}
|
{translate('::App.DeveloperKit.CrudEndpoints.Copy')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="text-xs bg-slate-800 dark:bg-gray-900 text-green-300 rounded-lg p-3 overflow-x-auto max-h-48 font-mono">
|
<pre className="text-xs bg-slate-800 dark:bg-gray-900 text-green-300 rounded-lg p-3 overflow-x-auto max-h-48 font-mono">
|
||||||
{ep.csharpCode}
|
{ep.csharpCode}
|
||||||
|
|
@ -787,9 +852,13 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<div className="border-t border-slate-200 dark:border-gray-700 px-2 py-1 bg-slate-50 dark:bg-gray-800 flex items-center gap-4 text-xs text-slate-500 dark:text-gray-400">
|
<div className="border-t border-slate-200 dark:border-gray-700 px-2 py-1 bg-slate-50 dark:bg-gray-800 flex items-center gap-4 text-xs text-slate-500 dark:text-gray-400">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<FaCheckCircle className="text-green-400" />
|
<FaCheckCircle className="text-green-400" />
|
||||||
{selectedTableEndpoints.filter((e) => e.isActive).length} {translate('::App.DeveloperKit.CrudEndpoints.ActiveCount')}
|
{selectedTableEndpoints.filter((e) => e.isActive).length}{' '}
|
||||||
|
{translate('::App.DeveloperKit.CrudEndpoints.ActiveCount')}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{selectedTableEndpoints.filter((e) => !e.isActive).length}{' '}
|
||||||
|
{translate('::App.DeveloperKit.CrudEndpoints.InactiveCount')}
|
||||||
</span>
|
</span>
|
||||||
<span>{selectedTableEndpoints.filter((e) => !e.isActive).length} {translate('::App.DeveloperKit.CrudEndpoints.InactiveCount')}</span>
|
|
||||||
<span className="ml-auto">
|
<span className="ml-auto">
|
||||||
{translate('::App.DeveloperKit.CrudEndpoints.EndpointSummary')}
|
{translate('::App.DeveloperKit.CrudEndpoints.EndpointSummary')}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
const defaultTemplate = `using System;
|
const defaultTemplate = `using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -227,43 +228,55 @@ const DynamicServiceEditor: React.FC = () => {
|
||||||
{translate('::App.DeveloperKit.DynamicServices.Editor.BackToServices')}
|
{translate('::App.DeveloperKit.DynamicServices.Editor.BackToServices')}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={copyCode}
|
onClick={copyCode}
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-slate-300 dark:border-gray-700 rounded-lg text-slate-600 dark:text-gray-300 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaCopy className="h-3.5 w-3.5" />}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg border border-slate-300 px-4 py-2 text-slate-600 transition-colors hover:!bg-slate-50 dark:border-gray-700 dark:text-gray-300 dark:hover:!bg-gray-800"
|
||||||
>
|
>
|
||||||
<FaCopy className="w-3.5 h-3.5" />
|
|
||||||
{translate('::App.DeveloperKit.DynamicServices.Editor.CopyCode')}
|
{translate('::App.DeveloperKit.DynamicServices.Editor.CopyCode')}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={handleTestCompile}
|
onClick={handleTestCompile}
|
||||||
disabled={isCompiling || !code.trim()}
|
disabled={isCompiling || !code.trim()}
|
||||||
className="flex items-center gap-2 bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600 dark:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
>
|
icon={
|
||||||
{isCompiling ? (
|
isCompiling ? (
|
||||||
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
|
<FaSpinner className="h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<FaPlay className="w-3.5 h-3.5" />
|
<FaPlay className="h-3.5 w-3.5" />
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg !bg-orange-500 px-4 py-2 text-white transition-colors hover:!bg-orange-600 disabled:cursor-not-allowed disabled:opacity-50 dark:!bg-orange-700"
|
||||||
|
>
|
||||||
{isCompiling
|
{isCompiling
|
||||||
? translate('::App.DeveloperKit.DynamicServices.Editor.Compiling')
|
? translate('::App.DeveloperKit.DynamicServices.Editor.Compiling')
|
||||||
: translate('::App.DeveloperKit.DynamicServices.Editor.TestCompile')}
|
: translate('::App.DeveloperKit.DynamicServices.Editor.TestCompile')}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:bg-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
>
|
color="emerald-600"
|
||||||
{isPublishing ? (
|
icon={
|
||||||
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
|
isPublishing ? (
|
||||||
|
<FaSpinner className="h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<FaSave className="w-3.5 h-3.5" />
|
<FaSave className="h-3.5 w-3.5" />
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg px-4 py-2 text-white transition-colors hover:!bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50 dark:!bg-emerald-800"
|
||||||
|
>
|
||||||
{isPublishing
|
{isPublishing
|
||||||
? translate('::App.DeveloperKit.DynamicServices.Editor.Publishing')
|
? translate('::App.DeveloperKit.DynamicServices.Editor.Publishing')
|
||||||
: translate('::App.DeveloperKit.DynamicServices.Editor.Publish')}
|
: translate('::App.DeveloperKit.DynamicServices.Editor.Publish')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { dynamicServiceService, type DynamicServiceDto } from '@/services/dynami
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
const DynamicServiceManager: React.FC = () => {
|
const DynamicServiceManager: React.FC = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -160,13 +161,16 @@ const DynamicServiceManager: React.FC = () => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-auto">
|
<div className="w-full sm:w-auto">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={openSwagger}
|
onClick={openSwagger}
|
||||||
className="w-full sm:w-auto justify-center flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800 transition-colors"
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
icon={<FaExternalLinkAlt className="h-3 w-3" />}
|
||||||
|
className="w-full !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg px-4 py-2 text-white transition-colors hover:!bg-green-700 dark:!bg-green-700 dark:hover:!bg-green-800 sm:w-auto"
|
||||||
>
|
>
|
||||||
<FaExternalLinkAlt className="w-3 h-3" />
|
|
||||||
Swagger
|
Swagger
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-auto">
|
<div className="w-full sm:w-auto">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -244,13 +248,15 @@ const DynamicServiceManager: React.FC = () => {
|
||||||
>
|
>
|
||||||
<FaRegEdit className="w-4 h-4" />
|
<FaRegEdit className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => deleteService(service.id)}
|
onClick={() => deleteService(service.id)}
|
||||||
className="p-2 text-slate-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-gray-800 rounded transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaTrashAlt className="h-4 w-4" />}
|
||||||
|
className="!h-8 !w-8 !rounded !p-0 text-slate-500 transition-colors hover:!bg-red-50 hover:text-red-600 dark:text-gray-400 dark:hover:!bg-gray-800 dark:hover:text-red-400"
|
||||||
title={translate('::App.DeveloperKit.DynamicServices.DeleteTooltip')}
|
title={translate('::App.DeveloperKit.DynamicServices.DeleteTooltip')}
|
||||||
>
|
/>
|
||||||
<FaTrashAlt className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-man
|
||||||
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
||||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
type FolderKey = 'tables' | 'views' | 'procedures' | 'functions'
|
type FolderKey = 'tables' | 'views' | 'procedures' | 'functions'
|
||||||
|
|
||||||
|
|
@ -76,7 +77,10 @@ const SqlObjectExplorer = ({
|
||||||
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
|
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
|
||||||
const [dropping, setDropping] = useState(false)
|
const [dropping, setDropping] = useState(false)
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
show: boolean; x: number; y: number; node: TreeNode | null
|
show: boolean
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
node: TreeNode | null
|
||||||
}>({ show: false, x: 0, y: 0, node: null })
|
}>({ show: false, x: 0, y: 0, node: null })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -108,7 +112,8 @@ const SqlObjectExplorer = ({
|
||||||
data: obj,
|
data: obj,
|
||||||
})
|
})
|
||||||
|
|
||||||
const tree: TreeNode[] = [{
|
const tree: TreeNode[] = [
|
||||||
|
{
|
||||||
id: 'root',
|
id: 'root',
|
||||||
label: dataSource,
|
label: dataSource,
|
||||||
type: 'root',
|
type: 'root',
|
||||||
|
|
@ -148,7 +153,8 @@ const SqlObjectExplorer = ({
|
||||||
children: data.functions.map((f) => makeObjectNode('functions', f)),
|
children: data.functions.map((f) => makeObjectNode('functions', f)),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
|
|
||||||
setTreeData(tree)
|
setTreeData(tree)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -181,12 +187,17 @@ const SqlObjectExplorer = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNodeClick = (node: TreeNode) => {
|
const handleNodeClick = (node: TreeNode) => {
|
||||||
if (node.type !== 'object') { toggleNode(node.id); return }
|
if (node.type !== 'object') {
|
||||||
|
toggleNode(node.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (node.folder === 'tables') {
|
if (node.folder === 'tables') {
|
||||||
// Generate SELECT template for tables
|
// Generate SELECT template for tables
|
||||||
const t = node.data as DatabaseTableDto
|
const t = node.data as DatabaseTableDto
|
||||||
const fullName = t.fullName ?? (isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
|
const fullName =
|
||||||
|
t.fullName ??
|
||||||
|
(isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
|
||||||
onTemplateSelect?.(
|
onTemplateSelect?.(
|
||||||
isPostgreSql
|
isPostgreSql
|
||||||
? `SELECT *\nFROM ${fullName}\nLIMIT 10;`
|
? `SELECT *\nFROM ${fullName}\nLIMIT 10;`
|
||||||
|
|
@ -216,11 +227,7 @@ const SqlObjectExplorer = ({
|
||||||
|
|
||||||
const obj = node.data as SqlNativeObjectDto
|
const obj = node.data as SqlNativeObjectDto
|
||||||
const objectType =
|
const objectType =
|
||||||
node.folder === 'views'
|
node.folder === 'views' ? 'view' : node.folder === 'procedures' ? 'procedure' : 'function'
|
||||||
? 'view'
|
|
||||||
: node.folder === 'procedures'
|
|
||||||
? 'procedure'
|
|
||||||
: 'function'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
|
|
@ -260,15 +267,19 @@ const SqlObjectExplorer = ({
|
||||||
const buildDropSql = (node: TreeNode): string => {
|
const buildDropSql = (node: TreeNode): string => {
|
||||||
if (node.folder === 'tables') {
|
if (node.folder === 'tables') {
|
||||||
const t = node.data as DatabaseTableDto
|
const t = node.data as DatabaseTableDto
|
||||||
const fullName = t.fullName ?? (isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
|
const fullName =
|
||||||
|
t.fullName ??
|
||||||
|
(isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
|
||||||
return isPostgreSql ? `DROP TABLE IF EXISTS ${fullName};` : `DROP TABLE ${fullName};`
|
return isPostgreSql ? `DROP TABLE IF EXISTS ${fullName};` : `DROP TABLE ${fullName};`
|
||||||
}
|
}
|
||||||
const obj = node.data as SqlNativeObjectDto
|
const obj = node.data as SqlNativeObjectDto
|
||||||
const fullName = obj.fullName ?? (isPostgreSql ? `"${obj.schemaName}"."${obj.objectName}"` : `[${obj.schemaName}].[${obj.objectName}]`)
|
const fullName =
|
||||||
|
obj.fullName ??
|
||||||
|
(isPostgreSql
|
||||||
|
? `"${obj.schemaName}"."${obj.objectName}"`
|
||||||
|
: `[${obj.schemaName}].[${obj.objectName}]`)
|
||||||
const keyword =
|
const keyword =
|
||||||
node.folder === 'views' ? 'VIEW' :
|
node.folder === 'views' ? 'VIEW' : node.folder === 'procedures' ? 'PROCEDURE' : 'FUNCTION'
|
||||||
node.folder === 'procedures' ? 'PROCEDURE' :
|
|
||||||
'FUNCTION'
|
|
||||||
return isPostgreSql ? `DROP ${keyword} IF EXISTS ${fullName};` : `DROP ${keyword} ${fullName};`
|
return isPostgreSql ? `DROP ${keyword} IF EXISTS ${fullName};` : `DROP ${keyword} ${fullName};`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,9 +328,7 @@ const SqlObjectExplorer = ({
|
||||||
if (node.type === 'folder') {
|
if (node.type === 'folder') {
|
||||||
const open = expandedNodes.has(node.id)
|
const open = expandedNodes.has(node.id)
|
||||||
const cls = FOLDER_META[node.folder!]?.color ?? 'text-blue-500'
|
const cls = FOLDER_META[node.folder!]?.color ?? 'text-blue-500'
|
||||||
return open
|
return open ? <FaRegFolderOpen className={cls} /> : <FaRegFolder className={cls} />
|
||||||
? <FaRegFolderOpen className={cls} />
|
|
||||||
: <FaRegFolder className={cls} />
|
|
||||||
}
|
}
|
||||||
if (node.folder === 'tables') return <FaTable className="text-teal-500" />
|
if (node.folder === 'tables') return <FaTable className="text-teal-500" />
|
||||||
if (node.folder === 'views') return <FaEye className="text-purple-500" />
|
if (node.folder === 'views') return <FaEye className="text-purple-500" />
|
||||||
|
|
@ -337,7 +346,10 @@ const SqlObjectExplorer = ({
|
||||||
className="group flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
className="group flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||||
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
||||||
onClick={() => handleNodeClick(node)}
|
onClick={() => handleNodeClick(node)}
|
||||||
onContextMenu={(e) => { e.preventDefault(); setContextMenu({ show: true, x: e.clientX, y: e.clientY, node }) }}
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setContextMenu({ show: true, x: e.clientX, y: e.clientY, node })
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{node.type === 'object' && (
|
{node.type === 'object' && (
|
||||||
<input
|
<input
|
||||||
|
|
@ -351,13 +363,18 @@ const SqlObjectExplorer = ({
|
||||||
{getIcon(node)}
|
{getIcon(node)}
|
||||||
<span className="text-sm flex-1 truncate">{node.label}</span>
|
<span className="text-sm flex-1 truncate">{node.label}</span>
|
||||||
{node.type === 'object' && (
|
{node.type === 'object' && (
|
||||||
<button
|
<Button
|
||||||
title="Drop"
|
title="Drop"
|
||||||
className="opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-red-100 dark:hover:bg-red-900 text-red-500 transition-opacity flex-shrink-0"
|
size="xs"
|
||||||
onClick={(e) => { e.stopPropagation(); setDropConfirm({ node }) }}
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTrash className="text-xs" />
|
icon={<FaTrash className="text-xs" />}
|
||||||
</button>
|
className="opacity-0 group-hover:opacity-100 !h-6 !w-6 !px-0 hover:!bg-red-100 dark:hover:!bg-red-900 text-red-500 transition-opacity flex-shrink-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setDropConfirm({ node })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isExpanded && node.children && (
|
{isExpanded && node.children && (
|
||||||
|
|
@ -406,22 +423,38 @@ const SqlObjectExplorer = ({
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
className="flex-1 px-3 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-gray-100"
|
className="flex-1 px-3 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={loadObjects}
|
onClick={loadObjects}
|
||||||
disabled={loading || !dataSource}
|
disabled={loading || !dataSource}
|
||||||
className="px-3 py-1.5 text-sm border rounded-md border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaSyncAlt className={loading ? 'animate-spin' : ''} />}
|
||||||
|
className="!rounded-md border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||||||
title={translate('::App.Platform.Refresh')}
|
title={translate('::App.Platform.Refresh')}
|
||||||
>
|
/>
|
||||||
<FaSyncAlt className={loading ? 'animate-spin' : ''} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tree */}
|
{/* Tree */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{loading && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.Loading')}</div>}
|
{loading && (
|
||||||
{!loading && treeData.length === 0 && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.NoDataSourceSelected')}</div>}
|
<div className="text-center py-8 text-gray-500 text-sm">
|
||||||
{!loading && filteredTree.length > 0 && <div className="p-1">{filteredTree.map((n) => renderNode(n))}</div>}
|
{translate('::App.Platform.Loading')}
|
||||||
{!loading && treeData.length > 0 && filteredTree.length === 0 && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.NoResults')}</div>}
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && treeData.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-gray-500 text-sm">
|
||||||
|
{translate('::App.Platform.NoDataSourceSelected')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && filteredTree.length > 0 && (
|
||||||
|
<div className="p-1">{filteredTree.map((n) => renderNode(n))}</div>
|
||||||
|
)}
|
||||||
|
{!loading && treeData.length > 0 && filteredTree.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-gray-500 text-sm">
|
||||||
|
{translate('::App.Platform.NoResults')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Context menu */}
|
{/* Context menu */}
|
||||||
|
|
@ -434,8 +467,10 @@ const SqlObjectExplorer = ({
|
||||||
>
|
>
|
||||||
{/* TABLE object <20> Design */}
|
{/* TABLE object <20> Design */}
|
||||||
{isTableObj && (
|
{isTableObj && (
|
||||||
<button
|
<Button
|
||||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const t = ctxNode!.data as DatabaseTableDto
|
const t = ctxNode!.data as DatabaseTableDto
|
||||||
onGenerateTableScript?.(t.schemaName, t.tableName)
|
onGenerateTableScript?.(t.schemaName, t.tableName)
|
||||||
|
|
@ -443,12 +478,14 @@ const SqlObjectExplorer = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaCode className="text-blue-600" /> Script olustur
|
<FaCode className="text-blue-600" /> Script olustur
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isTableObj && (
|
{isTableObj && (
|
||||||
<button
|
<Button
|
||||||
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const t = ctxNode!.data as DatabaseTableDto
|
const t = ctxNode!.data as DatabaseTableDto
|
||||||
onDesignTable?.(t.schemaName, t.tableName)
|
onDesignTable?.(t.schemaName, t.tableName)
|
||||||
|
|
@ -456,53 +493,92 @@ const SqlObjectExplorer = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaTable className="text-teal-600" /> Design Table
|
<FaTable className="text-teal-600" /> Design Table
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* NATIVE object <20> View Definition */}
|
{/* NATIVE object <20> View Definition */}
|
||||||
{isNativeObj && (
|
{isNativeObj && (
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const obj = ctxNode!.data as SqlNativeObjectDto
|
const obj = ctxNode!.data as SqlNativeObjectDto
|
||||||
onViewDefinition?.(obj.schemaName, obj.objectName)
|
onViewDefinition?.(obj.schemaName, obj.objectName)
|
||||||
closeCtx()
|
closeCtx()
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{ctxNode!.folder === 'views' && <FaEye className="text-purple-500" />}
|
{ctxNode!.folder === 'views' && <FaEye className="text-purple-500" />}
|
||||||
{ctxNode!.folder === 'procedures' && <FaCog className="text-green-600" />}
|
{ctxNode!.folder === 'procedures' && <FaCog className="text-green-600" />}
|
||||||
{ctxNode!.folder === 'functions' && <FaCode className="text-orange-500" />}
|
{ctxNode!.folder === 'functions' && <FaCode className="text-orange-500" />}
|
||||||
View Definition
|
View Definition
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* FOLDER <20> New ... */}
|
{/* FOLDER <20> New ... */}
|
||||||
{isTablesDir && (
|
{isTablesDir && (
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
onClick={() => { onNewTable?.(); closeCtx() }}>
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
onNewTable?.()
|
||||||
|
closeCtx()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FaPlus className="text-teal-600" /> New Table
|
<FaPlus className="text-teal-600" /> New Table
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isViewsDir && (
|
{isViewsDir && (
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
onClick={() => { onTemplateSelect?.('', 'create-view'); closeCtx() }}>
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
onTemplateSelect?.('', 'create-view')
|
||||||
|
closeCtx()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FaPlus className="text-purple-500" /> New View
|
<FaPlus className="text-purple-500" /> New View
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isProcsDir && (
|
{isProcsDir && (
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
onClick={() => { onTemplateSelect?.('', 'create-procedure'); closeCtx() }}>
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
onTemplateSelect?.('', 'create-procedure')
|
||||||
|
closeCtx()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FaPlus className="text-green-600" /> New Stored Procedure
|
<FaPlus className="text-green-600" /> New Stored Procedure
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isFuncsDir && (
|
{isFuncsDir && (
|
||||||
<>
|
<>
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
onClick={() => { onTemplateSelect?.('', 'create-scalar-function'); closeCtx() }}>
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
onTemplateSelect?.('', 'create-scalar-function')
|
||||||
|
closeCtx()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FaPlus className="text-orange-500" /> New Scalar Function
|
<FaPlus className="text-orange-500" /> New Scalar Function
|
||||||
</button>
|
</Button>
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
onClick={() => { onTemplateSelect?.('', 'create-table-function'); closeCtx() }}>
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
onTemplateSelect?.('', 'create-table-function')
|
||||||
|
closeCtx()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FaPlus className="text-orange-500" /> New Table-Valued Function
|
<FaPlus className="text-orange-500" /> New Table-Valued Function
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -510,10 +586,17 @@ const SqlObjectExplorer = ({
|
||||||
{isFolderNode && (
|
{isFolderNode && (
|
||||||
<>
|
<>
|
||||||
<div className="my-1 border-t border-gray-100 dark:border-gray-700" />
|
<div className="my-1 border-t border-gray-100 dark:border-gray-700" />
|
||||||
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
<Button
|
||||||
onClick={() => { loadObjects(); closeCtx() }}>
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
loadObjects()
|
||||||
|
closeCtx()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<FaSyncAlt /> {translate('::App.Platform.Refresh')}
|
<FaSyncAlt /> {translate('::App.Platform.Refresh')}
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -523,8 +606,14 @@ const SqlObjectExplorer = ({
|
||||||
{/* Drop Confirm Dialog */}
|
{/* Drop Confirm Dialog */}
|
||||||
{dropConfirm && (
|
{dropConfirm && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center" onClick={() => !dropping && setDropConfirm(null)}>
|
<div
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full mx-4" onClick={(e) => e.stopPropagation()}>
|
className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
|
||||||
|
onClick={() => !dropping && setDropConfirm(null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full mx-4"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
|
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
|
||||||
<h6 className="font-semibold text-gray-900 dark:text-gray-100">Drop Object</h6>
|
<h6 className="font-semibold text-gray-900 dark:text-gray-100">Drop Object</h6>
|
||||||
|
|
@ -536,21 +625,26 @@ const SqlObjectExplorer = ({
|
||||||
{buildDropSql(dropConfirm.node)}
|
{buildDropSql(dropConfirm.node)}
|
||||||
</code>
|
</code>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<Button
|
||||||
disabled={dropping}
|
disabled={dropping}
|
||||||
className="px-4 py-1.5 text-sm rounded border hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
className="disabled:opacity-50"
|
||||||
onClick={() => setDropConfirm(null)}
|
onClick={() => setDropConfirm(null)}
|
||||||
>
|
>
|
||||||
Cancel
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
disabled={dropping}
|
disabled={dropping}
|
||||||
className="px-4 py-1.5 text-sm rounded bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 flex items-center gap-2"
|
size="sm"
|
||||||
|
variant="solid"
|
||||||
|
color="red-600"
|
||||||
|
className="hover:!bg-red-700 disabled:opacity-50 flex items-center gap-2"
|
||||||
onClick={handleDrop}
|
onClick={handleDrop}
|
||||||
>
|
>
|
||||||
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
|
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
|
||||||
Drop
|
Drop
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1629,28 +1629,32 @@ GO`,
|
||||||
|
|
||||||
{/* Mode Tabs */}
|
{/* Mode Tabs */}
|
||||||
<div className="flex gap-2 mb-2 border-b flex-shrink-0">
|
<div className="flex gap-2 mb-2 border-b flex-shrink-0">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setCopyDialogMode('objects')}
|
onClick={() => setCopyDialogMode('objects')}
|
||||||
className={`px-4 py-2 font-medium text-sm border-b-2 transition ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!h-auto !rounded-none border-b-2 !px-4 !py-2 text-sm font-medium transition hover:!bg-transparent active:!bg-transparent focus:!bg-transparent ${
|
||||||
copyDialogMode === 'objects'
|
copyDialogMode === 'objects'
|
||||||
? 'border-blue-500 text-blue-600'
|
? 'border-blue-500 !text-blue-600'
|
||||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
disabled={isCopyingObjects}
|
disabled={isCopyingObjects}
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.CopyObjects') || 'Nesneleri Kopyala'}
|
{translate('::App.Platform.CopyObjects') || 'Nesneleri Kopyala'}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setCopyDialogMode('sql')}
|
onClick={() => setCopyDialogMode('sql')}
|
||||||
className={`px-4 py-2 font-medium text-sm border-b-2 transition ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!h-auto !rounded-none border-b-2 !px-4 !py-2 text-sm font-medium transition hover:!bg-transparent active:!bg-transparent focus:!bg-transparent ${
|
||||||
copyDialogMode === 'sql'
|
copyDialogMode === 'sql'
|
||||||
? 'border-blue-500 text-blue-600'
|
? 'border-blue-500 !text-blue-600'
|
||||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
disabled={isCopyingObjects}
|
disabled={isCopyingObjects}
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.DirectSqlScript') || 'Direkt SQL Script'}
|
{translate('::App.Platform.DirectSqlScript') || 'Direkt SQL Script'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1">
|
<div className="flex-1 min-h-0 overflow-y-auto pr-1">
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,11 @@ const EMPTY_INDEX: Omit<TableIndex, 'id'> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const INDEX_TYPES: { value: IndexType; label: string; desc: string }[] = [
|
const INDEX_TYPES: { value: IndexType; label: string; desc: string }[] = [
|
||||||
{ value: 'PrimaryKey', label: 'Primary Key', desc: 'App.SqlQueryManager.IndexType_PrimaryKey_Desc' },
|
{
|
||||||
|
value: 'PrimaryKey',
|
||||||
|
label: 'Primary Key',
|
||||||
|
desc: 'App.SqlQueryManager.IndexType_PrimaryKey_Desc',
|
||||||
|
},
|
||||||
{ value: 'UniqueKey', label: 'Unique Key', desc: 'App.SqlQueryManager.IndexType_UniqueKey_Desc' },
|
{ value: 'UniqueKey', label: 'Unique Key', desc: 'App.SqlQueryManager.IndexType_UniqueKey_Desc' },
|
||||||
{ value: 'Index', label: 'Index', desc: 'App.SqlQueryManager.IndexType_Index_Desc' },
|
{ value: 'Index', label: 'Index', desc: 'App.SqlQueryManager.IndexType_Index_Desc' },
|
||||||
]
|
]
|
||||||
|
|
@ -345,7 +349,10 @@ function generateCreateTableSql(
|
||||||
}
|
}
|
||||||
} else if (allInnerLines.length > 0) {
|
} else if (allInnerLines.length > 0) {
|
||||||
// Remove trailing comma from last column if no PK
|
// Remove trailing comma from last column if no PK
|
||||||
allInnerLines[allInnerLines.length - 1] = allInnerLines[allInnerLines.length - 1].replace(/,$/, '')
|
allInnerLines[allInnerLines.length - 1] = allInnerLines[allInnerLines.length - 1].replace(
|
||||||
|
/,$/,
|
||||||
|
'',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-PK index lines (UNIQUE, regular INDEX) — outside CREATE TABLE
|
// Non-PK index lines (UNIQUE, regular INDEX) — outside CREATE TABLE
|
||||||
|
|
@ -358,16 +365,13 @@ function generateCreateTableSql(
|
||||||
if (idx.indexType === 'UniqueKey') {
|
if (idx.indexType === 'UniqueKey') {
|
||||||
extraIndexLines.push(`-- 🔒 Unique Key: [${idx.indexName}]`)
|
extraIndexLines.push(`-- 🔒 Unique Key: [${idx.indexName}]`)
|
||||||
extraIndexLines.push(`ALTER TABLE ${fullTableName}`)
|
extraIndexLines.push(`ALTER TABLE ${fullTableName}`)
|
||||||
extraIndexLines.push(` ADD CONSTRAINT [${idx.indexName}] UNIQUE ${clustered} (${colsSql});`)
|
extraIndexLines.push(
|
||||||
|
` ADD CONSTRAINT [${idx.indexName}] UNIQUE ${clustered} (${colsSql});`,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
extraIndexLines.push(`-- 📋 Index: [${idx.indexName}]`)
|
extraIndexLines.push(`-- 📋 Index: [${idx.indexName}]`)
|
||||||
extraIndexLines.push(
|
extraIndexLines.push(
|
||||||
...buildCreateIndexIfNotExistsSql(
|
...buildCreateIndexIfNotExistsSql(fullTableName, idx.indexName, idx.isClustered, colsSql),
|
||||||
fullTableName,
|
|
||||||
idx.indexName,
|
|
||||||
idx.isClustered,
|
|
||||||
colsSql,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -499,7 +503,10 @@ function unwrapDefaultExpression(value: string): string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSqlTypeToDesigner(dataTypeRaw: string, lengthRaw: string): {
|
function mapSqlTypeToDesigner(
|
||||||
|
dataTypeRaw: string,
|
||||||
|
lengthRaw: string,
|
||||||
|
): {
|
||||||
dataType: SqlDataType
|
dataType: SqlDataType
|
||||||
maxLength: string
|
maxLength: string
|
||||||
} {
|
} {
|
||||||
|
|
@ -562,9 +569,7 @@ function parseCreateTableColumns(script: string): ColumnDefinition[] {
|
||||||
const trimmed = def.trim()
|
const trimmed = def.trim()
|
||||||
if (!trimmed) continue
|
if (!trimmed) continue
|
||||||
|
|
||||||
if (
|
if (/^(CONSTRAINT|PRIMARY\s+KEY|UNIQUE\s+KEY|UNIQUE|FOREIGN\s+KEY|CHECK)\b/i.test(trimmed)) {
|
||||||
/^(CONSTRAINT|PRIMARY\s+KEY|UNIQUE\s+KEY|UNIQUE|FOREIGN\s+KEY|CHECK)\b/i.test(trimmed)
|
|
||||||
) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -780,12 +785,7 @@ function generateAlterTableSql(
|
||||||
} else {
|
} else {
|
||||||
lines.push(`-- 📋 Index: [${ix.indexName}]`)
|
lines.push(`-- 📋 Index: [${ix.indexName}]`)
|
||||||
lines.push(
|
lines.push(
|
||||||
...buildCreateIndexIfNotExistsSql(
|
...buildCreateIndexIfNotExistsSql(fullTableName, ix.indexName, ix.isClustered, colsSql),
|
||||||
fullTableName,
|
|
||||||
ix.indexName,
|
|
||||||
ix.isClustered,
|
|
||||||
colsSql,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
lines.push('')
|
lines.push('')
|
||||||
|
|
@ -830,15 +830,17 @@ function generateAlterTableSql(
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'Index / Key', 'İlişkiler', 'T-SQL Önizleme'] as const
|
const STEPS = [
|
||||||
|
'Sütun Tasarımı',
|
||||||
|
'Entity Ayarları',
|
||||||
|
'Index / Key',
|
||||||
|
'İlişkiler',
|
||||||
|
'T-SQL Önizleme',
|
||||||
|
] as const
|
||||||
type Step = 0 | 1 | 2 | 3 | 4
|
type Step = 0 | 1 | 2 | 3 | 4
|
||||||
|
|
||||||
function StepContentWrapper({ children }: { children: React.ReactNode }) {
|
function StepContentWrapper({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <div className="min-h-[420px] flex flex-col">{children}</div>
|
||||||
<div className="min-h-[420px] flex flex-col">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
||||||
|
|
@ -1321,9 +1323,7 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
setColumns((prev) => {
|
setColumns((prev) => {
|
||||||
const existingNonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
const existingNonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
||||||
const existingNames = new Set(
|
const existingNames = new Set(existingNonEmpty.map((c) => c.columnName.trim().toLowerCase()))
|
||||||
existingNonEmpty.map((c) => c.columnName.trim().toLowerCase()),
|
|
||||||
)
|
|
||||||
|
|
||||||
const toAdd: ColumnDefinition[] = []
|
const toAdd: ColumnDefinition[] = []
|
||||||
for (const col of parsedColumns) {
|
for (const col of parsedColumns) {
|
||||||
|
|
@ -1433,13 +1433,7 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
setSettings((s) => ({ ...s, tableName: recalculatedTableName }))
|
setSettings((s) => ({ ...s, tableName: recalculatedTableName }))
|
||||||
syncAutoPkName(recalculatedTableName)
|
syncAutoPkName(recalculatedTableName)
|
||||||
}, [
|
}, [isEditMode, columns, settings.menuPrefix, settings.entityName, settings.tableName])
|
||||||
isEditMode,
|
|
||||||
columns,
|
|
||||||
settings.menuPrefix,
|
|
||||||
settings.entityName,
|
|
||||||
settings.tableName,
|
|
||||||
])
|
|
||||||
|
|
||||||
// ── FK Relationship handlers ───────────────────────────────────────────────
|
// ── FK Relationship handlers ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -1498,7 +1492,11 @@ const SqlTableDesignerDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFk = () => {
|
const saveFk = () => {
|
||||||
if (!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim() || !fkForm.referencedColumn.trim()) {
|
if (
|
||||||
|
!fkForm.fkColumnName.trim() ||
|
||||||
|
!fkForm.referencedTable.trim() ||
|
||||||
|
!fkForm.referencedColumn.trim()
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1906,29 +1904,35 @@ const SqlTableDesignerDialog = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 flex items-center justify-center gap-1">
|
<div className="col-span-1 flex items-center justify-center gap-1">
|
||||||
<button
|
<Button
|
||||||
onClick={() => moveColumn(col.id, 'up')}
|
onClick={() => moveColumn(col.id, 'up')}
|
||||||
disabled={idx === 0}
|
disabled={idx === 0}
|
||||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 text-gray-500"
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaArrowUp className="text-xs" />}
|
||||||
|
className="!h-6 !w-6 !px-0 hover:!bg-gray-100 dark:hover:!bg-gray-700 disabled:opacity-30 text-gray-500"
|
||||||
title={translate('::App.SqlQueryManager.MoveUp')}
|
title={translate('::App.SqlQueryManager.MoveUp')}
|
||||||
>
|
/>
|
||||||
<FaArrowUp className="text-xs" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => moveColumn(col.id, 'down')}
|
onClick={() => moveColumn(col.id, 'down')}
|
||||||
disabled={idx === columns.length - 1}
|
disabled={idx === columns.length - 1}
|
||||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 text-gray-500"
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaArrowDown className="text-xs" />}
|
||||||
|
className="!h-6 !w-6 !px-0 hover:!bg-gray-100 dark:hover:!bg-gray-700 disabled:opacity-30 text-gray-500"
|
||||||
title={translate('::App.SqlQueryManager.MoveDown')}
|
title={translate('::App.SqlQueryManager.MoveDown')}
|
||||||
>
|
/>
|
||||||
<FaArrowDown className="text-xs" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => removeColumn(col.id)}
|
onClick={() => removeColumn(col.id)}
|
||||||
className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 text-red-500"
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTrash className="text-xs" />}
|
||||||
|
className="!h-6 !w-6 !px-0 hover:!bg-red-50 dark:hover:!bg-red-900/30 text-red-500"
|
||||||
title={translate('::App.SqlQueryManager.Delete')}
|
title={translate('::App.SqlQueryManager.Delete')}
|
||||||
>
|
/>
|
||||||
<FaTrash className="text-xs" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -1959,27 +1963,32 @@ const SqlTableDesignerDialog = ({
|
||||||
{translate('::App.SqlQueryManager.MenuName')} <span className="text-red-500">*</span>
|
{translate('::App.SqlQueryManager.MenuName')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isEditMode}
|
disabled={isEditMode}
|
||||||
onClick={() => setMenuAddDialogOpen(true)}
|
onClick={() => setMenuAddDialogOpen(true)}
|
||||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
color="green-500"
|
||||||
|
className="!h-auto !px-2 !py-0.5 hover:!bg-green-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<FaPlus className="text-xs" />{' '}
|
<FaPlus className="text-xs" />{' '}
|
||||||
{translate('::ListForms.Wizard.Step1.AddNewMenu') || 'Add Menu'}
|
{translate('::ListForms.Wizard.Step1.AddNewMenu') || 'Add Menu'}
|
||||||
</button>
|
</Button>
|
||||||
{settings.menuValue && !isEditMode && (
|
{settings.menuValue && !isEditMode && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedMenuCode('')
|
setSelectedMenuCode('')
|
||||||
setSettings((s) => ({ ...s, menuValue: '', menuPrefix: '', tableName: '' }))
|
setSettings((s) => ({ ...s, menuValue: '', menuPrefix: '', tableName: '' }))
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
className="!h-auto !px-2 !py-0.5 border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
|
||||||
>
|
>
|
||||||
<FaTimes className="text-xs" />{' '}
|
<FaTimes className="text-xs" />{' '}
|
||||||
{translate('::ListForms.Wizard.ClearSelection') || 'Clear'}
|
{translate('::ListForms.Wizard.ClearSelection') || 'Clear'}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2047,8 +2056,8 @@ const SqlTableDesignerDialog = ({
|
||||||
{/* Warning: no Id column */}
|
{/* Warning: no Id column */}
|
||||||
{!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
{!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
||||||
<div className="px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 rounded text-xs text-amber-700 dark:text-amber-300">
|
<div className="px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 rounded text-xs text-amber-700 dark:text-amber-300">
|
||||||
Full Audited kolonları seçmediğiniz için PK kolonu manuel tanımlanmalıdır. Bu kolon
|
Full Audited kolonları seçmediğiniz için PK kolonu manuel tanımlanmalıdır. Bu kolon int
|
||||||
int veya uniqueidentifier olabilir.
|
veya uniqueidentifier olabilir.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2074,13 +2083,16 @@ const SqlTableDesignerDialog = ({
|
||||||
? translate('::App.SqlQueryManager.NoRelationshipsDefined')
|
? translate('::App.SqlQueryManager.NoRelationshipsDefined')
|
||||||
: `${relationships.length} ${translate('::App.SqlQueryManager.Relationship')}`}
|
: `${relationships.length} ${translate('::App.SqlQueryManager.Relationship')}`}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
onClick={openAddFk}
|
onClick={openAddFk}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs rounded-lg transition-colors"
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
color="indigo-600"
|
||||||
|
className="!h-auto !px-3 !py-1.5 !rounded-lg hover:!bg-indigo-700 transition-colors"
|
||||||
>
|
>
|
||||||
<FaPlus className="w-2.5 h-2.5" />{' '}
|
<FaPlus className="w-2.5 h-2.5" />{' '}
|
||||||
{translate('::App.SqlQueryManager.AddRelationship')}
|
{translate('::App.SqlQueryManager.AddRelationship')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
|
|
@ -2137,20 +2149,24 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
<button
|
<Button
|
||||||
onClick={() => openEditFk(rel)}
|
onClick={() => openEditFk(rel)}
|
||||||
title={translate('::App.SqlQueryManager.Edit')}
|
title={translate('::App.SqlQueryManager.Edit')}
|
||||||
className="p-1.5 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors"
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaEdit className="w-3.5 h-3.5" />}
|
||||||
<button
|
className="!h-7 !w-7 !px-0 text-gray-400 hover:text-indigo-600 hover:!bg-indigo-50 dark:hover:!bg-indigo-900/20 transition-colors"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
onClick={() => deleteFk(rel.id)}
|
onClick={() => deleteFk(rel.id)}
|
||||||
title={translate('::App.SqlQueryManager.Delete')}
|
title={translate('::App.SqlQueryManager.Delete')}
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTrash className="w-3.5 h-3.5" />}
|
||||||
|
className="!h-7 !w-7 !px-0 text-gray-400 hover:text-red-600 hover:!bg-red-50 dark:hover:!bg-red-900/20 transition-colors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -2170,12 +2186,14 @@ const SqlTableDesignerDialog = ({
|
||||||
? translate('::App.SqlQueryManager.EditRelationship')
|
? translate('::App.SqlQueryManager.EditRelationship')
|
||||||
: translate('::App.SqlQueryManager.AddNewRelationship')}
|
: translate('::App.SqlQueryManager.AddNewRelationship')}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setFkModalOpen(false)}
|
onClick={() => setFkModalOpen(false)}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
size="sm"
|
||||||
>
|
variant="plain"
|
||||||
<FaTimes className="w-4 h-4" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
|
className="!h-8 !w-8 !px-0 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
|
|
@ -2187,11 +2205,13 @@ const SqlTableDesignerDialog = ({
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{REL_TYPES.map((t) => (
|
{REL_TYPES.map((t) => (
|
||||||
<button
|
<Button
|
||||||
key={t.value}
|
key={t.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFkForm((f) => ({ ...f, relationshipType: t.value }))}
|
onClick={() => setFkForm((f) => ({ ...f, relationshipType: t.value }))}
|
||||||
className={`flex-1 py-2 px-3 rounded-lg border text-sm font-medium transition-all ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`flex-1 !h-auto !rounded-lg border !py-2 !px-3 text-sm font-medium transition-all ${
|
||||||
fkForm.relationshipType === t.value
|
fkForm.relationshipType === t.value
|
||||||
? 'bg-indigo-600 border-indigo-600 text-white'
|
? 'bg-indigo-600 border-indigo-600 text-white'
|
||||||
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:border-indigo-300'
|
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:border-indigo-300'
|
||||||
|
|
@ -2199,7 +2219,7 @@ const SqlTableDesignerDialog = ({
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
<span className="block text-xs opacity-70">{t.desc}</span>
|
<span className="block text-xs opacity-70">{t.desc}</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2215,9 +2235,7 @@ const SqlTableDesignerDialog = ({
|
||||||
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
|
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">{translate('::App.Select')}</option>
|
||||||
{translate('::App.Select')}
|
|
||||||
</option>
|
|
||||||
{columns
|
{columns
|
||||||
.filter((c) => c.columnName.trim())
|
.filter((c) => c.columnName.trim())
|
||||||
.map((c) => (
|
.map((c) => (
|
||||||
|
|
@ -2240,9 +2258,7 @@ const SqlTableDesignerDialog = ({
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">{translate('::App.Select')}</option>
|
||||||
{translate('::App.Select')}
|
|
||||||
</option>
|
|
||||||
{dbTables.map((t) => (
|
{dbTables.map((t) => (
|
||||||
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
|
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
|
||||||
{t.tableName}
|
{t.tableName}
|
||||||
|
|
@ -2270,7 +2286,9 @@ const SqlTableDesignerDialog = ({
|
||||||
<option
|
<option
|
||||||
key={col}
|
key={col}
|
||||||
value={col}
|
value={col}
|
||||||
disabled={!targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())}
|
disabled={
|
||||||
|
!targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
|
{targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
|
||||||
? `${col} (PK/UNIQUE)`
|
? `${col} (PK/UNIQUE)`
|
||||||
|
|
@ -2278,7 +2296,9 @@ const SqlTableDesignerDialog = ({
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{fkForm.referencedTable && !targetColsLoading && targetTableKeyColumns.length === 0 && (
|
{fkForm.referencedTable &&
|
||||||
|
!targetColsLoading &&
|
||||||
|
targetTableKeyColumns.length === 0 && (
|
||||||
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
||||||
Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı.
|
Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -2362,13 +2382,15 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-2xl">
|
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-2xl">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setFkModalOpen(false)}
|
onClick={() => setFkModalOpen(false)}
|
||||||
className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
className="text-gray-600 dark:text-gray-300 hover:!bg-gray-100 dark:hover:!bg-gray-700 !rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={saveFk}
|
onClick={saveFk}
|
||||||
disabled={
|
disabled={
|
||||||
!fkForm.fkColumnName.trim() ||
|
!fkForm.fkColumnName.trim() ||
|
||||||
|
|
@ -2378,10 +2400,13 @@ const SqlTableDesignerDialog = ({
|
||||||
(c) => c.toLowerCase() === fkForm.referencedColumn.trim().toLowerCase(),
|
(c) => c.toLowerCase() === fkForm.referencedColumn.trim().toLowerCase(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
size="sm"
|
||||||
|
variant="solid"
|
||||||
|
color="indigo-600"
|
||||||
|
className="hover:!bg-indigo-700 disabled:opacity-50 !rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{translate('::App.SqlQueryManager.Save')}
|
{translate('::App.SqlQueryManager.Save')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
@ -2423,20 +2448,27 @@ const SqlTableDesignerDialog = ({
|
||||||
? translate('::App.SqlQueryManager.NoIndexesDefined')
|
? translate('::App.SqlQueryManager.NoIndexesDefined')
|
||||||
: `${indexes.length} ${translate('::App.SqlQueryManager.IndexKey')}`}
|
: `${indexes.length} ${translate('::App.SqlQueryManager.IndexKey')}`}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
onClick={openAddIndex}
|
onClick={openAddIndex}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs rounded-lg transition-colors"
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
color="indigo-600"
|
||||||
|
className="!h-auto !px-3 !py-1.5 !rounded-lg hover:!bg-indigo-700 transition-colors"
|
||||||
>
|
>
|
||||||
<FaPlus className="w-2.5 h-2.5" /> {translate('::App.SqlQueryManager.AddIndexKey')}
|
<FaPlus className="w-2.5 h-2.5" /> {translate('::App.SqlQueryManager.AddIndexKey')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
{indexes.length === 0 && (
|
{indexes.length === 0 && (
|
||||||
<div className="py-8 text-center border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/50">
|
<div className="py-8 text-center border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/50">
|
||||||
<FaKey className="text-3xl mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
<FaKey className="text-3xl mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
||||||
<p className="text-sm text-gray-500">{translate('::App.SqlQueryManager.NoIndexesDefined')}</p>
|
<p className="text-sm text-gray-500">
|
||||||
<p className="text-xs text-gray-400 mt-1">{translate('::App.SqlQueryManager.StepIsOptional')}</p>
|
{translate('::App.SqlQueryManager.NoIndexesDefined')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
{translate('::App.SqlQueryManager.StepIsOptional')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -2481,20 +2513,24 @@ const SqlTableDesignerDialog = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
<button
|
<Button
|
||||||
onClick={() => openEditIndex(idx)}
|
onClick={() => openEditIndex(idx)}
|
||||||
title={translate('::App.SqlQueryManager.Edit')}
|
title={translate('::App.SqlQueryManager.Edit')}
|
||||||
className="p-1.5 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors"
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaEdit className="w-3.5 h-3.5" />}
|
||||||
<button
|
className="!h-7 !w-7 !px-0 text-gray-400 hover:text-indigo-600 hover:!bg-indigo-50 dark:hover:!bg-indigo-900/20 transition-colors"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
onClick={() => deleteIndex(idx.id)}
|
onClick={() => deleteIndex(idx.id)}
|
||||||
title={translate('::App.SqlQueryManager.Delete')}
|
title={translate('::App.SqlQueryManager.Delete')}
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
size="xs"
|
||||||
>
|
variant="plain"
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTrash className="w-3.5 h-3.5" />}
|
||||||
|
className="!h-7 !w-7 !px-0 text-gray-400 hover:text-red-600 hover:!bg-red-50 dark:hover:!bg-red-900/20 transition-colors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -2511,14 +2547,18 @@ const SqlTableDesignerDialog = ({
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h2 className="text-base font-bold text-gray-900 dark:text-white">
|
<h2 className="text-base font-bold text-gray-900 dark:text-white">
|
||||||
{editingIndexId ? translate('::App.SqlQueryManager.EditIndexKey') : translate('::App.SqlQueryManager.AddNewIndexKey')}
|
{editingIndexId
|
||||||
|
? translate('::App.SqlQueryManager.EditIndexKey')
|
||||||
|
: translate('::App.SqlQueryManager.AddNewIndexKey')}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIndexModalOpen(false)}
|
onClick={() => setIndexModalOpen(false)}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
size="sm"
|
||||||
>
|
variant="plain"
|
||||||
<FaTimes className="w-4 h-4" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTimes className="w-4 h-4" />}
|
||||||
|
className="!h-8 !w-8 !px-0 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
|
|
@ -2530,7 +2570,7 @@ const SqlTableDesignerDialog = ({
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{INDEX_TYPES.map((t) => (
|
{INDEX_TYPES.map((t) => (
|
||||||
<button
|
<Button
|
||||||
key={t.value}
|
key={t.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -2540,7 +2580,9 @@ const SqlTableDesignerDialog = ({
|
||||||
indexName: buildIndexName(t.value, f.columns),
|
indexName: buildIndexName(t.value, f.columns),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className={`flex-1 py-2 px-3 rounded-lg border text-sm font-medium transition-all ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`flex-1 !h-auto !rounded-lg border !py-2 !px-3 text-sm font-medium transition-all ${
|
||||||
indexForm.indexType === t.value
|
indexForm.indexType === t.value
|
||||||
? 'bg-indigo-600 border-indigo-600 text-white'
|
? 'bg-indigo-600 border-indigo-600 text-white'
|
||||||
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:border-indigo-300'
|
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:border-indigo-300'
|
||||||
|
|
@ -2548,7 +2590,7 @@ const SqlTableDesignerDialog = ({
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
<span className="block text-xs opacity-70">{translate('::' + t.desc)}</span>
|
<span className="block text-xs opacity-70">{translate('::' + t.desc)}</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2562,7 +2604,9 @@ const SqlTableDesignerDialog = ({
|
||||||
type="text"
|
type="text"
|
||||||
value={indexForm.indexName}
|
value={indexForm.indexName}
|
||||||
onChange={(e) => setIndexForm((f) => ({ ...f, indexName: e.target.value }))}
|
onChange={(e) => setIndexForm((f) => ({ ...f, indexName: e.target.value }))}
|
||||||
placeholder={buildIndexName(indexForm.indexType, indexForm.columns) || `PK_EntityName_Id`}
|
placeholder={
|
||||||
|
buildIndexName(indexForm.indexType, indexForm.columns) || `PK_EntityName_Id`
|
||||||
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2590,8 +2634,12 @@ const SqlTableDesignerDialog = ({
|
||||||
<div className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden">
|
<div className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden">
|
||||||
<div className="grid grid-cols-12 gap-2 px-3 py-1.5 bg-gray-50 dark:bg-gray-700 text-xs font-semibold text-gray-500">
|
<div className="grid grid-cols-12 gap-2 px-3 py-1.5 bg-gray-50 dark:bg-gray-700 text-xs font-semibold text-gray-500">
|
||||||
<div className="col-span-1" />
|
<div className="col-span-1" />
|
||||||
<div className="col-span-7">{translate('::App.Listform.ListformField.Column')}</div>
|
<div className="col-span-7">
|
||||||
<div className="col-span-4">{translate('::App.SqlQueryManager.SortOrder')}</div>
|
{translate('::App.Listform.ListformField.Column')}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-4">
|
||||||
|
{translate('::App.SqlQueryManager.SortOrder')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-44 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-700">
|
<div className="max-h-44 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
{columns
|
{columns
|
||||||
|
|
@ -2681,9 +2729,7 @@ const SqlTableDesignerDialog = ({
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={indexForm.description}
|
value={indexForm.description}
|
||||||
onChange={(e) =>
|
onChange={(e) => setIndexForm((f) => ({ ...f, description: e.target.value }))}
|
||||||
setIndexForm((f) => ({ ...f, description: e.target.value }))
|
|
||||||
}
|
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder={translate('::App.SqlQueryManager.OptionalDescriptionPlaceholder')}
|
placeholder={translate('::App.SqlQueryManager.OptionalDescriptionPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 resize-none"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 resize-none"
|
||||||
|
|
@ -2693,19 +2739,24 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-2xl">
|
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-2xl">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIndexModalOpen(false)}
|
onClick={() => setIndexModalOpen(false)}
|
||||||
className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
className="text-gray-600 dark:text-gray-300 hover:!bg-gray-100 dark:hover:!bg-gray-700 !rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={saveIndex}
|
onClick={saveIndex}
|
||||||
disabled={!indexForm.indexName.trim() || indexForm.columns.length === 0}
|
disabled={!indexForm.indexName.trim() || indexForm.columns.length === 0}
|
||||||
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
size="sm"
|
||||||
|
variant="solid"
|
||||||
|
color="indigo-600"
|
||||||
|
className="hover:!bg-indigo-700 disabled:opacity-50 !rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{translate('::App.SqlQueryManager.Save')}
|
{translate('::App.SqlQueryManager.Save')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
@ -2722,12 +2773,14 @@ const SqlTableDesignerDialog = ({
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{translate('::App.SqlQueryManager.GeneratedSqlDescription')}
|
{translate('::App.SqlQueryManager.GeneratedSqlDescription')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button
|
||||||
className="text-xs text-blue-500 hover:underline"
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
className="!h-auto !px-1 !py-0 text-xs text-blue-500 hover:underline"
|
||||||
onClick={() => navigator.clipboard.writeText(generatedSql)}
|
onClick={() => navigator.clipboard.writeText(generatedSql)}
|
||||||
>
|
>
|
||||||
{translate('::App.SqlQueryManager.Copy')}
|
{translate('::App.SqlQueryManager.Copy')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="bg-gray-900 text-green-300 rounded-lg p-4 text-xs overflow-auto max-h-96 leading-relaxed whitespace-pre">
|
<pre className="bg-gray-900 text-green-300 rounded-lg p-4 text-xs overflow-auto max-h-96 leading-relaxed whitespace-pre">
|
||||||
{generatedSql}
|
{generatedSql}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
||||||
import { ReactElement, useRef, useState } from 'react'
|
import { ReactElement, useRef, useState } from 'react'
|
||||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
hideImageHoverPreview,
|
hideImageHoverPreview,
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
|
|
@ -117,10 +118,13 @@ const ImageUploadEditorComponent = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title="Sil"
|
title="Sil"
|
||||||
onClick={() => removeImage(i)}
|
onClick={() => removeImage(i)}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className="!absolute !h-[18px] !w-[18px] !bg-red-600 !p-0 !text-[11px] !text-white hover:!bg-red-700 active:!bg-red-800"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -6,
|
top: -6,
|
||||||
|
|
@ -139,7 +143,7 @@ const ImageUploadEditorComponent = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -154,11 +158,14 @@ const ImageUploadEditorComponent = ({
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={(e) => handleFiles(e.target.files)}
|
onChange={(e) => handleFiles(e.target.files)}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title="Görsel Yükle"
|
title="Görsel Yükle"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
|
variant="solid"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !rounded !px-2.5 !py-1 !text-[13px]"
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 10px',
|
padding: '4px 10px',
|
||||||
background: uploading ? '#aaa' : '#0078d4',
|
background: uploading ? '#aaa' : '#0078d4',
|
||||||
|
|
@ -177,7 +184,7 @@ const ImageUploadEditorComponent = ({
|
||||||
) : (
|
) : (
|
||||||
<FaUpload style={{ fontSize: 14 }} />
|
<FaUpload style={{ fontSize: 14 }} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
hideImageHoverPreview,
|
hideImageHoverPreview,
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
|
|
@ -112,11 +113,14 @@ const ImageViewerEditorComponent = ({
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
||||||
{urls.map((url, index) => (
|
{urls.map((url, index) => (
|
||||||
<button
|
<Button
|
||||||
key={`${url}-${index}`}
|
key={`${url}-${index}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openImageInNewTab(url)}
|
onClick={() => openImageInNewTab(url)}
|
||||||
style={{ border: 0, background: 'transparent', padding: 0, cursor: 'zoom-in' }}
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!border-0 !bg-transparent !p-0 hover:!bg-transparent active:!bg-transparent dark:hover:!bg-transparent dark:active:!bg-transparent"
|
||||||
|
style={{ cursor: 'zoom-in' }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
|
|
@ -136,7 +140,7 @@ const ImageViewerEditorComponent = ({
|
||||||
currentTarget.src = '/img/others/no-image.png'
|
currentTarget.src = '/img/others/no-image.png'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,15 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
|
||||||
? translate('::App.Forum.PostManagement.ReplytoTopic')
|
? translate('::App.Forum.PostManagement.ReplytoTopic')
|
||||||
: translate('::App.Forum.PostManagement.NewPost')}
|
: translate('::App.Forum.PostManagement.NewPost')}
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={onClose} className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
onClick={onClose}
|
||||||
|
className="!inline-flex !h-auto items-center justify-center !bg-transparent !p-1 text-gray-400 transition-colors hover:!bg-transparent hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,15 @@ export function CreateTopicModal({ onClose, onSubmit }: CreateTopicModalProps) {
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{translate('::App.Forum.TopicManagement.NewTopic')}
|
{translate('::App.Forum.TopicManagement.NewTopic')}
|
||||||
</h3>
|
</h3>
|
||||||
<button onClick={onClose} className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
onClick={onClose}
|
||||||
|
className="!inline-flex !h-auto items-center justify-center !bg-transparent !p-1 text-gray-400 transition-colors hover:!bg-transparent hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
<FaTimes className="w-5 h-5" />
|
<FaTimes className="w-5 h-5" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ForumPost } from '@/proxy/forum/forum'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { showDbDateAsIs } from '@/utils/dateUtils'
|
import { showDbDateAsIs } from '@/utils/dateUtils'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
post: ForumPost
|
post: ForumPost
|
||||||
|
|
@ -64,26 +65,32 @@ export function ForumPostCard({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onLike(post.id, isFirst)}
|
onClick={() => onLike(post.id, isFirst)}
|
||||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-sm transition-colors ${
|
className={`!inline-flex !h-auto items-center gap-1 rounded-full !px-3 py-1 text-sm transition-colors ${
|
||||||
isLiked
|
isLiked
|
||||||
? 'bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800'
|
? '!bg-red-100 text-red-600 hover:!bg-red-200 dark:!bg-red-900 dark:text-red-200 dark:hover:!bg-red-800'
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
: '!bg-gray-100 text-gray-600 hover:!bg-gray-200 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
|
<FaHeart className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
|
||||||
<span>{post.likeCount}</span>
|
<span>{post.likeCount}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{!isFirst && (
|
{!isFirst && (
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => onReply(post.id)}
|
onClick={() => onReply(post.id)}
|
||||||
className="flex items-center space-x-1 px-3 py-1 rounded-full text-sm bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
className="!inline-flex !h-auto items-center gap-1 rounded-full !bg-gray-100 !px-3 py-1 text-sm text-gray-600 transition-colors hover:!bg-gray-200 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700"
|
||||||
>
|
>
|
||||||
<FaReply className="w-4 h-4" />
|
<FaReply className="w-4 h-4" />
|
||||||
<span>{translate('::App.Forum.PostManagement.PostReply')}</span>
|
<span>{translate('::App.Forum.PostManagement.PostReply')}</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -310,27 +310,33 @@ export function ForumView({
|
||||||
<nav className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
<nav className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{selectedCategory && (
|
{selectedCategory && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => handleBreadcrumbClick('forum')}
|
onClick={() => handleBreadcrumbClick('forum')}
|
||||||
className={`transition-colors ${
|
className={`!inline-flex !h-auto items-center !bg-transparent !p-0 transition-colors ${
|
||||||
viewState === 'categories'
|
viewState === 'categories'
|
||||||
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
||||||
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Forum</div>
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Forum</div>
|
||||||
</button>
|
</Button>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
onClick={() => handleBreadcrumbClick('category')}
|
onClick={() => handleBreadcrumbClick('category')}
|
||||||
className={`transition-colors ${
|
className={`!inline-flex !h-auto items-center !bg-transparent !p-0 transition-colors ${
|
||||||
viewState === 'topics'
|
viewState === 'topics'
|
||||||
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
||||||
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedCategory.name}
|
{selectedCategory.name}
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{selectedTopic && (
|
{selectedTopic && (
|
||||||
|
|
@ -350,7 +356,7 @@ export function ForumView({
|
||||||
icon={<FaPlus className="w-4 h-4" />}
|
icon={<FaPlus className="w-4 h-4" />}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={() => setShowCreateTopic(true)}
|
onClick={() => setShowCreateTopic(true)}
|
||||||
className="flex items-center space-x-2 bg-blue-600 dark:bg-blue-700 text-white px-4 py-2 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors"
|
className="!inline-flex items-center gap-2 rounded-lg !bg-blue-600 !px-4 py-2 text-white transition-colors hover:!bg-blue-700 dark:!bg-blue-700 dark:hover:!bg-blue-800"
|
||||||
>
|
>
|
||||||
<span>{translate('::App.Forum.TopicManagement.NewTopic')}</span>
|
<span>{translate('::App.Forum.TopicManagement.NewTopic')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -361,7 +367,7 @@ export function ForumView({
|
||||||
icon={<FaPlus className="w-4 h-4" />}
|
icon={<FaPlus className="w-4 h-4" />}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={() => setShowCreatePost(true)}
|
onClick={() => setShowCreatePost(true)}
|
||||||
className="flex items-center space-x-2 bg-emerald-600 dark:bg-emerald-700 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:hover:bg-emerald-800 transition-colors"
|
className="!inline-flex items-center gap-2 rounded-lg !bg-emerald-600 !px-4 py-2 text-white transition-colors hover:!bg-emerald-700 dark:!bg-emerald-700 dark:hover:!bg-emerald-800"
|
||||||
>
|
>
|
||||||
<span>{translate('::App.Forum.PostManagement.NewPost')}</span>
|
<span>{translate('::App.Forum.PostManagement.NewPost')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -373,7 +379,7 @@ export function ForumView({
|
||||||
icon={<FaSearch className="w-4 h-4" />}
|
icon={<FaSearch className="w-4 h-4" />}
|
||||||
onClick={() => setIsSearchModalOpen(true)}
|
onClick={() => setIsSearchModalOpen(true)}
|
||||||
variant="default"
|
variant="default"
|
||||||
className="hidden md:flex items-center space-x-2 px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
className="hidden items-center gap-2 rounded-lg border border-gray-300 !px-4 py-2 transition-colors hover:!bg-gray-50 dark:border-gray-700 dark:hover:!bg-gray-800 md:!inline-flex"
|
||||||
>
|
>
|
||||||
<span className="text-gray-500 dark:text-gray-300">
|
<span className="text-gray-500 dark:text-gray-300">
|
||||||
{translate('::App.Forum.TopicManagement.Searchtopics')}
|
{translate('::App.Forum.TopicManagement.Searchtopics')}
|
||||||
|
|
@ -385,7 +391,7 @@ export function ForumView({
|
||||||
icon={<FaSearch className="w-5 h-5" />}
|
icon={<FaSearch className="w-5 h-5" />}
|
||||||
onClick={() => setIsSearchModalOpen(true)}
|
onClick={() => setIsSearchModalOpen(true)}
|
||||||
variant="default"
|
variant="default"
|
||||||
className="md:hidden p-2 text-gray-400 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-100 transition-colors"
|
className="!inline-flex !h-auto items-center justify-center !p-2 text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-300 dark:hover:text-gray-100 md:!hidden"
|
||||||
></Button>
|
></Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { FaTimes, FaSearch, FaFolder, FaRegComment, FaFileAlt } from 'react-icon
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
import { showDbDateAsIs } from '@/utils/dateUtils'
|
import { showDbDateAsIs } from '@/utils/dateUtils'
|
||||||
import { useForumSearch } from '@/utils/hooks/useForumSearch'
|
import { useForumSearch } from '@/utils/hooks/useForumSearch'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
interface SearchModalProps {
|
interface SearchModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
|
@ -109,12 +110,13 @@ export function SearchModal({
|
||||||
className="flex-1 outline-none text-lg bg-transparent text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500"
|
className="flex-1 outline-none text-lg bg-transparent text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors ml-3"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes className="w-5 h-5" />
|
icon={<FaTimes className="h-5 w-5" />}
|
||||||
</button>
|
className="ml-3 !h-8 !w-8 !px-0 text-gray-400 transition-colors hover:!bg-transparent hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-y-auto max-h-96">
|
<div className="overflow-y-auto max-h-96">
|
||||||
|
|
@ -136,14 +138,18 @@ export function SearchModal({
|
||||||
Categories ({searchResults.categories.length})
|
Categories ({searchResults.categories.length})
|
||||||
</div>
|
</div>
|
||||||
{searchResults.categories.map((category, index) => (
|
{searchResults.categories.map((category, index) => (
|
||||||
<button
|
<Button
|
||||||
key={`category-${category.id}`}
|
key={`category-${category.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCategorySelect(category)
|
onCategorySelect(category)
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
variant="plain"
|
||||||
selectedIndex === index ? 'bg-blue-50 dark:bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400' : ''
|
shape="none"
|
||||||
|
className={`w-full !h-auto !justify-start !rounded-none !px-4 !py-3 transition-colors hover:!bg-gray-50 dark:hover:!bg-gray-800 ${
|
||||||
|
selectedIndex === index
|
||||||
|
? '!bg-blue-50 dark:!bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400'
|
||||||
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3 flex-1">
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
|
|
@ -153,14 +159,18 @@ export function SearchModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="font-medium text-gray-900 dark:text-white">{category.name}</div>
|
<div className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{category.name}
|
||||||
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">
|
<div className="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">
|
||||||
{category.description}
|
{category.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500">{category.topicCount} topics</div>
|
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
</button>
|
{category.topicCount} topics
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -174,15 +184,17 @@ export function SearchModal({
|
||||||
{searchResults.topics.map((topic, index) => {
|
{searchResults.topics.map((topic, index) => {
|
||||||
const globalIndex = searchResults.categories.length + index
|
const globalIndex = searchResults.categories.length + index
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={`topic-${topic.id}`}
|
key={`topic-${topic.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onTopicSelect(topic)
|
onTopicSelect(topic)
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`w-full !h-auto !justify-start !rounded-none !px-4 !py-3 transition-colors hover:!bg-gray-50 dark:hover:!bg-gray-800 ${
|
||||||
selectedIndex === globalIndex
|
selectedIndex === globalIndex
|
||||||
? 'bg-blue-50 dark:bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400'
|
? '!bg-blue-50 dark:!bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -201,8 +213,10 @@ export function SearchModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500">{topic.replyCount} replies</div>
|
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
</button>
|
{topic.replyCount} replies
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -218,15 +232,17 @@ export function SearchModal({
|
||||||
const globalIndex =
|
const globalIndex =
|
||||||
searchResults.categories.length + searchResults.topics.length + index
|
searchResults.categories.length + searchResults.topics.length + index
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={`post-${post.id}`}
|
key={`post-${post.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPostSelect(post)
|
onPostSelect(post)
|
||||||
onClose()
|
onClose()
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`w-full !h-auto !justify-start !rounded-none !px-4 !py-3 transition-colors hover:!bg-gray-50 ${
|
||||||
selectedIndex === globalIndex
|
selectedIndex === globalIndex
|
||||||
? 'bg-blue-50 border-r-2 border-blue-500'
|
? '!bg-blue-50 border-r-2 border-blue-500'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -250,7 +266,7 @@ export function SearchModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">{post.likeCount} likes</div>
|
<div className="text-xs text-gray-400">{post.likeCount} likes</div>
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
import { currentLocalDate } from '@/utils/dateUtils'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import UpcomingEvents from './widgets/UpcomingEvents'
|
import UpcomingEvents from './widgets/UpcomingEvents'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
dayjs.extend(isBetween)
|
dayjs.extend(isBetween)
|
||||||
|
|
@ -245,7 +246,12 @@ const IntranetDashboard: React.FC = () => {
|
||||||
const renderWidgetComponent = (widgetId: string) => {
|
const renderWidgetComponent = (widgetId: string) => {
|
||||||
switch (widgetId) {
|
switch (widgetId) {
|
||||||
case 'upcoming-events':
|
case 'upcoming-events':
|
||||||
return <UpcomingEvents events={intranetDashboard?.events || []} onEventClick={setSelectedEvent} />
|
return (
|
||||||
|
<UpcomingEvents
|
||||||
|
events={intranetDashboard?.events || []}
|
||||||
|
onEventClick={setSelectedEvent}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case 'today-birthdays':
|
case 'today-birthdays':
|
||||||
return <TodayBirthdays employees={intranetDashboard?.birthdays || []} />
|
return <TodayBirthdays employees={intranetDashboard?.birthdays || []} />
|
||||||
case 'documents':
|
case 'documents':
|
||||||
|
|
@ -422,12 +428,14 @@ const IntranetDashboard: React.FC = () => {
|
||||||
>
|
>
|
||||||
🎨 {translate('::App.Platform.Intranet.Dashboard.DesignMode')}
|
🎨 {translate('::App.Platform.Intranet.Dashboard.DesignMode')}
|
||||||
</label>
|
</label>
|
||||||
<button
|
<Button
|
||||||
id="design-mode-toggle"
|
id="design-mode-toggle"
|
||||||
onClick={() => setIsDesignMode(!isDesignMode)}
|
onClick={() => setIsDesignMode(!isDesignMode)}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={`
|
className={`
|
||||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
relative !inline-flex !h-6 !w-11 !min-w-11 !items-center !justify-start !rounded-full !border-0 !px-0 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||||
${isDesignMode ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}
|
${isDesignMode ? '!bg-blue-600 hover:!bg-blue-600' : '!bg-gray-300 hover:!bg-gray-300 dark:!bg-gray-600 dark:hover:!bg-gray-600'}
|
||||||
`}
|
`}
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={isDesignMode}
|
aria-checked={isDesignMode}
|
||||||
|
|
@ -438,21 +446,23 @@ const IntranetDashboard: React.FC = () => {
|
||||||
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reset Button - Sadece design mode aktifken görünsün */}
|
{/* Reset Button - Sadece design mode aktifken görünsün */}
|
||||||
{isDesignMode && (
|
{isDesignMode && (
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.removeItem(WIDGET_ORDER_KEY)
|
localStorage.removeItem(WIDGET_ORDER_KEY)
|
||||||
initializeDefaultOrder()
|
initializeDefaultOrder()
|
||||||
}}
|
}}
|
||||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors flex items-center gap-1"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !items-center gap-1 !rounded-none !px-0 !py-0 text-sm text-gray-500 transition-colors hover:!bg-transparent hover:text-gray-700 active:!bg-transparent focus:!bg-transparent dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
title="Widget düzenini varsayılana döndür"
|
title="Widget düzenini varsayılana döndür"
|
||||||
>
|
>
|
||||||
🔄 {translate('::App.Platform.Intranet.Dashboard.Reset')}
|
🔄 {translate('::App.Platform.Intranet.Dashboard.Reset')}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -630,10 +640,7 @@ const IntranetDashboard: React.FC = () => {
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
<EventModal
|
<EventModal event={selectedEvent} onClose={() => setSelectedEvent(null)} />
|
||||||
event={selectedEvent}
|
|
||||||
onClose={() => setSelectedEvent(null)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import LocationPicker from './LocationPicker'
|
||||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
import { SocialMediaDto } from '@/proxy/intranet/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import { Avatar } from '@/components/ui'
|
import { Avatar, Button } from '@/components/ui'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
|
||||||
interface CreatePostProps {
|
interface CreatePostProps {
|
||||||
|
|
@ -189,18 +189,20 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} (
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} (
|
||||||
{mediaItems.length})
|
{mediaItems.length})
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearMedia()
|
clearMedia()
|
||||||
}}
|
}}
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !rounded-none !px-0 !py-0 text-sm font-medium text-red-600 hover:!bg-transparent hover:text-red-700 active:!bg-transparent focus:!bg-transparent"
|
||||||
title={translate(
|
title={translate(
|
||||||
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle',
|
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
{mediaItems.map((item) => (
|
{mediaItems.map((item) => (
|
||||||
|
|
@ -224,25 +226,31 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeMediaItem(item.id)}
|
onClick={() => removeMediaItem(item.id)}
|
||||||
className="absolute -top-2 -right-2 p-1 bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
variant="solid"
|
||||||
>
|
color="red-600"
|
||||||
<FaTimes className="w-4 h-4" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTimes className="h-4 w-4" />}
|
||||||
|
className="absolute -right-2 -top-2 !h-6 !w-6 !px-0 text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
/>
|
||||||
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
|
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
|
||||||
{item.type === 'image' ? '📷' : '🎥'}
|
{item.type === 'image' ? '📷' : '🎥'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowMediaManager(true)}
|
onClick={() => setShowMediaManager(true)}
|
||||||
className="h-24 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg flex items-center justify-center hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-24 !w-full !rounded-lg border-2 border-dashed border-gray-300 !p-0 transition-colors hover:border-blue-500 hover:!bg-blue-50 dark:border-gray-600 dark:hover:!bg-blue-900/20"
|
||||||
>
|
>
|
||||||
<FaImages className="w-6 h-6 text-gray-400" />
|
<span className="flex h-full w-full items-center justify-center">
|
||||||
</button>
|
<FaImages className="h-6 w-6 text-gray-400" />
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -258,16 +266,18 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
<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.Location')}
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}
|
||||||
</h4>
|
</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"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !rounded-none !px-0 !py-0 text-sm font-medium text-red-600 hover:!bg-transparent hover:text-red-700 active:!bg-transparent focus:!bg-transparent"
|
||||||
title={translate(
|
title={translate(
|
||||||
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle',
|
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700">
|
<div className="p-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
|
|
@ -296,16 +306,18 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
<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.Poll')}
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearMedia()
|
clearMedia()
|
||||||
}}
|
}}
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !rounded-none !px-0 !py-0 text-sm font-medium text-red-600 hover:!bg-transparent hover:text-red-700 active:!bg-transparent focus:!bg-transparent"
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')}
|
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')}
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -329,25 +341,28 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
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 && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removePollOption(index)}
|
onClick={() => removePollOption(index)}
|
||||||
className="p-2 text-gray-500 hover:text-red-600"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes className="w-5 h-5" />
|
icon={<FaTimes className="h-5 w-5" />}
|
||||||
</button>
|
className="!h-9 !w-9 !px-0 text-gray-500 hover:!bg-transparent hover:text-red-600 active:!bg-transparent focus:!bg-transparent"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{pollOptions.length < 6 && (
|
{pollOptions.length < 6 && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addPollOption}
|
onClick={addPollOption}
|
||||||
className="mt-2 text-sm text-blue-600 hover:text-blue-700 font-medium"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="mt-2 !h-auto !rounded-none !px-0 !py-0 text-sm font-medium text-blue-600 hover:!bg-transparent hover:text-blue-700 active:!bg-transparent focus:!bg-transparent"
|
||||||
>
|
>
|
||||||
+ {translate('::App.Platform.Intranet.SocialWall.CreatePost.AddOption')}
|
+ {translate('::App.Platform.Intranet.SocialWall.CreatePost.AddOption')}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -363,7 +378,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700"
|
className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700"
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 relative">
|
<div className="flex gap-2 relative">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (mediaType === 'media' && mediaItems.length > 0) {
|
if (mediaType === 'media' && mediaItems.length > 0) {
|
||||||
|
|
@ -376,11 +391,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
setShowMediaManager(true)
|
setShowMediaManager(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'p-2 rounded-full transition-colors',
|
'relative !h-9 !w-9 !px-0 overflow-visible 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 hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700',
|
||||||
)}
|
)}
|
||||||
title={
|
title={
|
||||||
mediaType === 'media'
|
mediaType === 'media'
|
||||||
|
|
@ -388,14 +405,16 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle')
|
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FaImages className="w-5 h-5" />
|
<span className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<FaImages className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
{mediaType === 'media' && mediaItems.length > 0 && (
|
{mediaType === 'media' && mediaItems.length > 0 && (
|
||||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-600 text-white text-xs rounded-full flex items-center justify-center">
|
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-600 text-white text-xs rounded-full flex items-center justify-center">
|
||||||
{mediaItems.length}
|
{mediaItems.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Başka bir tip seçiliyse temizle
|
// Başka bir tip seçiliyse temizle
|
||||||
|
|
@ -404,49 +423,55 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
}
|
}
|
||||||
setMediaType(mediaType === 'poll' ? null : 'poll')
|
setMediaType(mediaType === 'poll' ? null : 'poll')
|
||||||
}}
|
}}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaChartBar className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'p-2 rounded-full transition-colors',
|
'!h-9 !w-9 !px-0 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 hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700',
|
||||||
)}
|
)}
|
||||||
title={
|
title={
|
||||||
mediaType === 'poll'
|
mediaType === 'poll'
|
||||||
? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')
|
? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')
|
||||||
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')
|
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<FaChartBar className="w-5 h-5" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||||
className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaSmile className="h-5 w-5" />}
|
||||||
|
className="!h-9 !w-9 !px-0 text-gray-600 transition-colors hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700"
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.AddEmojiTitle')}
|
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.AddEmojiTitle')}
|
||||||
>
|
/>
|
||||||
<FaSmile className="w-5 h-5" />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowLocationPicker(true)}
|
onClick={() => setShowLocationPicker(true)}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaMapMarkerAlt className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'p-2 rounded-full transition-colors',
|
'!h-9 !w-9 !px-0 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 hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700',
|
||||||
)}
|
)}
|
||||||
title={
|
title={
|
||||||
location
|
location
|
||||||
? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle')
|
? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle')
|
||||||
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')
|
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<FaMapMarkerAlt className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Emoji Picker */}
|
{/* Emoji Picker */}
|
||||||
{showEmojiPicker && (
|
{showEmojiPicker && (
|
||||||
<div ref={emojiPickerRef} className="absolute bottom-6 left-0 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-2">
|
<div
|
||||||
|
ref={emojiPickerRef}
|
||||||
|
className="absolute bottom-6 left-0 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-2"
|
||||||
|
>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
searchDisabled
|
searchDisabled
|
||||||
theme={theme.mode === 'dark' ? Theme.DARK : Theme.LIGHT}
|
theme={theme.mode === 'dark' ? Theme.DARK : Theme.LIGHT}
|
||||||
|
|
@ -457,13 +482,15 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!content.trim() && mediaItems.length === 0 && !mediaType}
|
disabled={!content.trim() && mediaItems.length === 0 && !mediaType}
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!h-auto !rounded-full !px-6 !py-2 font-medium text-white transition-colors hover:!bg-blue-700 disabled:cursor-not-allowed disabled:!bg-gray-400"
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Submit')}
|
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Submit')}
|
||||||
</button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { FaExternalLinkAlt, FaMapMarkerAlt } from 'react-icons/fa'
|
import { FaExternalLinkAlt, FaMapMarkerAlt } from 'react-icons/fa'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
interface LocationData {
|
interface LocationData {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -20,7 +21,7 @@ interface LocationMapProps {
|
||||||
const LocationMap: React.FC<LocationMapProps> = ({
|
const LocationMap: React.FC<LocationMapProps> = ({
|
||||||
location,
|
location,
|
||||||
className = '',
|
className = '',
|
||||||
showDirections = true
|
showDirections = true,
|
||||||
}) => {
|
}) => {
|
||||||
const locationData: LocationData = JSON.parse(location)
|
const locationData: LocationData = JSON.parse(location)
|
||||||
|
|
||||||
|
|
@ -44,9 +45,11 @@ const LocationMap: React.FC<LocationMapProps> = ({
|
||||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`
|
return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const { translate } = useLocalization();
|
const { translate } = useLocalization()
|
||||||
return (
|
return (
|
||||||
<div className={`relative rounded-lg overflow-hidden bg-gray-200 dark:bg-gray-700 ${className}`}>
|
<div
|
||||||
|
className={`relative rounded-lg overflow-hidden bg-gray-200 dark:bg-gray-700 ${className}`}
|
||||||
|
>
|
||||||
{/* Map Container */}
|
{/* Map Container */}
|
||||||
<div className="relative w-full h-64 group">
|
<div className="relative w-full h-64 group">
|
||||||
{/* OpenStreetMap iframe for demo */}
|
{/* OpenStreetMap iframe for demo */}
|
||||||
|
|
@ -75,13 +78,18 @@ const LocationMap: React.FC<LocationMapProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Click to open overlay - invisible but clickable */}
|
{/* Click to open overlay - invisible but clickable */}
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={handleOpenGoogleMaps}
|
onClick={handleOpenGoogleMaps}
|
||||||
className="absolute inset-0 w-full h-full cursor-pointer group"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="absolute inset-0 !h-full !w-full !rounded-none !p-0 cursor-pointer hover:!bg-transparent active:!bg-transparent focus:!bg-transparent"
|
||||||
aria-label={translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
aria-label={translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}</span>
|
<span className="sr-only">
|
||||||
</button>
|
{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Hover Effect */}
|
{/* Hover Effect */}
|
||||||
<div className="absolute inset-0 bg-blue-600/0 group-hover:bg-blue-600/10 transition-colors duration-200" />
|
<div className="absolute inset-0 bg-blue-600/0 group-hover:bg-blue-600/10 transition-colors duration-200" />
|
||||||
|
|
@ -90,13 +98,18 @@ const LocationMap: React.FC<LocationMapProps> = ({
|
||||||
{/* Directions Button */}
|
{/* Directions Button */}
|
||||||
{showDirections && (
|
{showDirections && (
|
||||||
<div className="p-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
<div className="p-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={handleOpenGoogleMaps}
|
onClick={handleOpenGoogleMaps}
|
||||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
icon={<FaExternalLinkAlt className="h-5 w-5" />}
|
||||||
|
className="w-full !h-auto !rounded-lg !px-4 !py-2.5 font-medium text-white transition-colors hover:!bg-blue-700"
|
||||||
>
|
>
|
||||||
<FaExternalLinkAlt className="w-5 h-5" />
|
<span>
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}</span>
|
{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
||||||
</button>
|
</span>
|
||||||
|
</Button>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationMap.ClickForDirections')}
|
{translate('::App.Platform.Intranet.SocialWall.LocationMap.ClickForDirections')}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { FaTimes, FaSearch, FaMapMarkerAlt } from 'react-icons/fa'
|
import { FaTimes, FaSearch, FaMapMarkerAlt } from 'react-icons/fa'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
interface LocationPickerProps {
|
interface LocationPickerProps {
|
||||||
onSelect: (location: string) => void
|
onSelect: (location: string) => void
|
||||||
|
|
@ -258,12 +259,13 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.AddLocation')}
|
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.AddLocation')}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
icon={<FaTimes className="h-5 w-5 text-gray-500 dark:text-gray-400" />}
|
||||||
</button>
|
className="!h-9 !w-9 !px-0 transition-colors hover:!bg-gray-100 dark:hover:!bg-gray-700"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
|
|
@ -330,14 +332,16 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{locations.map((location) => (
|
{locations.map((location) => (
|
||||||
<button
|
<Button
|
||||||
key={location.id}
|
key={location.id}
|
||||||
onClick={() => handleSelect(location)}
|
onClick={() => handleSelect(location)}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full text-left p-3 rounded-lg transition-all hover:bg-gray-50 dark:hover:bg-gray-700',
|
'w-full !h-auto !justify-start !rounded-lg border-2 !p-3 text-left transition-all hover:!bg-gray-50 dark:hover:!bg-gray-700',
|
||||||
selectedLocation?.id === location.id
|
selectedLocation?.id === location.id
|
||||||
? 'bg-blue-50 dark:bg-blue-900/30 border-2 border-blue-500'
|
? '!bg-blue-50 border-blue-500 dark:!bg-blue-900/30'
|
||||||
: 'border-2 border-transparent',
|
: 'border-transparent',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
|
@ -385,7 +389,7 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -408,19 +412,22 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
variant="plain"
|
||||||
|
className="!h-auto !rounded-lg !px-4 !py-2 text-gray-700 transition-colors hover:!bg-gray-200 dark:text-gray-200 dark:hover:!bg-gray-700"
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
disabled={!selectedLocation}
|
disabled={!selectedLocation}
|
||||||
className="px-6 py-2 bg-blue-600 dark:bg-blue-700 text-white font-medium rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800 disabled:bg-gray-400 dark:disabled:bg-gray-700 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!h-auto !rounded-lg !px-6 !py-2 font-medium text-white transition-colors hover:!bg-blue-700 disabled:cursor-not-allowed disabled:!bg-gray-400 dark:bg-blue-700 dark:hover:!bg-blue-800 dark:disabled:!bg-gray-700"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Add')}
|
{translate('::ListForms.Wizard.Add')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { motion } from 'framer-motion'
|
||||||
import { FaTimes, FaLink, FaUpload } from 'react-icons/fa'
|
import { FaTimes, FaLink, FaUpload } from 'react-icons/fa'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
import { SocialMediaDto } from '@/proxy/intranet/models'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
interface MediaManagerProps {
|
interface MediaManagerProps {
|
||||||
media: SocialMediaDto[]
|
media: SocialMediaDto[]
|
||||||
|
|
@ -12,7 +13,7 @@ interface MediaManagerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose }) => {
|
const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose }) => {
|
||||||
const { translate } = useLocalization();
|
const { translate } = useLocalization()
|
||||||
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
|
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
|
||||||
const [urlInput, setUrlInput] = useState('')
|
const [urlInput, setUrlInput] = useState('')
|
||||||
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
||||||
|
|
@ -50,7 +51,7 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
const newMedia: SocialMediaDto = {
|
const newMedia: SocialMediaDto = {
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
urls: [urlInput]
|
urls: [urlInput],
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange([...media, newMedia])
|
onChange([...media, newMedia])
|
||||||
|
|
@ -72,45 +73,50 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddMedia')}</h2>
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
<button
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddMedia')}
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
icon={<FaTimes className="h-5 w-5 text-gray-500 dark:text-gray-400" />}
|
||||||
</button>
|
className="!h-9 !w-9 !px-0 transition-colors hover:!bg-gray-100 dark:hover:!bg-gray-700"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex border-b border-gray-200 dark:border-gray-700 px-4">
|
<div className="flex border-b border-gray-200 dark:border-gray-700 px-4">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setActiveTab('upload')}
|
onClick={() => setActiveTab('upload')}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaUpload className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'px-4 py-3 font-medium border-b-2 transition-colors',
|
'!h-auto !items-center gap-2 !rounded-none border-b-2 !px-4 !py-3 font-medium transition-colors',
|
||||||
activeTab === 'upload'
|
activeTab === 'upload'
|
||||||
? 'border-blue-600 text-blue-600'
|
? 'border-blue-600 !text-blue-600'
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
: 'border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<span>
|
||||||
<FaUpload className="w-5 h-5" />
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}</span>
|
</span>
|
||||||
</div>
|
</Button>
|
||||||
</button>
|
<Button
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('url')}
|
onClick={() => setActiveTab('url')}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaLink className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'px-4 py-3 font-medium border-b-2 transition-colors',
|
'!h-auto !items-center gap-2 !rounded-none border-b-2 !px-4 !py-3 font-medium transition-colors',
|
||||||
activeTab === 'url'
|
activeTab === 'url'
|
||||||
? 'border-blue-600 text-blue-600'
|
? 'border-blue-600 !text-blue-600'
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
: 'border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FaLink className="w-5 h-5" />
|
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddByUrl')}</span>
|
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddByUrl')}</span>
|
||||||
</div>
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|
@ -124,7 +130,9 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.ClickToSelectFile')}
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.ClickToSelectFile')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.ImageOrVideoFormats')}
|
{translate(
|
||||||
|
'::App.Platform.Intranet.SocialWall.MediaManager.ImageOrVideoFormats',
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|
@ -143,28 +151,32 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.MediaType')}
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.MediaType')}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setMediaType('image')}
|
onClick={() => setMediaType('image')}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex-1 py-2 px-4 rounded-lg font-medium transition-colors',
|
'flex-1 !h-auto !rounded-lg !px-4 !py-2 font-medium transition-colors',
|
||||||
mediaType === 'image'
|
mediaType === 'image'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 !text-white'
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
: 'bg-gray-100 text-gray-700 hover:!bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:!bg-gray-600',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Image')}
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Image')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setMediaType('video')}
|
onClick={() => setMediaType('video')}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex-1 py-2 px-4 rounded-lg font-medium transition-colors',
|
'flex-1 !h-auto !rounded-lg !px-4 !py-2 font-medium transition-colors',
|
||||||
mediaType === 'video'
|
mediaType === 'video'
|
||||||
? 'bg-blue-600 text-white'
|
? '!bg-blue-600 !text-white'
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
: 'bg-gray-100 text-gray-700 hover:!bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:!bg-gray-600',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Video')}
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Video')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
@ -173,16 +185,22 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
value={urlInput}
|
value={urlInput}
|
||||||
onChange={(e) => setUrlInput(e.target.value)}
|
onChange={(e) => setUrlInput(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleUrlAdd()}
|
onKeyPress={(e) => e.key === 'Enter' && handleUrlAdd()}
|
||||||
placeholder={mediaType === 'image' ? translate('::App.Platform.Intranet.SocialWall.MediaManager.EnterImageUrl') : translate('::App.Platform.Intranet.SocialWall.MediaManager.EnterVideoUrl')}
|
placeholder={
|
||||||
|
mediaType === 'image'
|
||||||
|
? translate('::App.Platform.Intranet.SocialWall.MediaManager.EnterImageUrl')
|
||||||
|
: translate('::App.Platform.Intranet.SocialWall.MediaManager.EnterVideoUrl')
|
||||||
|
}
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleUrlAdd}
|
onClick={handleUrlAdd}
|
||||||
disabled={!urlInput.trim()}
|
disabled={!urlInput.trim()}
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!h-auto !rounded-lg !px-6 !py-2 font-medium text-white transition-colors hover:!bg-blue-700 disabled:cursor-not-allowed disabled:!bg-gray-400"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.Wizard.Add')}
|
{translate('::ListForms.Wizard.Add')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -191,7 +209,8 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
{media.length > 0 && (
|
{media.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddedMedia')} ({media.length})
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddedMedia')} (
|
||||||
|
{media.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-3">
|
||||||
{media.map((item) => (
|
{media.map((item) => (
|
||||||
|
|
@ -204,7 +223,10 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-24 bg-gray-900 rounded-lg flex items-center justify-center">
|
<div className="w-full h-24 bg-gray-900 rounded-lg flex items-center justify-center">
|
||||||
<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>
|
||||||
|
|
@ -212,14 +234,18 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
onClick={() => removeMedia(item.id)}
|
onClick={() => removeMedia(item.id)}
|
||||||
className="absolute -top-2 -right-2 p-1 bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
variant="solid"
|
||||||
>
|
color="red-600"
|
||||||
<FaTimes className="w-4 h-4" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaTimes className="h-4 w-4" />}
|
||||||
|
className="absolute -right-2 -top-2 !h-6 !w-6 !px-0 text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
/>
|
||||||
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
|
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
|
||||||
{item.type === 'image' ? translate('::App.Platform.Intranet.SocialWall.MediaManager.ImageIcon') : translate('::App.Platform.Intranet.SocialWall.MediaManager.VideoIcon')}
|
{item.type === 'image'
|
||||||
|
? translate('::App.Platform.Intranet.SocialWall.MediaManager.ImageIcon')
|
||||||
|
: translate('::App.Platform.Intranet.SocialWall.MediaManager.VideoIcon')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -230,19 +256,24 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
variant="plain"
|
||||||
|
className="!h-auto !rounded-lg !px-4 !py-2 text-gray-700 transition-colors hover:!bg-gray-100 dark:text-gray-300 dark:hover:!bg-gray-700"
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={media.length === 0}
|
disabled={media.length === 0}
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!h-auto !rounded-lg !px-6 !py-2 font-medium text-white transition-colors hover:!bg-blue-700 disabled:cursor-not-allowed disabled:!bg-gray-400"
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Done', { count: media.length })}
|
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Done', {
|
||||||
</button>
|
count: media.length,
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ 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'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
import { Avatar } from '@/components/ui'
|
import { Avatar, Button } from '@/components/ui'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
dayjs.locale('tr')
|
dayjs.locale('tr')
|
||||||
|
|
@ -202,15 +202,19 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
const isSelected = pollUserVoteId === option.id
|
const isSelected = pollUserVoteId === option.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={option.id}
|
key={option.id}
|
||||||
onClick={() => option.id && !hasVoted && !isExpired && onVote(post.id, option.id)}
|
onClick={() =>
|
||||||
|
option.id && !hasVoted && !isExpired && onVote(post.id, option.id)
|
||||||
|
}
|
||||||
disabled={hasVoted || isExpired}
|
disabled={hasVoted || isExpired}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full text-left p-3 rounded-lg relative overflow-hidden transition-all',
|
'w-full !h-auto !justify-start !rounded-lg !p-3 text-left relative overflow-hidden transition-all',
|
||||||
{
|
{
|
||||||
'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500': isSelected,
|
'!bg-blue-100 dark:!bg-blue-900 border-2 border-blue-500': isSelected,
|
||||||
'bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500':
|
'bg-white dark:bg-gray-600 hover:!bg-gray-50 dark:hover:!bg-gray-500':
|
||||||
!isSelected && !hasVoted && !isExpired,
|
!isSelected && !hasVoted && !isExpired,
|
||||||
'bg-white dark:bg-gray-600 cursor-not-allowed': hasVoted || isExpired,
|
'bg-white dark:bg-gray-600 cursor-not-allowed': hasVoted || isExpired,
|
||||||
},
|
},
|
||||||
|
|
@ -232,7 +236,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -294,13 +298,14 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{post.isOwnPost && (
|
{post.isOwnPost && (
|
||||||
<button
|
<Button
|
||||||
onClick={() => onDelete(post.id)}
|
onClick={() => onDelete(post.id)}
|
||||||
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaTrash className="h-3 w-3" />}
|
||||||
|
className="!h-8 !w-8 !px-0 text-gray-500 transition-colors hover:!bg-red-50 hover:text-red-600 dark:hover:!bg-red-900/20"
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.PostItem.DeletePost')}
|
title={translate('::App.Platform.Intranet.SocialWall.PostItem.DeletePost')}
|
||||||
>
|
/>
|
||||||
<FaTrash className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -312,26 +317,30 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-6 pt-3 border-t border-gray-100 dark:border-gray-700">
|
<div className="flex items-center gap-6 pt-3 border-t border-gray-100 dark:border-gray-700">
|
||||||
<button
|
<Button
|
||||||
onClick={() => onLike(post.id)}
|
onClick={() => onLike(post.id)}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={post.isLiked ? <FaHeart className="h-5 w-5" /> : <FaRegHeart className="h-5 w-5" />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center gap-2 transition-colors',
|
'!h-auto !items-center gap-2 !rounded-none !px-0 !py-0 transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent',
|
||||||
post.isLiked
|
post.isLiked
|
||||||
? 'text-red-600 hover:text-red-700'
|
? 'text-red-600 hover:text-red-700'
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-red-600',
|
: 'text-gray-600 dark:text-gray-400 hover:text-red-600',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{post.isLiked ? <FaHeart className="w-5 h-5" /> : <FaRegHeart className="w-5 h-5" />}
|
|
||||||
<span className="text-sm font-medium">{postLikeCount}</span>
|
<span className="text-sm font-medium">{postLikeCount}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => setShowComments(!showComments)}
|
onClick={() => setShowComments(!showComments)}
|
||||||
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 transition-colors"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaRegCommentAlt className="h-5 w-5" />}
|
||||||
|
className="!h-auto !items-center gap-2 !rounded-none !px-0 !py-0 text-gray-600 transition-colors hover:!bg-transparent hover:text-blue-600 active:!bg-transparent focus:!bg-transparent dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<FaRegCommentAlt className="w-5 h-5" />
|
|
||||||
<span className="text-sm font-medium">{postComments.length}</span>
|
<span className="text-sm font-medium">{postComments.length}</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comments Section */}
|
{/* Comments Section */}
|
||||||
|
|
@ -355,13 +364,15 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
||||||
)}
|
)}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-full 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-full bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!commentText.trim()}
|
disabled={!commentText.trim()}
|
||||||
className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
variant="solid"
|
||||||
>
|
color="blue-600"
|
||||||
<FaPaperPlane className="w-5 h-5" />
|
shape="circle"
|
||||||
</button>
|
icon={<FaPaperPlane className="h-5 w-5" />}
|
||||||
|
className="!h-10 !w-10 !px-0 text-white transition-colors hover:!bg-blue-700 disabled:cursor-not-allowed disabled:!bg-gray-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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 { intranetService } from '@/services/intranet.service'
|
import { intranetService } from '@/services/intranet.service'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
@ -67,7 +68,14 @@ const SocialWall: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
let mediaInput: { type: 'image' | 'video' | 'poll'; urls?: string[]; pollQuestion?: string; pollOptions?: { text: string }[] } | 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) {
|
||||||
|
|
@ -174,26 +182,30 @@ const SocialWall: React.FC = () => {
|
||||||
<div className="mx-auto px-4">
|
<div className="mx-auto px-4">
|
||||||
{/* Filter Tabs */}
|
{/* Filter Tabs */}
|
||||||
<div className="flex gap-4 mb-6 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex gap-4 mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<button
|
<Button
|
||||||
onClick={() => setFilter('all')}
|
onClick={() => setFilter('all')}
|
||||||
className={`pb-3 px-1 border-b-2 transition-colors font-medium ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!h-auto !rounded-none border-b-2 !bg-transparent !px-1 !pb-3 !pt-0 font-medium transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent ${
|
||||||
filter === 'all'
|
filter === 'all'
|
||||||
? 'border-blue-600 text-blue-600'
|
? '!border-b-blue-600 !text-blue-600'
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
: '!border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.AllPosts')}
|
{translate('::App.Platform.Intranet.SocialWall.AllPosts')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setFilter('mine')}
|
onClick={() => setFilter('mine')}
|
||||||
className={`pb-3 px-1 border-b-2 transition-colors font-medium ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!h-auto !rounded-none border-b-2 !bg-transparent !px-1 !pb-3 !pt-0 font-medium transition-colors hover:!bg-transparent active:!bg-transparent focus:!bg-transparent ${
|
||||||
filter === 'mine'
|
filter === 'mine'
|
||||||
? 'border-blue-600 text-blue-600'
|
? '!border-b-blue-600 !text-blue-600'
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
: '!border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MyPosts')}
|
{translate('::App.Platform.Intranet.SocialWall.MyPosts')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Post */}
|
{/* Create Post */}
|
||||||
|
|
@ -234,7 +246,8 @@ const SocialWall: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
{!hasMore && posts.length > 0 && (
|
{!hasMore && posts.length > 0 && (
|
||||||
<p className="text-center text-sm text-gray-400 dark:text-gray-600 py-6">
|
<p className="text-center text-sm text-gray-400 dark:text-gray-600 py-6">
|
||||||
{translate('::App.Platform.Intranet.SocialWall.AllPostsLoaded') || 'Tüm gönderiler yüklendi'}
|
{translate('::App.Platform.Intranet.SocialWall.AllPostsLoaded') ||
|
||||||
|
'Tüm gönderiler yüklendi'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
FaPaperPlane,
|
FaPaperPlane,
|
||||||
FaHeart,
|
FaHeart,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
import { AnnouncementCommentDto, AnnouncementDto } from '@/proxy/intranet/models'
|
import { AnnouncementCommentDto, AnnouncementDto } from '@/proxy/intranet/models'
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
import useLocale from '@/utils/hooks/useLocale'
|
||||||
|
|
@ -197,11 +197,13 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
{announcement.viewCount}{' '}
|
{announcement.viewCount}{' '}
|
||||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')}
|
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleLike}
|
onClick={handleLike}
|
||||||
disabled={liking}
|
disabled={liking}
|
||||||
className={`flex items-center gap-2 border-0 transition-colors text-xs font-medium ${
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
className={`!h-auto !items-center gap-2 !rounded !border-0 !px-2 !py-1 text-xs font-medium transition-colors ${
|
||||||
isLiked
|
isLiked
|
||||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
|
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
|
||||||
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||||
|
|
@ -209,7 +211,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
>
|
>
|
||||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
||||||
{likes > 0 && likes}
|
{likes > 0 && likes}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -244,47 +246,52 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
className="w-full h-56 object-contain cursor-zoom-in"
|
className="w-full h-56 object-contain cursor-zoom-in"
|
||||||
onClick={() => openLightbox(activePhoto)}
|
onClick={() => openLightbox(activePhoto)}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openLightbox(activePhoto)}
|
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"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaExpand className="h-4 w-4" />}
|
||||||
|
className="absolute top-2 right-2 !h-7 !w-7 !rounded-lg !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
title="Tam ekran"
|
title="Tam ekran"
|
||||||
>
|
/>
|
||||||
<FaExpand className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setActivePhoto((i) => (i - 1 + images.length) % images.length)
|
setActivePhoto((i) => (i - 1 + images.length) % images.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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronLeft className="w-4 h-4" />
|
icon={<FaChevronLeft className="h-4 w-4" />}
|
||||||
</button>
|
className="absolute left-2 top-1/2 !h-8 !w-8 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
<button
|
/>
|
||||||
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setActivePhoto((i) => (i + 1) % images.length)
|
setActivePhoto((i) => (i + 1) % images.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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronRight className="w-4 h-4" />
|
icon={<FaChevronRight className="h-4 w-4" />}
|
||||||
</button>
|
className="absolute right-2 top-1/2 !h-8 !w-8 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||||
{images.map((img, idx) => (
|
{images.map((img, idx) => (
|
||||||
<button
|
<Button
|
||||||
key={idx}
|
key={idx}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActivePhoto(idx)}
|
onClick={() => setActivePhoto(idx)}
|
||||||
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`flex-shrink-0 !h-16 !w-16 !rounded-lg !px-0 overflow-hidden border-2 transition-all ${
|
||||||
idx === activePhoto
|
idx === activePhoto
|
||||||
? 'border-blue-500 opacity-100'
|
? 'border-blue-500 opacity-100'
|
||||||
: 'border-transparent opacity-60 hover:opacity-90'
|
: 'border-transparent opacity-60 hover:opacity-90'
|
||||||
|
|
@ -295,7 +302,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
alt={`${announcement.title} thumbnail ${idx + 1}`}
|
alt={`${announcement.title} thumbnail ${idx + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -405,26 +412,32 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => setCommentText(e.target.value)}
|
onChange={(e) => setCommentText(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder')}
|
placeholder={translate(
|
||||||
|
'::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder',
|
||||||
|
)}
|
||||||
rows={1}
|
rows={1}
|
||||||
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-blue-500 transition-colors"
|
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-blue-500 transition-colors"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmitComment}
|
onClick={handleSubmitComment}
|
||||||
disabled={!commentText.trim() || submitting}
|
disabled={!commentText.trim() || submitting}
|
||||||
className="p-2.5 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-colors flex-shrink-0"
|
variant="solid"
|
||||||
>
|
color="blue-500"
|
||||||
<FaPaperPlane className="w-4 h-4" />
|
shape="none"
|
||||||
</button>
|
icon={<FaPaperPlane className="h-4 w-4" />}
|
||||||
|
className="!h-10 !w-10 flex-shrink-0 !rounded-xl !px-0 text-white transition-colors hover:!bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm font-medium flex-shrink-0"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!h-auto flex-shrink-0 !rounded-lg !px-2 !py-2 text-sm font-medium text-white transition-colors hover:!bg-blue-700"
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Close')}
|
{translate('::App.Platform.Close')}
|
||||||
</button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
|
|
||||||
{/* Lightbox */}
|
{/* Lightbox */}
|
||||||
|
|
@ -440,26 +453,29 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
onClick={closeLightbox}
|
onClick={closeLightbox}
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
||||||
<button
|
<Button
|
||||||
onClick={closeLightbox}
|
onClick={closeLightbox}
|
||||||
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes className="w-8 h-8" />
|
icon={<FaTimes className="h-8 w-8" />}
|
||||||
</button>
|
className="absolute top-4 right-4 z-10 !h-12 !w-12 !px-0 text-white transition-colors hover:text-gray-300"
|
||||||
|
/>
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
onClick={prevLightbox}
|
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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronLeft className="w-6 h-6" />
|
icon={<FaChevronLeft className="h-6 w-6" />}
|
||||||
</button>
|
className="absolute left-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
<button
|
/>
|
||||||
|
<Button
|
||||||
onClick={nextLightbox}
|
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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronRight className="w-6 h-6" />
|
icon={<FaChevronRight className="h-6 w-6" />}
|
||||||
</button>
|
className="absolute right-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<motion.img
|
<motion.img
|
||||||
|
|
@ -475,13 +491,15 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||||
{images.map((_, idx) => (
|
{images.map((_, idx) => (
|
||||||
<button
|
<Button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setLightboxIndex(idx)
|
setLightboxIndex(idx)
|
||||||
}}
|
}}
|
||||||
className={`w-2.5 h-2.5 rounded-full transition-all ${
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className={`!h-2.5 !w-2.5 !min-w-2.5 !px-0 transition-all ${
|
||||||
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
FaPaperPlane,
|
FaPaperPlane,
|
||||||
FaHeart,
|
FaHeart,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
import { EventCommentDto, EventDto } from '@/proxy/intranet/models'
|
import { EventCommentDto, EventDto } from '@/proxy/intranet/models'
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
import useLocale from '@/utils/hooks/useLocale'
|
||||||
|
|
@ -165,10 +165,12 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Button
|
||||||
onClick={handleLike}
|
onClick={handleLike}
|
||||||
disabled={liking}
|
disabled={liking}
|
||||||
className={`flex items-center gap-2 border-0 transition-colors text-xs font-xs border ${
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
className={`!h-auto !items-center gap-2 !rounded !border-0 !px-2 !py-1 text-xs font-xs transition-colors ${
|
||||||
isLiked
|
isLiked
|
||||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
|
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
|
||||||
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||||
|
|
@ -176,7 +178,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
>
|
>
|
||||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
||||||
{likes > 0 && likes}
|
{likes > 0 && likes}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -209,33 +211,36 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
className="w-full h-56 object-contain cursor-zoom-in"
|
className="w-full h-56 object-contain cursor-zoom-in"
|
||||||
onClick={() => openLightbox(activePhoto)}
|
onClick={() => openLightbox(activePhoto)}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={() => openLightbox(activePhoto)}
|
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"
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={<FaExpand className="h-4 w-4" />}
|
||||||
|
className="absolute top-2 right-2 !h-7 !w-7 !rounded-lg !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
title="Tam ekran"
|
title="Tam ekran"
|
||||||
>
|
/>
|
||||||
<FaExpand className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
{photos.length > 1 && (
|
{photos.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setActivePhoto((i) => (i - 1 + photos.length) % photos.length)
|
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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronLeft className="w-4 h-4" />
|
icon={<FaChevronLeft className="h-4 w-4" />}
|
||||||
</button>
|
className="absolute left-2 top-1/2 !h-8 !w-8 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
<button
|
/>
|
||||||
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setActivePhoto((i) => (i + 1) % photos.length)
|
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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronRight className="w-4 h-4" />
|
icon={<FaChevronRight className="h-4 w-4" />}
|
||||||
</button>
|
className="absolute right-2 top-1/2 !h-8 !w-8 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,10 +248,12 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
{photos.length > 1 && (
|
{photos.length > 1 && (
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||||
{photos.map((photo, idx) => (
|
{photos.map((photo, idx) => (
|
||||||
<button
|
<Button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => setActivePhoto(idx)}
|
onClick={() => setActivePhoto(idx)}
|
||||||
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all ${
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`flex-shrink-0 !h-16 !w-16 !rounded-lg !px-0 overflow-hidden border-2 transition-all ${
|
||||||
idx === activePhoto
|
idx === activePhoto
|
||||||
? 'border-green-500 opacity-100'
|
? 'border-green-500 opacity-100'
|
||||||
: 'border-transparent opacity-60 hover:opacity-90'
|
: 'border-transparent opacity-60 hover:opacity-90'
|
||||||
|
|
@ -257,7 +264,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
alt={`Thumbnail ${idx + 1}`}
|
alt={`Thumbnail ${idx + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -331,22 +338,26 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
rows={1}
|
rows={1}
|
||||||
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"
|
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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmitComment}
|
onClick={handleSubmitComment}
|
||||||
disabled={!commentText.trim() || submitting}
|
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"
|
variant="solid"
|
||||||
>
|
color="green-500"
|
||||||
<FaPaperPlane className="w-4 h-4" />
|
shape="none"
|
||||||
</button>
|
icon={<FaPaperPlane className="h-4 w-4" />}
|
||||||
|
className="!h-10 !w-10 flex-shrink-0 !rounded-xl !px-0 text-white transition-colors hover:!bg-green-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
type="button"
|
type="button"
|
||||||
className="px-2 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm font-medium flex-shrink-0"
|
variant="solid"
|
||||||
|
color="green-600"
|
||||||
|
className="!h-auto flex-shrink-0 !rounded-lg !px-2 !py-2 text-sm font-medium text-white transition-colors hover:!bg-green-700"
|
||||||
>
|
>
|
||||||
{translate('::App.Platform.Close')}
|
{translate('::App.Platform.Close')}
|
||||||
</button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
|
|
||||||
{/* Lightbox */}
|
{/* Lightbox */}
|
||||||
|
|
@ -362,26 +373,29 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
onClick={closeLightbox}
|
onClick={closeLightbox}
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
||||||
<button
|
<Button
|
||||||
onClick={closeLightbox}
|
onClick={closeLightbox}
|
||||||
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaTimes className="w-8 h-8" />
|
icon={<FaTimes className="h-8 w-8" />}
|
||||||
</button>
|
className="absolute top-4 right-4 z-10 !h-12 !w-12 !px-0 text-white transition-colors hover:text-gray-300"
|
||||||
|
/>
|
||||||
{photos.length > 1 && (
|
{photos.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
onClick={prevLightbox}
|
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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronLeft className="w-6 h-6" />
|
icon={<FaChevronLeft className="h-6 w-6" />}
|
||||||
</button>
|
className="absolute left-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
<button
|
/>
|
||||||
|
<Button
|
||||||
onClick={nextLightbox}
|
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"
|
variant="plain"
|
||||||
>
|
shape="circle"
|
||||||
<FaChevronRight className="w-6 h-6" />
|
icon={<FaChevronRight className="h-6 w-6" />}
|
||||||
</button>
|
className="absolute right-4 top-1/2 z-10 !h-12 !w-12 -translate-y-1/2 !px-0 bg-black/50 text-white transition-colors hover:!bg-black/70"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<motion.img
|
<motion.img
|
||||||
|
|
@ -397,13 +411,15 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
||||||
{photos.length > 1 && (
|
{photos.length > 1 && (
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||||
{photos.map((_, idx) => (
|
{photos.map((_, idx) => (
|
||||||
<button
|
<Button
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setLightboxIndex(idx)
|
setLightboxIndex(idx)
|
||||||
}}
|
}}
|
||||||
className={`w-2.5 h-2.5 rounded-full transition-all ${
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className={`!h-2.5 !w-2.5 !min-w-2.5 !px-0 transition-all ${
|
||||||
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
idx === lightboxIndex ? 'bg-white' : 'bg-white/40 hover:bg-white/70'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { DocumentDto } from '@/proxy/intranet/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { getFileIcon, getFileType } from '@/proxy/intranet/utils'
|
import { getFileIcon, getFileType } from '@/proxy/intranet/utils'
|
||||||
import { FILE_URL } from '@/constants/app.constant'
|
import { FILE_URL } from '@/constants/app.constant'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 B'
|
if (bytes === 0) return '0 B'
|
||||||
|
|
@ -15,7 +16,7 @@ const formatFileSize = (bytes: number): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents }) => {
|
const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents }) => {
|
||||||
const { translate } = useLocalization();
|
const { translate } = useLocalization()
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
|
@ -51,12 +52,14 @@ const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents })
|
||||||
{doc.isReadOnly && (
|
{doc.isReadOnly && (
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span className="text-orange-500">🔒 {translate('::App.Platform.Intranet.Widgets.RecentDocuments.ReadOnly')}</span>
|
<span className="text-orange-500">
|
||||||
|
🔒 {translate('::App.Platform.Intranet.Widgets.RecentDocuments.ReadOnly')}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const url = FILE_URL(doc.path, doc.tenantId)
|
const url = FILE_URL(doc.path, doc.tenantId)
|
||||||
|
|
@ -69,11 +72,15 @@ const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents })
|
||||||
link.click()
|
link.click()
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
}}
|
}}
|
||||||
className="p-2 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded-lg transition-colors group"
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
icon={
|
||||||
|
<FaDownload className="h-5 w-5 text-gray-600 transition-colors group-hover:text-blue-600 dark:text-gray-400 dark:group-hover:text-blue-400" />
|
||||||
|
}
|
||||||
|
className="group !h-9 !w-9 !rounded-lg !px-0 transition-colors hover:!bg-blue-100 dark:hover:!bg-blue-900/30"
|
||||||
title={translate('::App.Platform.Intranet.Widgets.RecentDocuments.Download')}
|
title={translate('::App.Platform.Intranet.Widgets.RecentDocuments.Download')}
|
||||||
>
|
/>
|
||||||
<FaDownload className="w-5 h-5 text-gray-600 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
import { SurveyAnswerDto, SurveyDto, SurveyQuestionDto } from '@/proxy/intranet/models'
|
import { SurveyAnswerDto, SurveyDto, SurveyQuestionDto } from '@/proxy/intranet/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
@ -95,18 +95,20 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
<button
|
<Button
|
||||||
key={star}
|
key={star}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleAnswerChange(question.id, star)}
|
onClick={() => handleAnswerChange(question.id, star)}
|
||||||
className={`text-2xl transition-colors ${
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className={`!h-8 !w-8 !px-0 text-2xl transition-colors ${
|
||||||
(answers[question.id] ?? 0) >= star
|
(answers[question.id] ?? 0) >= star
|
||||||
? 'text-yellow-400'
|
? 'text-yellow-400'
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-yellow-300'
|
: 'text-gray-300 dark:text-gray-600 hover:text-yellow-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{hasError && (
|
{hasError && (
|
||||||
|
|
@ -273,21 +275,24 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
||||||
</Dialog.Body>
|
</Dialog.Body>
|
||||||
|
|
||||||
<Dialog.Footer className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
<Dialog.Footer className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
variant="default"
|
||||||
|
className="flex-1 !rounded-lg border-gray-300 text-gray-700 transition-colors hover:!bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:!bg-gray-700"
|
||||||
>
|
>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="flex-1 !rounded-lg text-white transition-colors hover:!bg-blue-700"
|
||||||
>
|
>
|
||||||
{isUpdate
|
{isUpdate
|
||||||
? translate('::App.Platform.Intranet.SurveyModal.Update')
|
? translate('::App.Platform.Intranet.SurveyModal.Update')
|
||||||
: translate('::App.Platform.Intranet.SurveyModal.Submit')}
|
: translate('::App.Platform.Intranet.SurveyModal.Submit')}
|
||||||
</button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { SurveyDto } from '@/proxy/intranet/models'
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
import useLocale from '@/utils/hooks/useLocale'
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
import { currentLocalDate } from '@/utils/dateUtils'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
interface SurveysProps {
|
interface SurveysProps {
|
||||||
surveys?: SurveyDto[]
|
surveys?: SurveyDto[]
|
||||||
|
|
@ -43,11 +44,13 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Background gradient on hover */}
|
{/* Background gradient on hover */}
|
||||||
<div className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none ${
|
<div
|
||||||
|
className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20'
|
? 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20'
|
||||||
: 'bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20'
|
: 'bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20'
|
||||||
}`}></div>
|
}`}
|
||||||
|
></div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Survey Title */}
|
{/* Survey Title */}
|
||||||
|
|
@ -55,16 +58,24 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<span className="flex-shrink-0 inline-flex items-center justify-center w-5 h-5 bg-green-500 rounded-full">
|
<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}>
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<h4 className={`text-base font-semibold transition-colors ${
|
<h4
|
||||||
|
className={`text-base font-semibold transition-colors ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'text-green-800 dark:text-green-300 group-hover:text-green-700 dark:group-hover:text-green-200'
|
? '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'
|
: 'text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300'
|
||||||
}`}>
|
}`}
|
||||||
|
>
|
||||||
{survey.title}
|
{survey.title}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,7 +88,11 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{daysLeft > 0 ? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.DaysLeft', { count: daysLeft }) : translate('::App.Platform.Intranet.Widgets.ActiveSurveys.LastDay')}
|
{daysLeft > 0
|
||||||
|
? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.DaysLeft', {
|
||||||
|
count: daysLeft,
|
||||||
|
})
|
||||||
|
: translate('::App.Platform.Intranet.Widgets.ActiveSurveys.LastDay')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,7 +103,9 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
<FaQuestionCircle className="w-3 h-3 text-blue-600 dark:text-blue-300" />
|
<FaQuestionCircle className="w-3 h-3 text-blue-600 dark:text-blue-300" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.Questions')}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.Questions')}
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">
|
<p className="font-semibold text-gray-900 dark:text-white">
|
||||||
{survey.questions.length}
|
{survey.questions.length}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -100,7 +117,9 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
<FaUsers className="w-3 h-3 text-green-600 dark:text-green-300" />
|
<FaUsers className="w-3 h-3 text-green-600 dark:text-green-300" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.Responses')}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.Responses')}
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">
|
<p className="font-semibold text-gray-900 dark:text-white">
|
||||||
{survey.responses}
|
{survey.responses}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -112,7 +131,9 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
<FaClock className="w-3 h-3 text-purple-600 dark:text-purple-300" />
|
<FaClock className="w-3 h-3 text-purple-600 dark:text-purple-300" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.Duration')}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.Duration')}
|
||||||
|
</p>
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">~5dk</p>
|
<p className="font-semibold text-gray-900 dark:text-white">~5dk</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,7 +142,9 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span className="text-gray-600 dark:text-gray-400">{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.CompletionRate')}</span>
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.CompletionRate')}
|
||||||
|
</span>
|
||||||
<span className="text-gray-800 dark:text-gray-100 font-medium">
|
<span className="text-gray-800 dark:text-gray-100 font-medium">
|
||||||
{Math.round((survey.responses / 100) * 100)}%
|
{Math.round((survey.responses / 100) * 100)}%
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -141,16 +164,21 @@ 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 text-white text-sm font-medium rounded-lg transition-all duration-300 transform group-hover:scale-[1.02] shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 dark:focus:ring-purple-400 ${
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
icon={
|
||||||
|
<FaArrowRight className="h-3 w-3 transition-transform group-hover:translate-x-1" />
|
||||||
|
}
|
||||||
|
className={`w-full !h-auto !justify-center gap-2 !rounded-lg !px-4 !py-3 text-sm font-medium text-white shadow-sm transition-all duration-300 hover:shadow-md group-hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-purple-400 ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 dark:from-green-600 dark:to-emerald-600 dark:hover:from-green-700 dark:hover:to-emerald-700'
|
? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 dark:from-green-600 dark:to-emerald-600 dark:hover:from-green-700 dark:hover:to-emerald-700'
|
||||||
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 dark:from-purple-700 dark:to-pink-700 dark:hover:from-purple-800 dark:hover:to-pink-800'
|
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 dark:from-purple-700 dark:to-pink-700 dark:hover:from-purple-800 dark:hover:to-pink-800'
|
||||||
}`}>
|
}`}
|
||||||
|
>
|
||||||
{isCompleted
|
{isCompleted
|
||||||
? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses')
|
? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses')
|
||||||
: translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
|
: translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
|
||||||
<FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactElement, useRef, useState } from 'react'
|
import { ReactElement, useRef, useState } from 'react'
|
||||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
hideImageHoverPreview,
|
hideImageHoverPreview,
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
|
|
@ -119,10 +120,13 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||||
cursor: 'zoom-in',
|
cursor: 'zoom-in',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title="Sil"
|
title="Sil"
|
||||||
onClick={() => removeImage(i)}
|
onClick={() => removeImage(i)}
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className="!absolute !h-[18px] !w-[18px] !bg-red-600 !p-0 !text-[11px] !text-white hover:!bg-red-700 active:!bg-red-800"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -6,
|
top: -6,
|
||||||
|
|
@ -141,7 +145,7 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
@ -153,11 +157,14 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={(e) => handleFiles(e.target.files)}
|
onChange={(e) => handleFiles(e.target.files)}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
title="Görsel Yükle"
|
title="Görsel Yükle"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
|
variant="solid"
|
||||||
|
shape="none"
|
||||||
|
className="!h-auto !rounded !px-2.5 !py-1 !text-[13px]"
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 10px',
|
padding: '4px 10px',
|
||||||
background: uploading ? '#aaa' : '#0078d4',
|
background: uploading ? '#aaa' : '#0078d4',
|
||||||
|
|
@ -176,7 +183,7 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||||
) : (
|
) : (
|
||||||
<FaUpload style={{ fontSize: 14 }} />
|
<FaUpload style={{ fontSize: 14 }} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement } from 'react'
|
||||||
|
import { Button } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
hideImageHoverPreview,
|
hideImageHoverPreview,
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
|
|
@ -156,11 +157,14 @@ const ImageViewerEditorComponent = (templateData: any): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
||||||
{urls.map((url, index) => (
|
{urls.map((url, index) => (
|
||||||
<button
|
<Button
|
||||||
key={`${url}-${index}`}
|
key={`${url}-${index}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openImageInNewTab(url)}
|
onClick={() => openImageInNewTab(url)}
|
||||||
style={{ border: 0, background: 'transparent', padding: 0, cursor: 'zoom-in' }}
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className="!border-0 !bg-transparent !p-0 hover:!bg-transparent active:!bg-transparent dark:hover:!bg-transparent dark:active:!bg-transparent"
|
||||||
|
style={{ cursor: 'zoom-in' }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
|
|
@ -180,7 +184,7 @@ const ImageViewerEditorComponent = (templateData: any): ReactElement => {
|
||||||
currentTarget.src = '/img/others/no-image.png'
|
currentTarget.src = '/img/others/no-image.png'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -149,35 +149,61 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
>
|
>
|
||||||
{isDesignMode && (
|
{isDesignMode && (
|
||||||
<div className="flex gap-2 items-center mr-2">
|
<div className="flex gap-2 items-center mr-2">
|
||||||
<button onClick={openCreateModal} title="New Item">
|
<Button
|
||||||
<FaPlus size={16} className="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300" />
|
onClick={openCreateModal}
|
||||||
</button>
|
title="New Item"
|
||||||
<button onClick={handleDelete} title="Delete Item">
|
size="xs"
|
||||||
<FaTrashAlt size={16} className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300" />
|
variant="plain"
|
||||||
</button>
|
shape="circle"
|
||||||
|
icon={
|
||||||
|
<FaPlus
|
||||||
|
size={16}
|
||||||
|
className="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="!h-6 !w-6 !px-0"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleDelete}
|
||||||
|
title="Delete Item"
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={
|
||||||
|
<FaTrashAlt
|
||||||
|
size={16}
|
||||||
|
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="!h-6 !w-6 !px-0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div className="flex-shrink-0 text-gray-600 dark:text-gray-300 text-xl">
|
<div className="flex-shrink-0 text-gray-600 dark:text-gray-300 text-xl">
|
||||||
{navigationIcon[item.icon || ''] ? (
|
{navigationIcon[item.icon || ''] ? (
|
||||||
React.createElement(navigationIcon[item.icon || ''], { className: 'text-gray-400 dark:text-gray-500' })
|
React.createElement(navigationIcon[item.icon || ''], {
|
||||||
|
className: 'text-gray-400 dark:text-gray-500',
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<FaQuestionCircle className="text-gray-400 dark:text-gray-500" />
|
<FaQuestionCircle className="text-gray-400 dark:text-gray-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openEditModal}
|
onClick={openEditModal}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={`
|
className={`
|
||||||
truncate text-gray-800 dark:text-gray-100 leading-6 text-sm text-left
|
!h-auto !justify-start !rounded-none !px-0 !py-0 truncate text-gray-800 dark:text-gray-100 leading-6 text-sm text-left hover:!bg-transparent active:!bg-transparent focus:!bg-transparent
|
||||||
${item.children && item.children.length > 0 ? 'font-semibold' : 'font-normal'}
|
${item.children && item.children.length > 0 ? 'font-semibold' : 'font-normal'}
|
||||||
${isDesignMode ? 'hover:text-blue-600 dark:hover:text-blue-400' : ''}
|
${isDesignMode ? 'hover:text-blue-600 dark:hover:text-blue-400' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{translate('::' + item.displayName)}
|
{translate('::' + item.displayName)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
|
@ -221,7 +247,9 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
}
|
}
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification title="Başarılı" type="success">
|
<Notification title="Başarılı" type="success">
|
||||||
{modalMode === 'edit' ? translate('::KayitGuncellendi') : translate('::KayitEklendi')}
|
{modalMode === 'edit'
|
||||||
|
? translate('::KayitGuncellendi')
|
||||||
|
: translate('::KayitEklendi')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'top-end' },
|
{ placement: 'top-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -271,11 +299,21 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="URL" className="mb-2">
|
<FormItem label="URL" className="mb-2">
|
||||||
<Field type="text" name="url" component={Input} className="h-8 text-sm px-2 dark:bg-gray-900 dark:text-gray-100" />
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="url"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2 dark:bg-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Icon" className="mb-2">
|
<FormItem label="Icon" className="mb-2">
|
||||||
<Field type="text" name="icon" component={Input} className="h-8 text-sm px-2 dark:bg-gray-900 dark:text-gray-100" />
|
<Field
|
||||||
|
type="text"
|
||||||
|
name="icon"
|
||||||
|
component={Input}
|
||||||
|
className="h-8 text-sm px-2 dark:bg-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Parent Code" className="mb-2">
|
<FormItem label="Parent Code" className="mb-2">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Container } from '@/components/shared'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
export const MenuManager = () => {
|
export const MenuManager = () => {
|
||||||
const { menuItems, setMenuItems, loading, error, refetch, saveMenuData } = useMenuData()
|
const { menuItems, setMenuItems, loading, error, refetch, saveMenuData } = useMenuData()
|
||||||
|
|
@ -70,12 +71,15 @@ export const MenuManager = () => {
|
||||||
<h2 className="text-lg font-semibold">Error Loading Menu</h2>
|
<h2 className="text-lg font-semibold">Error Loading Menu</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">{error}</p>
|
<p className="text-gray-600 dark:text-gray-300 mb-6">{error}</p>
|
||||||
<button
|
<Button
|
||||||
onClick={refetch}
|
onClick={refetch}
|
||||||
className="w-full bg-blue-600 dark:bg-blue-700 text-white py-2 px-4 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors"
|
block
|
||||||
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!rounded-lg !py-2 hover:!bg-blue-700 dark:bg-blue-700 dark:hover:!bg-blue-800 transition-colors"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -94,8 +98,12 @@ export const MenuManager = () => {
|
||||||
{/* Sol kısım: Başlık */}
|
{/* Sol kısım: Başlık */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FaBars size={20} className="text-gray-600 dark:text-gray-300" />
|
<FaBars size={20} className="text-gray-600 dark:text-gray-300" />
|
||||||
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">Menu Manager</h2>
|
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">({menuItems.length} root items)</span>
|
Menu Manager
|
||||||
|
</h2>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
({menuItems.length} root items)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sağ kısım: Design Mode + Save butonu */}
|
{/* Sağ kısım: Design Mode + Save butonu */}
|
||||||
|
|
@ -106,11 +114,13 @@ export const MenuManager = () => {
|
||||||
>
|
>
|
||||||
Design Mode
|
Design Mode
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
onClick={handleToggleDesignMode}
|
onClick={handleToggleDesignMode}
|
||||||
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
className={`
|
className={`
|
||||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
relative !inline-flex !h-6 !w-11 !min-w-11 !justify-start !rounded-full !border-0 !px-0 !items-center transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||||||
${isDesignMode ? 'bg-blue-600 dark:bg-blue-700' : 'bg-gray-200 dark:bg-gray-700'}
|
${isDesignMode ? 'bg-blue-600 hover:!bg-blue-600 dark:bg-blue-700 dark:hover:!bg-blue-700' : 'bg-gray-200 hover:!bg-gray-200 dark:bg-gray-700 dark:hover:!bg-gray-700'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -119,16 +129,18 @@ export const MenuManager = () => {
|
||||||
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!isDesignMode || isSaving}
|
disabled={!isDesignMode || isSaving}
|
||||||
|
size="sm"
|
||||||
|
variant="plain"
|
||||||
className={`
|
className={`
|
||||||
flex items-center gap-2 px-2 py-1 rounded-lg transition-colors
|
!inline-flex !h-auto !items-center !justify-center gap-2 !rounded-lg !px-2 !py-1 transition-colors
|
||||||
${isDesignMode ? 'bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-800 text-white' : 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'}
|
${isDesignMode ? 'bg-green-600 text-white hover:!bg-green-700 dark:bg-green-700 dark:hover:!bg-green-800' : 'bg-gray-300 text-gray-500 dark:bg-gray-700 dark:text-gray-400 cursor-not-allowed'}
|
||||||
${isSaving ? 'opacity-50' : ''}
|
${isSaving ? 'opacity-50' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|
@ -143,7 +155,7 @@ export const MenuManager = () => {
|
||||||
Save Changes
|
Save Changes
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { useLocalization } from "@/utils/hooks/useLocalization";
|
import { useLocalization } from "@/utils/hooks/useLocalization";
|
||||||
import { createDemoAsync } from "@/services/demo.service";
|
import { createDemoAsync } from "@/services/demo.service";
|
||||||
import { DemoDto } from "@/proxy/demo/models";
|
import { DemoDto } from "@/proxy/demo/models";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
interface DemoModalProps {
|
interface DemoModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -112,12 +113,14 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
||||||
<div className="mx-auto bg-white rounded-xl shadow-lg max-w-md w-full relative dark:bg-gray-900">
|
<div className="mx-auto bg-white rounded-xl shadow-lg max-w-md w-full relative dark:bg-gray-900">
|
||||||
<div className="max-w-md w-full bg-gradient-to-br from-blue-50 to-indigo-100 rounded-3xl p-8 text-center border border-blue-200 shadow-xl dark:from-gray-900 dark:to-gray-800 dark:border-gray-700">
|
<div className="max-w-md w-full bg-gradient-to-br from-blue-50 to-indigo-100 rounded-3xl p-8 text-center border border-blue-200 shadow-xl dark:from-gray-900 dark:to-gray-800 dark:border-gray-700">
|
||||||
{/* Kapat Butonu */}
|
{/* Kapat Butonu */}
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsSubmitted(false)}
|
onClick={() => setIsSubmitted(false)}
|
||||||
className="absolute top-4 right-4 text-gray-500 hover:text-gray-800 text-2xl dark:text-gray-400 dark:hover:text-gray-100"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className="absolute top-4 right-4 !h-8 !w-8 !px-0 text-2xl text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<FaCheckCircle className="w-8 h-8 text-white" />
|
<FaCheckCircle className="w-8 h-8 text-white" />
|
||||||
|
|
@ -128,7 +131,7 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
||||||
<p className="text-gray-600 mb-6 dark:text-gray-300">
|
<p className="text-gray-600 mb-6 dark:text-gray-300">
|
||||||
{translate('::Public.demo.resultMessage')}
|
{translate('::Public.demo.resultMessage')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -143,10 +146,12 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-all duration-300"
|
variant="solid"
|
||||||
|
color="blue-600"
|
||||||
|
className="!h-auto !rounded-xl !px-6 !py-3 text-white transition-all duration-300 hover:!bg-blue-700"
|
||||||
>
|
>
|
||||||
{translate('::Public.demo.newDemo')}
|
{translate('::Public.demo.newDemo')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,14 +165,16 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto py-5 z-50 dark:bg-gray-950/70">
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto py-5 z-50 dark:bg-gray-950/70">
|
||||||
<div className="mx-auto bg-white rounded-xl shadow-lg max-w-2xl w-full relative dark:bg-gray-900">
|
<div className="mx-auto bg-white rounded-xl shadow-lg max-w-2xl w-full relative dark:bg-gray-900">
|
||||||
<button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
title="Kapat"
|
title="Kapat"
|
||||||
aria-label="Kapat"
|
aria-label="Kapat"
|
||||||
className="absolute top-4 right-4 text-gray-500 hover:text-gray-800 text-2xl dark:text-gray-400 dark:hover:text-gray-100"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
className="absolute top-4 right-4 !h-8 !w-8 !px-0 text-2xl text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
|
@ -364,13 +371,14 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-300 transform hover:scale-[1.02] hover:shadow-lg flex items-center justify-center gap-2"
|
variant="plain"
|
||||||
|
icon={<FaPaperPlane className="h-5 w-5" />}
|
||||||
|
className="w-full !h-auto !justify-center !rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 !px-6 !py-4 font-semibold text-white transition-all duration-300 hover:scale-[1.02] hover:from-blue-700 hover:to-purple-700 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<FaPaperPlane className="w-5 h-5" />
|
|
||||||
{translate('::Public.demo.send')}
|
{translate('::Public.demo.send')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import SelectableBlock from './designer/SelectableBlock'
|
||||||
import { DesignerSelection } from './designer/types'
|
import { DesignerSelection } from './designer/types'
|
||||||
import { useDesignerState } from './designer/useDesignerState'
|
import { useDesignerState } from './designer/useDesignerState'
|
||||||
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
||||||
import { Notification, toast } from '@/components/ui'
|
import { Button, Notification, toast } from '@/components/ui'
|
||||||
|
|
||||||
interface HomeSlideServiceContent {
|
interface HomeSlideServiceContent {
|
||||||
icon: string
|
icon: string
|
||||||
|
|
@ -926,13 +926,14 @@ const Home: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDesignMode && !isPanelVisible && (
|
{isDesignMode && !isPanelVisible && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsPanelVisible(true)}
|
onClick={() => setIsPanelVisible(true)}
|
||||||
className="fixed right-4 top-1/2 z-40 -translate-y-1/2 rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-xl"
|
variant="plain"
|
||||||
|
className="fixed right-4 top-1/2 z-40 -translate-y-1/2 !h-auto !rounded-full bg-slate-900 !px-4 !py-2 text-sm font-semibold text-white shadow-xl"
|
||||||
>
|
>
|
||||||
{translate('::Public.designer.showPanel')}
|
{translate('::Public.designer.showPanel')}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hero Carousel */}
|
{/* Hero Carousel */}
|
||||||
|
|
@ -1030,29 +1031,33 @@ const Home: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
<button
|
<Button
|
||||||
onClick={prevSlide}
|
onClick={prevSlide}
|
||||||
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white p-4 rounded-full transition-all z-10"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaChevronLeft size={24} />}
|
||||||
|
className="absolute left-4 top-1/2 z-10 -translate-y-1/2 !h-14 !w-14 !px-0 bg-white/10 hover:!bg-white/20 backdrop-blur-sm text-white transition-all"
|
||||||
aria-label="Previous slide"
|
aria-label="Previous slide"
|
||||||
>
|
/>
|
||||||
<FaChevronLeft size={24} />
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={nextSlide}
|
onClick={nextSlide}
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/20 backdrop-blur-sm text-white p-4 rounded-full transition-all z-10"
|
variant="plain"
|
||||||
|
shape="circle"
|
||||||
|
icon={<FaChevronRight size={24} />}
|
||||||
|
className="absolute right-4 top-1/2 z-10 -translate-y-1/2 !h-14 !w-14 !px-0 bg-white/10 hover:!bg-white/20 backdrop-blur-sm text-white transition-all"
|
||||||
aria-label="Next slide"
|
aria-label="Next slide"
|
||||||
>
|
/>
|
||||||
<FaChevronRight size={24} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Slide Indicators */}
|
{/* Slide Indicators */}
|
||||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-3 z-10">
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-3 z-10">
|
||||||
{(content?.slides || []).map((_, index) => (
|
{(content?.slides || []).map((_, index) => (
|
||||||
<button
|
<Button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => setCurrentSlide(index)}
|
onClick={() => setCurrentSlide(index)}
|
||||||
className={`w-3 h-3 rounded-full transition-all ${
|
variant="plain"
|
||||||
index === currentSlide ? 'bg-white w-8' : 'bg-white/50 hover:bg-white/70'
|
shape="circle"
|
||||||
|
className={`!h-3 !min-h-3 !px-0 rounded-full transition-all ${
|
||||||
|
index === currentSlide ? '!w-8 bg-white' : '!w-3 bg-white/50 hover:bg-white/70'
|
||||||
}`}
|
}`}
|
||||||
aria-label={`Go to slide ${index + 1}`}
|
aria-label={`Go to slide ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1062,19 +1067,20 @@ const Home: React.FC = () => {
|
||||||
{isDesignMode && (
|
{isDesignMode && (
|
||||||
<div className="absolute bottom-24 left-1/2 z-20 -translate-x-1/2 flex gap-2 rounded-full bg-black/30 px-3 py-2 backdrop-blur">
|
<div className="absolute bottom-24 left-1/2 z-20 -translate-x-1/2 flex gap-2 rounded-full bg-black/30 px-3 py-2 backdrop-blur">
|
||||||
{(content?.slides || []).map((_, index) => (
|
{(content?.slides || []).map((_, index) => (
|
||||||
<button
|
<Button
|
||||||
key={`editor-slide-${index}`}
|
key={`editor-slide-${index}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentSlide(index)
|
setCurrentSlide(index)
|
||||||
handleSelectBlock(`slide-${index}`)
|
handleSelectBlock(`slide-${index}`)
|
||||||
}}
|
}}
|
||||||
className={`rounded-full px-3 py-1 text-xs font-semibold text-white transition ${
|
variant="plain"
|
||||||
|
className={`!h-auto !rounded-full !px-3 !py-1 text-xs font-semibold text-white transition ${
|
||||||
selectedBlockId === `slide-${index}` ? 'bg-sky-500' : 'bg-white/20 hover:bg-white/30'
|
selectedBlockId === `slide-${index}` ? 'bg-sky-500' : 'bg-white/20 hover:bg-white/30'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Slide {index + 1}
|
Slide {index + 1}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,12 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className="relative">
|
<div ref={wrapperRef} className="relative">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className={`flex items-center gap-2 w-full px-3 py-2 rounded-lg border text-sm text-left transition-colors
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`flex items-center gap-2 w-full !h-auto !justify-start !rounded-lg border !px-3 !py-2 text-sm text-left transition-colors
|
||||||
${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
${invalid ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
||||||
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:border-indigo-400`}
|
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:border-indigo-400`}
|
||||||
>
|
>
|
||||||
|
|
@ -68,7 +70,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
<FaChevronDown
|
<FaChevronDown
|
||||||
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute z-50 mt-1 left-0 w-[360px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl">
|
<div className="absolute z-50 mt-1 left-0 w-[360px] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl">
|
||||||
|
|
@ -84,7 +86,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-10 gap-0.5 p-2 max-h-[208px] overflow-y-auto">
|
<div className="grid grid-cols-10 gap-0.5 p-2 max-h-[208px] overflow-y-auto">
|
||||||
{displayed.map(([key, Icon]) => (
|
{displayed.map(([key, Icon]) => (
|
||||||
<button
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
title={key}
|
title={key}
|
||||||
|
|
@ -93,11 +95,13 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
setSearch('')
|
setSearch('')
|
||||||
}}
|
}}
|
||||||
className={`flex items-center justify-center h-10 w-full rounded text-xl transition-colors
|
variant="plain"
|
||||||
|
shape="none"
|
||||||
|
className={`!flex !h-10 !w-full !min-w-0 !items-center !justify-center !rounded !px-0 text-xl transition-colors
|
||||||
${value === key ? 'bg-indigo-500 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
|
${value === key ? 'bg-indigo-500 text-white' : 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
|
||||||
>
|
>
|
||||||
<Icon />
|
<Icon className="h-5 w-5" />
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
|
|
@ -105,13 +109,16 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{displayed.length} / {filtered.length}
|
{displayed.length} / {filtered.length}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
|
onClick={() => setLimit((l) => l + ICON_PAGE_SIZE)}
|
||||||
className="text-xs px-3 py-1 rounded bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 font-medium"
|
size="xs"
|
||||||
|
variant="twoTone"
|
||||||
|
color="indigo-600"
|
||||||
|
className="!h-auto !px-3 !py-1 !rounded text-xs bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 hover:!bg-indigo-100 dark:hover:!bg-indigo-900/50 font-medium"
|
||||||
>
|
>
|
||||||
Load More
|
Load More
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -265,10 +272,14 @@ export function MenuAddDialog({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelCls}>
|
<label className={labelCls}>
|
||||||
{translate('::App.Platform.Code') || 'Code'}{' '}
|
{translate('::App.Platform.Code') || 'Code'} <span className="text-red-500">*</span>
|
||||||
<span className="text-red-500">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input value={form.code} disabled placeholder="App.Wizard.MyMenu" className={disabledCls} />
|
<input
|
||||||
|
value={form.code}
|
||||||
|
disabled
|
||||||
|
placeholder="App.Wizard.MyMenu"
|
||||||
|
className={disabledCls}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue