<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 Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.Identity;
|
||||
using Volo.Abp.Uow;
|
||||
|
||||
namespace Sozsoft.Platform.Messenger;
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ public class MessengerAppService : ApplicationService
|
|||
private readonly IIdentitySessionRepository _identitySessionRepository;
|
||||
private readonly BlobManager _blobManager;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IHubContext<MessengerHub> _messengerHubContext;
|
||||
|
||||
public MessengerAppService(
|
||||
IRepository<IdentityUser, Guid> userRepository,
|
||||
|
|
@ -36,7 +39,8 @@ public class MessengerAppService : ApplicationService
|
|||
IRepository<MessengerConversationMessage, Guid> messageRepository,
|
||||
IIdentitySessionRepository identitySessionRepository,
|
||||
BlobManager blobManager,
|
||||
IConfiguration configuration)
|
||||
IConfiguration configuration,
|
||||
IHubContext<MessengerHub> messengerHubContext)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_conversationRepository = conversationRepository;
|
||||
|
|
@ -44,6 +48,7 @@ public class MessengerAppService : ApplicationService
|
|||
_identitySessionRepository = identitySessionRepository;
|
||||
_blobManager = blobManager;
|
||||
_configuration = configuration;
|
||||
_messengerHubContext = messengerHubContext;
|
||||
}
|
||||
|
||||
public async Task<List<MessengerContactDto>> GetContactsAsync(string? filter = null)
|
||||
|
|
@ -133,6 +138,7 @@ public class MessengerAppService : ApplicationService
|
|||
return MapConversation(conversation);
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
public async Task<MessengerConversationDto> CreateConversationAsync(MessengerConversationCreateUpdateDto input)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
|
@ -159,6 +165,7 @@ public class MessengerAppService : ApplicationService
|
|||
return MapConversation(conversation);
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
public async Task<MessengerConversationDto> UpdateConversationAsync(Guid id, MessengerConversationCreateUpdateDto input)
|
||||
{
|
||||
var conversation = await _conversationRepository.GetAsync(id);
|
||||
|
|
@ -178,6 +185,7 @@ public class MessengerAppService : ApplicationService
|
|||
return MapConversation(conversation);
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
public async Task DeleteConversationAsync(Guid id)
|
||||
{
|
||||
var conversation = await _conversationRepository.GetAsync(id);
|
||||
|
|
@ -220,6 +228,8 @@ public class MessengerAppService : ApplicationService
|
|||
.ToList();
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
[HttpPost("api/app/messenger/send-message")]
|
||||
public async Task<MessengerMessageDto> SendMessageAsync(MessengerSendMessageDto input)
|
||||
{
|
||||
var senderId = GetCurrentUserId();
|
||||
|
|
@ -271,7 +281,7 @@ public class MessengerAppService : ApplicationService
|
|||
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";
|
||||
|
|
@ -289,17 +299,23 @@ public class MessengerAppService : ApplicationService
|
|||
SentAt = Clock.Now
|
||||
};
|
||||
|
||||
await _messageRepository.InsertAsync(message, autoSave: true);
|
||||
await _messageRepository.InsertAsync(message, autoSave: false);
|
||||
|
||||
conversation.LastSenderId = senderId;
|
||||
conversation.LastMessagePreview = GetMessagePreview(trimmedText, input.Attachments.Count);
|
||||
conversation.LastMessageTime = message.SentAt;
|
||||
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)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
|
@ -335,13 +351,17 @@ public class MessengerAppService : ApplicationService
|
|||
conversation.LastMessageTime = lastMessage?.SentAt;
|
||||
await _conversationRepository.UpdateAsync(conversation, autoSave: true);
|
||||
|
||||
return new MessengerMessageDeletedDto
|
||||
var deletedMessage = new MessengerMessageDeletedDto
|
||||
{
|
||||
MessageId = message.Id,
|
||||
ConversationId = message.ConversationId,
|
||||
SenderId = message.SenderId,
|
||||
RecipientIds = recipientIds
|
||||
};
|
||||
|
||||
await PublishMessageDeletedAsync(deletedMessage);
|
||||
|
||||
return deletedMessage;
|
||||
}
|
||||
|
||||
[HttpPost("api/app/messenger/upload-attachment")]
|
||||
|
|
@ -523,4 +543,26 @@ public class MessengerAppService : ApplicationService
|
|||
|
||||
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)
|
||||
{
|
||||
var message = await _messengerAppService.SendMessageAsync(input);
|
||||
|
||||
var targetGroups = message.RecipientIds
|
||||
.Append(message.SenderId)
|
||||
.Distinct()
|
||||
.Select(UserGroupName)
|
||||
.ToList();
|
||||
|
||||
await Clients.Groups(targetGroups).SendAsync("MessengerMessageReceived", message);
|
||||
try
|
||||
{
|
||||
await _messengerAppService.SendMessageAsync(input);
|
||||
}
|
||||
catch (OperationCanceledException) when (Context.ConnectionAborted.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteMessage(Guid messageId)
|
||||
{
|
||||
var deletedMessage = await _messengerAppService.DeleteMessageAsync(messageId);
|
||||
|
||||
var targetGroups = deletedMessage.RecipientIds
|
||||
.Append(deletedMessage.SenderId)
|
||||
.Distinct()
|
||||
.Select(UserGroupName)
|
||||
.ToList();
|
||||
|
||||
await Clients.Groups(targetGroups).SendAsync("MessengerMessageDeleted", deletedMessage);
|
||||
try
|
||||
{
|
||||
await _messengerAppService.DeleteMessageAsync(messageId);
|
||||
}
|
||||
catch (OperationCanceledException) when (Context.ConnectionAborted.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private string TenantKey => _currentTenant.Id?.ToString("N") ?? "host";
|
||||
|
||||
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.MessageCount).HasDefaultValue(0);
|
||||
|
||||
b.HasIndex(x => new { x.TenantId, x.ParticipantKey }).IsUnique().HasFilter("[IsDeleted] = 0");
|
||||
|
||||
b.HasMany(x => x.Messages)
|
||||
.WithOne(x => x.Conversation)
|
||||
.HasForeignKey(x => x.ConversationId)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260624175615_Initial")]
|
||||
[Migration("20260625183130_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -3904,10 +3904,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId", "ParticipantKey")
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
|
||||
b.ToTable("Adm_T_MessengerConversation", (string)null);
|
||||
});
|
||||
|
||||
|
|
@ -3730,13 +3730,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
unique: true,
|
||||
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(
|
||||
name: "IX_Adm_T_MessengerConversationMessage_ConversationId",
|
||||
table: "Adm_T_MessengerConversationMessage",
|
||||
|
|
@ -3901,10 +3901,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId", "ParticipantKey")
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
|
||||
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 { ComponentDefinition } from '../../proxy/developerKit/componentInfo'
|
||||
import { generateSingleComponentJSX, generateUniqueId } from '@/utils/codeParser'
|
||||
import { FaCheck, FaCode, FaSpinner, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
||||
import { toast } from '../ui'
|
||||
import { FaCheck, FaCode, FaMousePointer, FaSave, FaCog, FaTimes } from 'react-icons/fa'
|
||||
import { Button, toast } from '../ui'
|
||||
import Notification from '../ui/Notification/Notification'
|
||||
|
||||
interface ComponentCodeEditorProps {
|
||||
|
|
@ -560,55 +560,57 @@ export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
|||
{/* Header */}
|
||||
<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">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleFormatCode}
|
||||
disabled={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"
|
||||
loading={isFormatting}
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
className={`px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 ${
|
||||
showSettings
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
icon={<FaCog className="w-4 h-4" />}
|
||||
variant={showSettings ? 'solid' : 'default'}
|
||||
color="blue-600"
|
||||
size="sm"
|
||||
>
|
||||
<FaCog className="w-4 h-4" />
|
||||
Ayarlar
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center justify-end">
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -656,14 +658,15 @@ export const ComponentCodeEditor: React.FC<ComponentCodeEditorProps> = ({
|
|||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Mini Harita</label>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setMinimap(!minimap)}
|
||||
className={`w-full px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
minimap ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
block
|
||||
variant={minimap ? 'solid' : 'default'}
|
||||
color="blue-600"
|
||||
size="sm"
|
||||
>
|
||||
{minimap ? 'Etkin' : 'Devre Dışı'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
FaEye,
|
||||
FaEyeSlash
|
||||
} from 'react-icons/fa';
|
||||
import { Button } from "../ui";
|
||||
import { PanelState } from "./data/componentDefinitions";
|
||||
|
||||
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" />
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">Panel Manager</h2>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<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" />
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{label}</span>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
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"}
|
||||
>
|
||||
{panelState[key] ? <FaEye className="w-4 h-4" /> : <FaEyeSlash className="w-4 h-4" />}
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import TailwindModal from "./TailwindModal";
|
|||
import { ComponentInfo, HookInfo, PropertyInfo } from "../../proxy/developerKit/componentInfo";
|
||||
import { getComponentDefinition } from "./data/componentDefinitions";
|
||||
import { Button } from "../ui";
|
||||
import { FaCheck, FaCode, FaTimes, FaTrash } from "react-icons/fa";
|
||||
|
||||
interface PropertyPanelProps {
|
||||
selectedComponent: ComponentInfo | null;
|
||||
|
|
@ -284,13 +285,16 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||
placeholder={`Enter ${property.name}`}
|
||||
/>
|
||||
{isTailwindProperty && (
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
TW
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{isColorProperty && (
|
||||
<input
|
||||
|
|
@ -317,13 +321,16 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||
placeholder={`Enter ${property.name}`}
|
||||
/>
|
||||
{isTailwindProperty && (
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
TW
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{isColorProperty && (
|
||||
<input
|
||||
|
|
@ -489,33 +496,31 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||
)}
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 p-1">
|
||||
<button
|
||||
className={`px-3 py-1 font-medium border-b-2 transition-colors ${
|
||||
activeTab === "props"
|
||||
? "border-blue-500 text-blue-700 bg-white dark:bg-gray-900 dark:text-blue-400 dark:border-blue-400"
|
||||
: "border-transparent text-gray-500 bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:border-transparent"
|
||||
}`}
|
||||
<Button
|
||||
variant={activeTab === "props" ? "twoTone" : "plain"}
|
||||
color="blue-600"
|
||||
size="xs"
|
||||
onClick={() => setActiveTab("props")}
|
||||
>
|
||||
Properties
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 font-medium border-b-2 transition-colors ${
|
||||
activeTab === "hooks"
|
||||
? "border-blue-500 text-blue-700 bg-white dark:bg-gray-900 dark:text-blue-400 dark:border-blue-400"
|
||||
: "border-transparent text-gray-500 bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:border-transparent"
|
||||
}`}
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === "hooks" ? "twoTone" : "plain"}
|
||||
color="blue-600"
|
||||
size="xs"
|
||||
onClick={() => setActiveTab("hooks")}
|
||||
>
|
||||
Hooks
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Sil Butonu */}
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
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"
|
||||
color="red-600"
|
||||
size="xs"
|
||||
icon={<FaTrash className="w-4 h-4" />}
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
if (selectedComponent) {
|
||||
if (
|
||||
|
|
@ -539,23 +544,23 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||
<Button
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color="green-600"
|
||||
icon={<FaCheck className="w-4 h-4" />}
|
||||
onClick={
|
||||
activeTab === "props"
|
||||
? handleApplyPropChanges
|
||||
: handleApplyHookChanges
|
||||
}
|
||||
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
|
||||
className={`flex-1 rounded-md font-medium transition-colors ${
|
||||
(activeTab === "props" ? hasChanges : hasHookChanges)
|
||||
? "bg-green-500 text-white hover:bg-green-600"
|
||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||
}`}
|
||||
className="flex-1"
|
||||
>
|
||||
Uygula
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
variant="solid"
|
||||
color="red-600"
|
||||
icon={<FaTimes className="w-4 h-4" />}
|
||||
onClick={
|
||||
activeTab === "props"
|
||||
? handleResetChanges
|
||||
|
|
@ -565,11 +570,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||
}
|
||||
}
|
||||
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
|
||||
className={`flex-1 rounded-md font-medium transition-colors ${
|
||||
(activeTab === "props" ? hasChanges : hasHookChanges)
|
||||
? "bg-red-500 text-white hover:bg-red-600"
|
||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||
}`}
|
||||
className="flex-1"
|
||||
>
|
||||
İptal
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { searchTailwindClasses, TAILWIND_CLASSES } from './data/tailwindClasses';
|
||||
import { Button } from '../ui';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
|
||||
interface TailwindModalProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -53,12 +54,13 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
|||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Tailwind CSS Classes</h2>
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
icon={<FaTimes className="w-5 h-5" />}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
title="Close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter */}
|
||||
|
|
@ -108,9 +110,12 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
|||
className="group relative"
|
||||
>
|
||||
<Button
|
||||
variant='default'
|
||||
block
|
||||
variant="twoTone"
|
||||
color="blue-600"
|
||||
size="xs"
|
||||
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">
|
||||
{className}
|
||||
|
|
@ -129,16 +134,17 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
|||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant='default'
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onSelectClass('')}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
|
@ -149,4 +155,4 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default TailwindModal;
|
||||
export default TailwindModal;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { FaUpload, FaFile, FaTimes, FaRegCircle } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface FileUploadAreaProps {
|
||||
onFileUpload: (file: File) => void
|
||||
|
|
@ -149,12 +150,13 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
icon={<FaTimes className="w-4 h-4" />}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
title={translate('::Clear')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -167,23 +169,19 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
|||
)}
|
||||
|
||||
{selectedFile && !error && (
|
||||
<button
|
||||
<Button
|
||||
onClick={uploadFile}
|
||||
disabled={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"
|
||||
loading={loading}
|
||||
block
|
||||
icon={<FaUpload className="w-4 h-4" />}
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
size="sm"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>{translate('::App.Uploading')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaUpload className="w-4 h-4" />
|
||||
<span>{translate('::App.Listforms.ImportManager.UploadFile')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{loading
|
||||
? translate('::App.Uploading')
|
||||
: translate('::App.Listforms.ImportManager.UploadFile')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { ImportService } from '@/services/import.service'
|
|||
import { GridDto } from '@/proxy/form/models'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface ImportDashboardProps {
|
||||
gridDto: GridDto
|
||||
|
|
@ -333,20 +334,25 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
|||
{/* 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">
|
||||
{['import', 'preview', 'history'].map((tab) => (
|
||||
<button
|
||||
<Button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab as TabNames)}
|
||||
className={`px-3 py-2 rounded-md font-medium transition-all duration-200 flex items-center space-x-2 ${
|
||||
activeTab === tab
|
||||
? 'bg-blue-500 text-white shadow-md'
|
||||
: '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'
|
||||
}`}
|
||||
icon={
|
||||
tab === 'import' ? (
|
||||
<FaUpload className="w-4 h-4" />
|
||||
) : 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>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -368,27 +374,27 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
|||
|
||||
{/* Template Options */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => generateTemplate('excel')}
|
||||
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')}
|
||||
</span>
|
||||
</button>
|
||||
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => generateTemplate('csv')}
|
||||
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')}
|
||||
</span>
|
||||
</button>
|
||||
{translate('::App.Listforms.ImportManager.CsvTemplate')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -567,18 +573,15 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
|||
</span>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => toggleSessionExecutes(session.id)}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
expandedSessions.has(session.id)
|
||||
? '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'
|
||||
: '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'
|
||||
}`}
|
||||
icon={<FaEye className="w-4 h-4" />}
|
||||
variant={expandedSessions.has(session.id) ? 'twoTone' : 'plain'}
|
||||
color="red-600"
|
||||
size="xs"
|
||||
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// Execute bilgilerini manuel olarak yenile
|
||||
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')}
|
||||
>
|
||||
<FaSync className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (currentSession?.id === session.id) {
|
||||
return // Don't delete if it's the current session
|
||||
|
|
@ -616,19 +620,16 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
|||
await loadImportHistory()
|
||||
}}
|
||||
disabled={currentSession?.id === session.id}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
currentSession?.id === session.id
|
||||
? 'text-gray-300 dark:text-gray-600 cursor-not-allowed'
|
||||
: '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'
|
||||
}`}
|
||||
icon={<FaTrashAlt className="w-4 h-4" />}
|
||||
variant="twoTone"
|
||||
color="red-600"
|
||||
size="xs"
|
||||
title={
|
||||
currentSession?.id === session.id
|
||||
? translate('::App.Listforms.ImportManager.CannotDeleteActiveSession')
|
||||
: translate('::App.Listforms.ImportManager.DeleteImportSession')
|
||||
}
|
||||
>
|
||||
<FaTrashAlt className="w-4 h-4" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -730,20 +731,24 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
|||
execute.status === 'failed') &&
|
||||
execute.errorRows > 0 && (
|
||||
<div className="mt-2">
|
||||
<button
|
||||
<Button
|
||||
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) ? (
|
||||
<FaChevronUp className="w-3 h-3" />
|
||||
) : (
|
||||
<FaChevronDown className="w-3 h-3" />
|
||||
)
|
||||
}
|
||||
variant="twoTone"
|
||||
color="orange-600"
|
||||
size="xs"
|
||||
>
|
||||
{expandedErrors.has(execute.id) ? (
|
||||
<FaChevronUp className="w-3 h-3" />
|
||||
) : (
|
||||
<FaChevronDown className="w-3 h-3" />
|
||||
)}
|
||||
<span>
|
||||
{execute.errorRows} hata detayı
|
||||
{expandedErrors.has(execute.id) ? ' gizle' : ' göster'}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{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">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ListFormImportDto } from '@/proxy/imports/models'
|
|||
import { GridDto } from '@/proxy/form/models'
|
||||
import { ImportService } from '@/services/import.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface ImportPreviewProps {
|
||||
session: ListFormImportDto
|
||||
|
|
@ -314,28 +315,27 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<FaTimes className="w-4 h-4" />
|
||||
<span>{translate('::Cancel')}</span>
|
||||
</button>
|
||||
<Button
|
||||
icon={<FaTimes className="w-4 h-4" />}
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
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 ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>{translate('::App.Listforms.Status.Processing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaPlay className="w-4 h-4" />
|
||||
<span>{translate('::App.Listforms.ImportManager.Button.ExecuteImport')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{loading
|
||||
? translate('::App.Listforms.Status.Processing')
|
||||
: translate('::App.Listforms.ImportManager.Button.ExecuteImport')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import Demo from '@/views/public/Demo'
|
|||
import { ScrollContext } from '@/contexts/ScrollContext'
|
||||
import useDarkMode from '@/utils/hooks/useDarkmode'
|
||||
import { THEME_ENUM } from '@/constants/theme.constant'
|
||||
import { Button } from '../ui'
|
||||
|
||||
interface NavLink {
|
||||
resourceKey?: string
|
||||
|
|
@ -108,13 +109,15 @@ const PublicLayout = () => {
|
|||
const isLoginLink = (resourceKey?: string) => resourceKey === 'Public.nav.giris'
|
||||
|
||||
const demoButton = (className = '', onClick?: () => void) => (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsDemoOpen(true)
|
||||
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="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')}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
|
||||
const themeToggle = (
|
||||
<div className="inline-flex items-center rounded-lg p-0.5">
|
||||
<button
|
||||
<div className="inline-flex h-9 items-center rounded-lg p-0.5">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
|
||||
aria-pressed={!isDarkMode}
|
||||
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 ${
|
||||
!isDarkMode ? 'bg-white text-gray-950' : 'text-gray-300 hover:text-white'
|
||||
icon={<LuSun size={15} strokeWidth={1.8} />}
|
||||
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"
|
||||
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
|
||||
aria-pressed={isDarkMode}
|
||||
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 ${
|
||||
isDarkMode ? 'bg-white text-gray-950' : 'text-gray-300 hover:text-white'
|
||||
icon={<LuMoon size={15} strokeWidth={1.8} />}
|
||||
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>
|
||||
)
|
||||
|
||||
|
|
@ -207,9 +216,11 @@ const PublicLayout = () => {
|
|||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
key={link.name}
|
||||
onClick={link.action}
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={`${baseClass} ${activeClass}`}
|
||||
>
|
||||
{link.icon && (
|
||||
|
|
@ -221,20 +232,21 @@ const PublicLayout = () => {
|
|||
)}
|
||||
{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" />
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 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="inline-flex items-center gap-1">
|
||||
{demoButton('h-9 px-3 rounded-lg')}
|
||||
<div className="w-px h-5 bg-white/20"></div>
|
||||
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
|
||||
{demoButton('rounded-lg')}
|
||||
<div className="h-5 w-px bg-white/20" />
|
||||
<div className="flex h-9 w-9 items-center justify-center">
|
||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||
</div>
|
||||
<div className="h-5 w-px bg-white/20" />
|
||||
{themeToggle}
|
||||
<div className="w-px h-5 bg-white/20" />
|
||||
<div className="h-5 w-px bg-white/20" />
|
||||
{navLinks
|
||||
.filter((l) => isLoginLink(l.resourceKey))
|
||||
.map((link) =>
|
||||
|
|
@ -242,7 +254,7 @@ const PublicLayout = () => {
|
|||
<Link
|
||||
key={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>
|
||||
|
|
@ -251,14 +263,21 @@ const PublicLayout = () => {
|
|||
</div>
|
||||
|
||||
{/* 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"
|
||||
onClick={toggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />}
|
||||
</button>
|
||||
icon={
|
||||
isOpen ? (
|
||||
<LuX size={20} strokeWidth={2} />
|
||||
) : (
|
||||
<LuMenu size={20} strokeWidth={2} />
|
||||
)
|
||||
}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
|
|
@ -309,26 +328,28 @@ const PublicLayout = () => {
|
|||
{link.name}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
key={link.name}
|
||||
onClick={() => {
|
||||
link.action?.()
|
||||
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"
|
||||
>
|
||||
{link.icon && (
|
||||
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
|
||||
)}
|
||||
{link.name}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<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 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">
|
||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { BillingCycle } from '@/proxy/order/models'
|
|||
import { CartState } from '@/utils/cartUtils'
|
||||
import { useScroll } from '@/contexts/ScrollContext'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface BillingControlsProps {
|
||||
globalBillingCycle: BillingCycle
|
||||
|
|
@ -48,38 +49,32 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => !hasCartItems && setGlobalBillingCycle('monthly')}
|
||||
disabled={hasCartItems}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
globalBillingCycle === 'monthly'
|
||||
? 'bg-blue-600 text-white shadow-md'
|
||||
: hasCartItems
|
||||
? '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'
|
||||
}`}
|
||||
variant={globalBillingCycle === 'monthly' ? 'solid' : 'default'}
|
||||
color="blue-600"
|
||||
size="sm"
|
||||
className={globalBillingCycle === 'monthly' ? 'shadow-md' : ''}
|
||||
title={
|
||||
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
||||
}
|
||||
>
|
||||
{translate('::Public.products.billingcycle.monthly')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => !hasCartItems && setGlobalBillingCycle('yearly')}
|
||||
disabled={hasCartItems}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
globalBillingCycle === 'yearly'
|
||||
? 'bg-blue-600 text-white shadow-md'
|
||||
: hasCartItems
|
||||
? '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'
|
||||
}`}
|
||||
variant={globalBillingCycle === 'yearly' ? 'solid' : 'default'}
|
||||
color="blue-600"
|
||||
size="sm"
|
||||
className={globalBillingCycle === 'yearly' ? 'shadow-md' : ''}
|
||||
title={
|
||||
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
|
||||
}
|
||||
>
|
||||
{translate('::Public.products.billingcycle.yearly')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -93,20 +88,16 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
<Button
|
||||
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}
|
||||
icon={<FaMinus className="w-4 h-4" />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
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="text-sm text-gray-600 dark:text-gray-300">
|
||||
{globalBillingCycle === 'monthly'
|
||||
|
|
@ -115,26 +106,25 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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}
|
||||
icon={<FaPlus className="w-4 h-4" />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
title={hasCartItems ? 'Sepette ürün varken periyod değiştirilemez' : undefined}
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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>
|
||||
|
||||
{cartItemsCount > 0 && (
|
||||
|
|
@ -142,7 +132,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
|||
{cartItemsCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
|||
import { FaTimes, FaMinus, FaPlus, FaShoppingBag, FaTrash } from 'react-icons/fa'
|
||||
import { BillingCycle, BasketItem } from '@/proxy/order/models'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface CartState {
|
||||
items: BasketItem[]
|
||||
|
|
@ -59,20 +60,21 @@ export const Cart: React.FC<CartProps> = ({
|
|||
</h2>
|
||||
<div className="flex items-center space-x-2">
|
||||
{cartState.items.length > 0 && (
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
>
|
||||
<FaTrash className="w-5 h-5" />
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors dark:text-gray-200 dark:hover:bg-gray-800"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
icon={<FaTimes className="w-5 h-5" />}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
/>
|
||||
</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">
|
||||
{translate('::' + item.product.name)}
|
||||
</h4>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => removeItem(item.product.id)}
|
||||
className="text-red-500 hover:text-red-700 ml-2"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
icon={<FaTimes className="w-4 h-4" />}
|
||||
variant="plain"
|
||||
color="red-600"
|
||||
size="xs"
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -125,19 +129,19 @@ export const Cart: React.FC<CartProps> = ({
|
|||
|
||||
{item.product.isQuantityBased && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaMinus className="w-3 h-3" />
|
||||
</button>
|
||||
icon={<FaMinus className="w-3 h-3" />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
/>
|
||||
<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)}
|
||||
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"
|
||||
>
|
||||
<FaPlus className="w-3 h-3" />
|
||||
</button>
|
||||
icon={<FaPlus className="w-3 h-3" />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -162,12 +166,15 @@ export const Cart: React.FC<CartProps> = ({
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { FaCheckCircle, FaArrowLeft } from 'react-icons/fa'
|
|||
import { useNavigate } from 'react-router-dom'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface OrderSuccessProps {
|
||||
orderId: string
|
||||
|
|
@ -40,13 +41,14 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, onBackToSho
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-center justify-center">
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
import { OrderService } from '@/services/order.service'
|
||||
import { CustomTenantDto } from '@/proxy/config/models'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface CartState {
|
||||
items: BasketItem[]
|
||||
|
|
@ -443,23 +444,26 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
|
|||
|
||||
{/* Butonlar */}
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
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'
|
||||
? translate('::Public.payment.buttons.completeOrder')
|
||||
: translate('::Public.payment.buttons.pay')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { FaPlus, FaMinus } from 'react-icons/fa'
|
|||
import { BillingCycle, Product, ProductDto } from '@/proxy/order/models'
|
||||
import { CartState } from '@/utils/cartUtils'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface ProductCardProps {
|
||||
product: ProductDto
|
||||
|
|
@ -119,19 +120,19 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|||
{translate('::Public.products.quantity')}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaMinus className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="w-12 text-center font-medium dark:text-gray-100">{quantity}</span>
|
||||
<button
|
||||
icon={<FaMinus className="w-4 h-4" />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
/>
|
||||
<span className="w-12 text-center font-sm dark:text-gray-100">{quantity}</span>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
</button>
|
||||
icon={<FaPlus className="w-4 h-4" />}
|
||||
variant="default"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -165,17 +166,17 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|||
const isDisabled = isNonQuantityProduct && isInCart
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
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 ${
|
||||
isDisabled
|
||||
? 'bg-gray-400 text-gray-700 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400'
|
||||
: 'bg-blue-600 hover:bg-blue-700 text-white hover:scale-[1.02] active:scale-[0.98]'
|
||||
}`}
|
||||
block
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
size="md"
|
||||
className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''}
|
||||
>
|
||||
{isDisabled ? translate('::Public.products.inCart') : translate('::Public.products.addToCart')}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { BillingCycle, ProductDto } from '@/proxy/order/models'
|
|||
import { OrderService } from '@/services/order.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Loading } from '../shared'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface ProductCatalogProps {
|
||||
searchQuery: string
|
||||
|
|
@ -88,7 +89,7 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
|
|||
return (
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
<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">
|
||||
<FaFilter className="w-5 h-5 text-gray-600 dark:text-gray-300" />
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
|
|
@ -97,30 +98,32 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
|
|||
</div>
|
||||
<div className="space-y-2">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
<Button
|
||||
key={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 ${
|
||||
selectedCategory === category
|
||||
? 'bg-blue-100 text-blue-800 border border-blue-200'
|
||||
: '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'
|
||||
}`}
|
||||
block
|
||||
variant={selectedCategory === category ? 'twoTone' : 'plain'}
|
||||
color="blue-600"
|
||||
size="md"
|
||||
className="justify-start text-left"
|
||||
>
|
||||
<span>
|
||||
{category === 'all'
|
||||
? translate('::Public.products.categories.all')
|
||||
: translate('::' + category)}
|
||||
<span className="flex w-full items-center justify-between gap-3">
|
||||
<span className="min-w-0 truncate">
|
||||
{category === 'all'
|
||||
? translate('::Public.products.categories.all')
|
||||
: translate('::' + category)}
|
||||
</span>
|
||||
<span
|
||||
className={`shrink-0 rounded-full px-2 py-0.5 text-xs ${
|
||||
selectedCategory === category
|
||||
? 'bg-blue-200 text-blue-800'
|
||||
: 'bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{getCategoryCount(category)}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded-full ${
|
||||
selectedCategory === category
|
||||
? 'bg-blue-200 text-blue-800'
|
||||
: 'bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{getCategoryCount(category)}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import React, { useState } from 'react'
|
|||
import { useNavigate } from 'react-router-dom'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface TenantFormProps {
|
||||
onSubmit: (tenant: CustomTenantDto) => void
|
||||
|
|
@ -84,9 +85,10 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
? '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'
|
||||
|
|
@ -98,11 +100,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{translate('::Public.products.tenantForm.existing.desc')}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
? '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'
|
||||
|
|
@ -114,7 +117,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{translate('::Public.products.tenantForm.new.desc')}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
</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">
|
||||
{translate('::Public.payment.customer.code')}
|
||||
</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">
|
||||
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
|
|
@ -143,17 +146,20 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
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>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{formData.organizationName && (
|
||||
|
|
@ -276,12 +282,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="Enter your organization name"
|
||||
value={formData.organizationName || ''}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -292,7 +298,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="Enter founder's name"
|
||||
value={formData.founder || ''}
|
||||
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>
|
||||
|
|
@ -300,7 +306,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -311,12 +317,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="+90 (___) ___-____"
|
||||
value={formData.phoneNumber || ''}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -327,14 +333,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="sample@email.com"
|
||||
value={formData.email || ''}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -345,14 +351,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
value={formData.address1 || ''}
|
||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||
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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -362,12 +368,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="Türkiye"
|
||||
value={formData.country || ''}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -378,7 +384,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="İstanbul"
|
||||
value={formData.city || ''}
|
||||
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>
|
||||
|
|
@ -386,7 +392,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -397,12 +403,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="Çekmeköy"
|
||||
value={formData.district || ''}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -413,7 +419,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="34782"
|
||||
value={formData.postalCode || ''}
|
||||
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>
|
||||
|
|
@ -421,7 +427,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -432,12 +438,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="Kozyatağı"
|
||||
value={formData.taxOffice}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -448,14 +454,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder="1234567890"
|
||||
value={formData.vknTckn}
|
||||
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>
|
||||
<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')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -465,7 +471,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
placeholder={translate('::Public.payment.customer.reference')}
|
||||
value={formData.reference || ''}
|
||||
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>
|
||||
|
|
@ -473,21 +479,24 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
|
|||
)}
|
||||
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { MigrateLogEntry } from '@/proxy/setup/models'
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface DbMigrateLogPanelProps {
|
||||
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">
|
||||
<span className="text-white font-semibold text-sm">DB Migration Logs</span>
|
||||
{done && (
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<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 { AVATAR_URL } from '@/constants/app.constant'
|
||||
import {
|
||||
deleteMessengerMessage,
|
||||
getMessengerContacts,
|
||||
getMessengerMessages,
|
||||
MessengerAttachmentDto,
|
||||
MessengerContactDto,
|
||||
MessengerMessageDto,
|
||||
sendMessengerMessage,
|
||||
uploadMessengerAttachment,
|
||||
} from '@/services/messenger.service'
|
||||
import { messengerSignalR } from '@/services/messenger.signalr'
|
||||
|
|
@ -43,6 +45,25 @@ const formatFileSize = (size: number) => {
|
|||
const canDeleteMessage = (message: MessengerMessageDto, userId: string) =>
|
||||
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 auth = useStoreState((state) => state.auth)
|
||||
const tenantId = auth.user.tenantId || auth.tenant?.tenantId
|
||||
|
|
@ -64,6 +85,7 @@ const MessengerWidget = () => {
|
|||
const [multiSelect, setMultiSelect] = useState(false)
|
||||
const [maximized, setMaximized] = useState(false)
|
||||
const [unreadByContact, setUnreadByContact] = useState<Record<string, number>>({})
|
||||
const [reachableContactIds, setReachableContactIds] = useState<string[]>([])
|
||||
|
||||
const unread = useMemo(
|
||||
() => Object.values(unreadByContact).reduce((total, count) => total + count, 0),
|
||||
|
|
@ -74,9 +96,17 @@ const MessengerWidget = () => {
|
|||
if (!auth.session.signedIn) return
|
||||
|
||||
const unsubscribeMessage = messengerSignalR.onMessage((message) => {
|
||||
setMessages((prev) =>
|
||||
prev.some((item) => item.id === message.id) ? prev : [...prev, message],
|
||||
)
|
||||
setMessages((prev) => mergeMessages(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))) {
|
||||
setUnreadByContact((current) => ({
|
||||
...current,
|
||||
|
|
@ -148,8 +178,8 @@ const MessengerWidget = () => {
|
|||
maxResultCount: 50,
|
||||
sorting: 'sentAt desc',
|
||||
})
|
||||
.then((response) => setMessages(response.data))
|
||||
.catch(() => setMessages([]))
|
||||
.then((response) => setMessages((prev) => mergeMessages(prev, response.data)))
|
||||
.catch(() => undefined)
|
||||
}, [open, selectedIds])
|
||||
|
||||
const selectedContacts = useMemo(
|
||||
|
|
@ -158,7 +188,11 @@ const MessengerWidget = () => {
|
|||
)
|
||||
|
||||
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(() => {
|
||||
if (selectedIds.length === 0) return []
|
||||
|
|
@ -175,7 +209,8 @@ const MessengerWidget = () => {
|
|||
|
||||
const toggleContact = (id: string) => {
|
||||
const contact = contacts.find((item) => item.id === id)
|
||||
if (!contact?.isOnline) {
|
||||
const canReachContact = contact?.isOnline || reachableContactIds.includes(id)
|
||||
if (!canReachContact) {
|
||||
toast.push(
|
||||
<Notification
|
||||
title={translate('::MessengerWidget.OfflineUsersCannotReceiveMessages')}
|
||||
|
|
@ -219,7 +254,6 @@ const MessengerWidget = () => {
|
|||
setOpen(false)
|
||||
setMaximized(false)
|
||||
setSelectedIds([])
|
||||
setMessages([])
|
||||
setText('')
|
||||
setAttachments([])
|
||||
setShowEmoji(false)
|
||||
|
|
@ -277,11 +311,12 @@ const MessengerWidget = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
await messengerSignalR.sendMessage({
|
||||
const response = await sendMessengerMessage({
|
||||
recipientIds: selectedIds,
|
||||
text: trimmedText,
|
||||
attachments,
|
||||
})
|
||||
setMessages((prev) => mergeMessages(prev, [response.data]))
|
||||
setText('')
|
||||
setAttachments([])
|
||||
setShowEmoji(false)
|
||||
|
|
@ -300,7 +335,8 @@ const MessengerWidget = () => {
|
|||
if (!window.confirm(translate('::MessengerWidget.DeleteMessageConfirmation'))) return
|
||||
|
||||
try {
|
||||
await messengerSignalR.deleteMessage(messageId)
|
||||
const response = await deleteMessengerMessage(messageId)
|
||||
setMessages((prev) => prev.filter((item) => item.id !== response.data.messageId))
|
||||
} catch {
|
||||
toast.push(
|
||||
<Notification title={translate('::MessengerWidget.MessageDeleteFailed')} type="danger" />,
|
||||
|
|
@ -325,10 +361,12 @@ const MessengerWidget = () => {
|
|||
<>
|
||||
{open && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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}
|
||||
/>
|
||||
<div className={panelClassName} role="dialog" aria-modal="true">
|
||||
|
|
@ -420,16 +458,20 @@ const MessengerWidget = () => {
|
|||
{contacts.map((contact) => {
|
||||
const active = selectedIds.includes(contact.id)
|
||||
const contactUnread = unreadByContact[contact.id] || 0
|
||||
const isReachable = contact.isOnline || reachableContactIds.includes(contact.id)
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
key={contact.id}
|
||||
type="button"
|
||||
onClick={() => toggleContact(contact.id)}
|
||||
disabled={!contact.isOnline}
|
||||
className={`mb-1 flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition ${
|
||||
disabled={!isReachable}
|
||||
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
|
||||
? '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'
|
||||
: 'cursor-not-allowed opacity-60'
|
||||
}`}
|
||||
|
|
@ -445,15 +487,15 @@ const MessengerWidget = () => {
|
|||
</span>
|
||||
<span
|
||||
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
|
||||
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.Offline')}
|
||||
</span>
|
||||
|
|
@ -466,7 +508,7 @@ const MessengerWidget = () => {
|
|||
{active && contactUnread === 0 && (
|
||||
<IoCheckmarkDone className="shrink-0 text-lg" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -628,14 +670,16 @@ const MessengerWidget = () => {
|
|||
</div>
|
||||
</div>
|
||||
{own && deletable && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
</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"
|
||||
>
|
||||
<span className="truncate">{file.fileName}</span>
|
||||
<button
|
||||
<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={() =>
|
||||
setAttachments((prev) =>
|
||||
prev.filter((item) => item.savedFileName !== file.savedFileName),
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -746,18 +793,23 @@ const MessengerWidget = () => {
|
|||
|
||||
<div className="fixed bottom-5 right-5 z-[52]">
|
||||
<Tooltip title={translate('::MessengerWidget.Title')}>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<IoChatbubbleEllipsesOutline />
|
||||
<span className="pointer-events-auto relative inline-flex">
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
color="emerald-600"
|
||||
shape="circle"
|
||||
icon={<IoChatbubbleEllipsesOutline />}
|
||||
onClick={() => (open ? closeWidget() : setOpen(true))}
|
||||
className="!h-12 !w-12 !px-0 text-2xl shadow-xl transition hover:!bg-emerald-700"
|
||||
/>
|
||||
{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">
|
||||
{unread > 99 ? '99+' : unread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ export interface MessengerGetMessagesInput {
|
|||
sorting?: string
|
||||
}
|
||||
|
||||
export interface MessengerSendMessageDto {
|
||||
conversationId?: string
|
||||
recipientIds: string[]
|
||||
text?: string
|
||||
attachments: MessengerAttachmentDto[]
|
||||
}
|
||||
|
||||
export interface MessengerMessageDto {
|
||||
id: string
|
||||
tenantId?: string
|
||||
|
|
@ -62,6 +69,19 @@ export const getMessengerMessages = (data: MessengerGetMessagesInput) =>
|
|||
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) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { store } from '@/store/store'
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
import { MessengerAttachmentDto, MessengerMessageDeletedDto, MessengerMessageDto } from './messenger.service'
|
||||
|
||||
export interface MessengerSendMessageDto {
|
||||
conversationId?: string
|
||||
recipientIds: string[]
|
||||
text?: string
|
||||
attachments: MessengerAttachmentDto[]
|
||||
}
|
||||
import {
|
||||
MessengerMessageDeletedDto,
|
||||
MessengerMessageDto,
|
||||
MessengerSendMessageDto,
|
||||
} from './messenger.service'
|
||||
|
||||
type MessageHandler = (message: MessengerMessageDto) => void
|
||||
type MessageDeletedHandler = (message: MessengerMessageDeletedDto) => void
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import Loading from '@/components/shared/Loading'
|
||||
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 { useStoreState } from '@/store'
|
||||
import UserProfileCard from '@/views/intranet/SocialWall/UserProfileCard'
|
||||
|
|
@ -32,20 +32,20 @@ type ViewMode = 'department' | 'jobPosition'
|
|||
// ---- JPG Export helpers ----
|
||||
// Tailwind class → hex (keep in sync with LEVEL_COLORS below)
|
||||
const TW_HEX: Record<string, string> = {
|
||||
'bg-blue-500': '#3b82f6',
|
||||
'bg-blue-500': '#3b82f6',
|
||||
'bg-indigo-500': '#6366f1',
|
||||
'bg-cyan-500': '#06b6d4',
|
||||
'bg-teal-500': '#14b8a6',
|
||||
'bg-cyan-500': '#06b6d4',
|
||||
'bg-teal-500': '#14b8a6',
|
||||
'bg-violet-500': '#8b5cf6',
|
||||
'bg-sky-500': '#0ea5e9',
|
||||
'bg-slate-500': '#64748b',
|
||||
'border-blue-200': '#bfdbfe',
|
||||
'bg-sky-500': '#0ea5e9',
|
||||
'bg-slate-500': '#64748b',
|
||||
'border-blue-200': '#bfdbfe',
|
||||
'border-indigo-200': '#c7d2fe',
|
||||
'border-cyan-200': '#a5f3fc',
|
||||
'border-teal-200': '#99f6e4',
|
||||
'border-cyan-200': '#a5f3fc',
|
||||
'border-teal-200': '#99f6e4',
|
||||
'border-violet-200': '#ddd6fe',
|
||||
'border-sky-200': '#bae6fd',
|
||||
'border-slate-200': '#e2e8f0',
|
||||
'border-sky-200': '#bae6fd',
|
||||
'border-slate-200': '#e2e8f0',
|
||||
}
|
||||
|
||||
function loadImg(src: string): Promise<HTMLImageElement | null> {
|
||||
|
|
@ -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.moveTo(x + r, y)
|
||||
ctx.lineTo(x + w - r, y)
|
||||
|
|
@ -73,7 +80,14 @@ function drawRR(ctx: CanvasRenderingContext2D, x: number, y: number, w: number,
|
|||
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.moveTo(x + r, y)
|
||||
ctx.lineTo(x + w - r, y)
|
||||
|
|
@ -233,7 +247,13 @@ async function captureAsJpeg(element: HTMLElement, filename: string, zoom: numbe
|
|||
} else {
|
||||
const nameSpan = row.querySelector('[data-user-name]') as HTMLElement | null
|
||||
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.font = `bold 8px sans-serif`
|
||||
ctx.fillStyle = '#4f46e5'
|
||||
|
|
@ -276,13 +296,13 @@ async function captureAsJpeg(element: HTMLElement, filename: string, zoom: numbe
|
|||
// ---- end JPG Export helpers ----
|
||||
|
||||
const LEVEL_COLORS = [
|
||||
{ border: 'border-blue-200', header: 'bg-blue-500', badge: 'bg-blue-50 text-blue-600' },
|
||||
{ border: 'border-indigo-200', header: 'bg-indigo-500', badge: 'bg-indigo-50 text-indigo-600' },
|
||||
{ border: 'border-cyan-200', header: 'bg-cyan-500', badge: 'bg-cyan-50 text-cyan-700' },
|
||||
{ border: 'border-teal-200', header: 'bg-teal-500', badge: 'bg-teal-50 text-teal-700' },
|
||||
{ border: 'border-violet-200', header: 'bg-violet-500', badge: 'bg-violet-50 text-violet-600' },
|
||||
{ border: 'border-sky-200', header: 'bg-sky-500', badge: 'bg-sky-50 text-sky-700' },
|
||||
{ border: 'border-slate-200', header: 'bg-slate-500', badge: 'bg-slate-50 text-slate-600' },
|
||||
{ border: 'border-blue-200', header: 'bg-blue-500', badge: 'bg-blue-50 text-blue-600' },
|
||||
{ border: 'border-indigo-200', header: 'bg-indigo-500', badge: 'bg-indigo-50 text-indigo-600' },
|
||||
{ border: 'border-cyan-200', header: 'bg-cyan-500', badge: 'bg-cyan-50 text-cyan-700' },
|
||||
{ border: 'border-teal-200', header: 'bg-teal-500', badge: 'bg-teal-50 text-teal-700' },
|
||||
{ border: 'border-violet-200', header: 'bg-violet-500', badge: 'bg-violet-50 text-violet-600' },
|
||||
{ border: 'border-sky-200', header: 'bg-sky-500', badge: 'bg-sky-50 text-sky-700' },
|
||||
{ border: 'border-slate-200', header: 'bg-slate-500', badge: 'bg-slate-50 text-slate-600' },
|
||||
]
|
||||
|
||||
interface TreeNode extends OrgChartNodeDto {
|
||||
|
|
@ -477,7 +497,11 @@ function OrgChartNode({
|
|||
nodeRefs.current[node.id] = el
|
||||
}}
|
||||
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 */}
|
||||
<div
|
||||
|
|
@ -492,35 +516,42 @@ function OrgChartNode({
|
|||
style={{ cursor: dragging ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
{/* 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' ? (
|
||||
<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" />
|
||||
)}
|
||||
<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}
|
||||
</span>
|
||||
{hasChildren && (
|
||||
<button
|
||||
<Button
|
||||
data-stop-drag="true"
|
||||
onClick={() => {
|
||||
setCollapsed((c) => !c)
|
||||
requestRepaint()
|
||||
}}
|
||||
className="text-white opacity-70 hover:opacity-100 transition-opacity ml-1 flex-shrink-0"
|
||||
>
|
||||
{collapsed ? (
|
||||
<FaChevronRight className="w-2.5 h-2.5" />
|
||||
) : (
|
||||
<FaChevronDown className="w-2.5 h-2.5" />
|
||||
)}
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={
|
||||
collapsed ? (
|
||||
<FaChevronRight className="h-2.5 w-2.5" />
|
||||
) : (
|
||||
<FaChevronDown className="h-2.5 w-2.5" />
|
||||
)
|
||||
}
|
||||
className="ml-1 !h-5 !w-5 flex-shrink-0 !px-0 text-white opacity-70 transition-opacity hover:opacity-100"
|
||||
></Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Users section */}
|
||||
{showUsers && (
|
||||
<div className="px-3 py-2">
|
||||
|
|
@ -531,7 +562,9 @@ function OrgChartNode({
|
|||
<UserAvatar
|
||||
user={user}
|
||||
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}
|
||||
/>
|
||||
<span data-user-name="" className="text-xs text-slate-600 truncate">
|
||||
|
|
@ -557,15 +590,15 @@ function OrgChartNode({
|
|||
{/* Children */}
|
||||
{hasChildren && !collapsed && (
|
||||
<div className="mt-6 sm:mt-8 md:mt-10 flex items-start gap-4 sm:gap-6 md:gap-10">
|
||||
{node.children.map((child, idx) => {
|
||||
const childPosition = positions[child.id] ?? { x: 0, y: 0 }
|
||||
{node.children.map((child, idx) => {
|
||||
const childPosition = positions[child.id] ?? { x: 0, y: 0 }
|
||||
|
||||
return (
|
||||
<div
|
||||
key={child.id}
|
||||
className="flex flex-col items-center relative"
|
||||
style={{ transform: `translate(${childPosition.x}px, ${childPosition.y}px)` }}
|
||||
>
|
||||
return (
|
||||
<div
|
||||
key={child.id}
|
||||
className="flex flex-col items-center relative"
|
||||
style={{ transform: `translate(${childPosition.x}px, ${childPosition.y}px)` }}
|
||||
>
|
||||
{/* Recursive child */}
|
||||
<OrgChartNode
|
||||
node={child}
|
||||
|
|
@ -582,8 +615,8 @@ function OrgChartNode({
|
|||
applySelfTransform={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -848,28 +881,38 @@ const OrgChart = () => {
|
|||
|
||||
<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">
|
||||
<button
|
||||
<Button
|
||||
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'
|
||||
? 'bg-blue-600 text-white shadow-sm'
|
||||
: 'text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white'
|
||||
? '!bg-blue-600 !text-white shadow-sm ring-1 ring-blue-300/70 dark:ring-blue-400/70'
|
||||
: '!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">{translate('::App.Hr.Department') || 'Departman'}</span>
|
||||
</button>
|
||||
<button
|
||||
<span className="hidden sm:inline">
|
||||
{translate('::App.Hr.Department') || 'Departman'}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
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'
|
||||
? 'bg-purple-600 text-white shadow-sm'
|
||||
: 'text-slate-600 dark:text-gray-300 hover:text-slate-800 dark:hover:text-white'
|
||||
? '!bg-purple-600 !text-white shadow-sm ring-1 ring-purple-300/70 dark:ring-purple-400/70'
|
||||
: '!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">{translate('::App.Hr.JobPosition') || 'Pozisyon'}</span>
|
||||
</button>
|
||||
<span className="hidden sm:inline">
|
||||
{translate('::App.Hr.JobPosition') || 'Pozisyon'}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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)}
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-slate-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaSearchPlus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaUndo className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={handleExportJpg}
|
||||
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"
|
||||
>
|
||||
<FaFileImage className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">{exporting ? 'İşleniyor…' : 'Export'}</span>
|
||||
</button>
|
||||
</Button>
|
||||
</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 gap-2">
|
||||
{step === 'columns' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setStep('table')}
|
||||
className="text-gray-400 hover:text-indigo-500 transition-colors"
|
||||
>
|
||||
<FaArrowLeft className="text-xs" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaArrowLeft className="text-xs" />}
|
||||
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">
|
||||
{step === 'table'
|
||||
|
|
@ -82,13 +83,14 @@ function TablePickerModal({
|
|||
: (pickerTable?.tableName ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes />}
|
||||
className="!h-7 !w-7 !px-0 text-gray-400 hover:!bg-transparent hover:text-gray-600 dark:hover:text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Step 1: Table list */}
|
||||
|
|
@ -112,7 +114,7 @@ function TablePickerModal({
|
|||
dbObjects.tables
|
||||
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
|
||||
.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.fullName}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
|
|
@ -135,11 +137,13 @@ function TablePickerModal({
|
|||
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>
|
||||
{t.tableName}
|
||||
</button>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -199,7 +203,7 @@ function TablePickerModal({
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!keyCol || !nameCol}
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -310,14 +317,16 @@ function FormFieldTabLookup({
|
|||
<FormItem
|
||||
label={translate('::ListForms.ListFormFieldEdit.LookupLookupQuery')}
|
||||
extra={
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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'}
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
invalid={errors.lookupDto?.lookupQuery && touched.lookupDto?.lookupQuery}
|
||||
errorMessage={errors.lookupDto?.lookupQuery}
|
||||
|
|
|
|||
|
|
@ -182,52 +182,60 @@ function TreeNode({
|
|||
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSaveEdit(node)
|
||||
}}
|
||||
className="p-1 text-green-500 hover:text-green-600 disabled:opacity-40"
|
||||
>
|
||||
<FaCheck className="text-xs" />
|
||||
</button>
|
||||
<button
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaCheck className="text-xs" />}
|
||||
className="!h-6 !w-6 !px-0 text-green-500 hover:!bg-transparent hover:text-green-600 disabled:opacity-40"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCancelEdit()
|
||||
}}
|
||||
className="p-1 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FaTimes className="text-xs" />
|
||||
</button>
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
title={translate('::ListForms.Wizard.Step1.Rename')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onStartEdit(node.code, node.displayName)
|
||||
}}
|
||||
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-indigo-500'}`}
|
||||
>
|
||||
<FaEdit className="text-xs" />
|
||||
</button>
|
||||
<button
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaEdit 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-indigo-500'}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
title={translate('::ListForms.Wizard.Step1.Delete')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(node)
|
||||
}}
|
||||
className={`p-1 ${isSelected ? 'text-indigo-200 hover:text-white' : 'text-gray-300 hover:text-red-500'}`}
|
||||
>
|
||||
<FaTrash className="text-xs" />
|
||||
</button>
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -343,7 +351,12 @@ function MenuTreeInline({
|
|||
|
||||
const handleDelete = async (node: MenuTreeNode & { id?: string }) => {
|
||||
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)
|
||||
try {
|
||||
await menuService.delete(node.id)
|
||||
|
|
@ -383,7 +396,9 @@ function MenuTreeInline({
|
|||
{isLoading ? (
|
||||
<div className="px-4 py-3 text-sm text-gray-400">Loading…</div>
|
||||
) : 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) => (
|
||||
<TreeNode
|
||||
|
|
@ -476,7 +491,8 @@ const WizardStep1 = ({
|
|||
asterisk={true}
|
||||
extra={
|
||||
<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>
|
||||
}
|
||||
>
|
||||
|
|
@ -501,7 +517,7 @@ const WizardStep1 = ({
|
|||
errorMessage={undefined}
|
||||
extra={
|
||||
<div className="flex items-center gap-2 ml-3">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMenuDialogParentCode(
|
||||
|
|
@ -511,22 +527,31 @@ const WizardStep1 = ({
|
|||
)
|
||||
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'}
|
||||
</button>
|
||||
<span className="whitespace-nowrap">
|
||||
{translate('::ListForms.Wizard.Add') || 'Ekle'}
|
||||
</span>
|
||||
</Button>
|
||||
{values.menuParentCode && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
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'}
|
||||
</button>
|
||||
{translate('::ListForms.Wizard.ClearSelection') || 'Seçimi Kaldır'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -541,7 +566,11 @@ const WizardStep1 = ({
|
|||
isLoading={isLoadingMenu}
|
||||
invalid={false}
|
||||
onReload={onReloadMenu}
|
||||
initialExpanded={values.menuParentCode ? getAncestorCodes(rawMenuItems, values.menuParentCode) : undefined}
|
||||
initialExpanded={
|
||||
values.menuParentCode
|
||||
? getAncestorCodes(rawMenuItems, values.menuParentCode)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
|
@ -565,7 +594,11 @@ const WizardStep1 = ({
|
|||
invalid={!!(errors.menuCode && touched.menuCode)}
|
||||
errorMessage={errors.menuCode}
|
||||
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
|
||||
type="text"
|
||||
|
|
@ -606,7 +639,9 @@ const WizardStep1 = ({
|
|||
type="text"
|
||||
autoComplete="off"
|
||||
name="languageTextMenuEn"
|
||||
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'}
|
||||
placeholder={
|
||||
translate('::ListForms.Wizard.Step1.DisplayNameEnglish') || 'English Menu Text'
|
||||
}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
|
|
@ -622,7 +657,9 @@ const WizardStep1 = ({
|
|||
type="text"
|
||||
autoComplete="off"
|
||||
name="languageTextMenuTr"
|
||||
placeholder={translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'}
|
||||
placeholder={
|
||||
translate('::ListForms.Wizard.Step1.DisplayNameTurkish') || 'Turkish Menu Text'
|
||||
}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
|
|
|
|||
|
|
@ -238,26 +238,28 @@ function SortableItem({
|
|||
>
|
||||
{/* Top row: drag handle + field name + remove */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
{...attributes}
|
||||
{...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}
|
||||
>
|
||||
<FaGripVertical className="text-sm" />
|
||||
</button>
|
||||
/>
|
||||
<span className="flex-1 text-xs font-semibold text-indigo-600 dark:text-indigo-400 truncate">
|
||||
{item.dataField}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<FaTimes className="text-[10px]" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Language Key + Turkish Caption + English Caption */}
|
||||
|
|
@ -362,14 +364,16 @@ function SortableItem({
|
|||
<span className="text-[10px] text-gray-400 font-medium flex-1">
|
||||
{translate('::ListForms.Wizard.Step3.LookupLookupQuery')}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
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 gap-2">
|
||||
{pickerStep === 'columns' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setPickerStep('table')}
|
||||
className="text-gray-400 hover:text-indigo-500 transition-colors"
|
||||
>
|
||||
<FaArrowLeft className="text-xs" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaArrowLeft className="text-xs" />}
|
||||
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">
|
||||
{pickerStep === 'table'
|
||||
|
|
@ -407,13 +412,14 @@ function SortableItem({
|
|||
: (pickerTable?.tableName ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setIsTablePickerOpen(false)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes />}
|
||||
className="!h-7 !w-7 !px-0 text-gray-400 hover:!bg-transparent hover:text-gray-600 dark:hover:text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Step 1: Table list */}
|
||||
|
|
@ -438,7 +444,7 @@ function SortableItem({
|
|||
dbObjects.tables
|
||||
.filter((t) => t.tableName.toLowerCase().includes(tableSearch.toLowerCase()))
|
||||
.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.fullName}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
|
|
@ -461,11 +467,13 @@ function SortableItem({
|
|||
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>
|
||||
{t.tableName}
|
||||
</button>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -525,7 +533,7 @@ function SortableItem({
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!pickerKeyCol || !pickerNameCol}
|
||||
onClick={() => {
|
||||
|
|
@ -538,10 +546,13 @@ function SortableItem({
|
|||
onLookupQueryChange(q)
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -679,42 +690,48 @@ function GroupCard({
|
|||
{translate('::ListForms.Wizard.Step3.Cols') || 'Cols:'}
|
||||
</span>
|
||||
{[1, 2, 3].map((n) => (
|
||||
<button
|
||||
<Button
|
||||
key={n}
|
||||
type="button"
|
||||
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
|
||||
? '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-indigo-500 !text-white'
|
||||
: '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}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{hasAvailable && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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={
|
||||
translate('::ListForms.Wizard.Step3.AddAllToGroupTitle') ||
|
||||
'Tüm mevcut sütunları bu gruba ekle'
|
||||
}
|
||||
>
|
||||
<FaArrowRight className="text-[9px]" />
|
||||
{translate('::ListForms.Wizard.Step3.AddAll') || 'Tümünü Ekle'}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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'}
|
||||
>
|
||||
<FaTrash className="text-xs" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Items drop zone */}
|
||||
|
|
@ -1083,14 +1100,16 @@ const WizardStep3 = ({
|
|||
))}
|
||||
|
||||
{/* Add Group */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -116,10 +116,12 @@ function Section({ title, badge, children, defaultOpen = true }: SectionProps) {
|
|||
const [open, setOpen] = useState(defaultOpen)
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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">
|
||||
{open ? (
|
||||
|
|
@ -134,7 +136,7 @@ function Section({ title, badge, children, defaultOpen = true }: SectionProps) {
|
|||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
{open && <div className="px-4 py-3 bg-white dark:bg-gray-900">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -533,7 +535,10 @@ const WizardStep7 = ({
|
|||
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
|
||||
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
|
||||
<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>
|
||||
{workflowItems.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import type { KeyboardEvent, MouseEvent, RefObject } from 'react'
|
||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
type PendingLink = {
|
||||
sourceId: string
|
||||
|
|
@ -289,16 +290,18 @@ function FlowNode({
|
|||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
ref={setNodeRef}
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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-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,
|
||||
'h-[158px] border-amber-600': item.kind === 'Compare',
|
||||
'!h-[158px] border-amber-600': item.kind === 'Compare',
|
||||
'border-violet-600': item.kind === 'Approval',
|
||||
'border-green-600': item.kind === 'Inform',
|
||||
'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 { WorkflowCriteria } from './WorkflowCriteria'
|
||||
import { WorkflowCanvas } from './WorkflowCanvas'
|
||||
import { Button } from '@/components/ui'
|
||||
import type { FormEvent, RefObject } from 'react'
|
||||
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
|
|
@ -43,12 +44,12 @@ type WorkflowDesignerProps = {
|
|||
}
|
||||
|
||||
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 =
|
||||
'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({
|
||||
busy,
|
||||
|
|
@ -174,11 +175,13 @@ function DesignerToolbar({
|
|||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
className={classNames(
|
||||
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}
|
||||
title="Demo akışı yükle"
|
||||
|
|
@ -186,12 +189,14 @@ function DesignerToolbar({
|
|||
>
|
||||
<FiRefreshCw />
|
||||
Demo
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
className={classNames(
|
||||
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}
|
||||
title="Düğümleri okunabilir şekilde yerleştir"
|
||||
|
|
@ -199,48 +204,54 @@ function DesignerToolbar({
|
|||
>
|
||||
<FiMaximize2 />
|
||||
Fit
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
className={classNames(
|
||||
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"
|
||||
onClick={onZoomIn}
|
||||
>
|
||||
<FiZoomIn />
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
className={classNames(
|
||||
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"
|
||||
onClick={onZoomOut}
|
||||
>
|
||||
<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">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
{kindOptions.map((option) => {
|
||||
const Icon = kindIcon[option.value]
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
key={option.value}
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
className={classNames(
|
||||
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}
|
||||
onClick={() => onAddCriteria(option.value)}
|
||||
>
|
||||
<Icon />
|
||||
{translate(`::ListForms.ListFormEdit.Workflow.Criteria${option.value}`)}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -258,32 +269,36 @@ function DesignerTabs({
|
|||
|
||||
return (
|
||||
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
role="tab"
|
||||
className={classNames(
|
||||
designerTabClass,
|
||||
activeTab === 'flow'
|
||||
? '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-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',
|
||||
)}
|
||||
onClick={() => onChange('flow')}
|
||||
>
|
||||
{translate('::ListForms.ListFormEdit.TabWorkflow')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
role="tab"
|
||||
className={classNames(
|
||||
designerTabClass,
|
||||
activeTab === 'criteria'
|
||||
? '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-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',
|
||||
)}
|
||||
onClick={() => onChange('criteria')}
|
||||
>
|
||||
{translate('::ListForms.ListFormEdit.Workflow.Criteria')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,19 +127,22 @@ function PermissionDialogContent({
|
|||
<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">
|
||||
{isParentPerm ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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 ? (
|
||||
<FaChevronDown className="text-gray-500 text-xs" />
|
||||
) : (
|
||||
<FaChevronRight className="text-gray-500 text-xs" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="w-5" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -121,19 +121,22 @@ function UserPermissionDialogContent({
|
|||
<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">
|
||||
{isParentPerm ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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 ? (
|
||||
<FaChevronDown className="text-gray-500 text-xs" />
|
||||
) : (
|
||||
<FaChevronRight className="text-gray-500 text-xs" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="w-5" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '@/proxy/videoroom/models'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface ChatPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
|
|
@ -51,57 +52,72 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||
{/* Header */}
|
||||
<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>
|
||||
<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} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mesaj Modu */}
|
||||
<div className="p-3 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMessageMode('public')
|
||||
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'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
? '!bg-blue-600 text-white'
|
||||
: '!bg-gray-200 text-gray-700 hover:!bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUsers size={12} />
|
||||
<span>Herkese</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{classSettings.allowPrivateMessages && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
? '!bg-green-600 text-white'
|
||||
: '!bg-gray-200 text-gray-700 hover:!bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUser size={12} />
|
||||
<span>Özel</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMessageMode('announcement')
|
||||
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'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
? '!bg-red-600 text-white'
|
||||
: '!bg-gray-200 text-gray-700 hover:!bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaBullhorn size={12} />
|
||||
<span>Duyuru</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -180,13 +196,15 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||
placeholder="Mesaj yaz..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { VideoroomDocumentDto } from '@/proxy/videoroom/models';
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface DocumentsPanelProps {
|
||||
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="flex items-center justify-between">
|
||||
<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" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -71,12 +78,15 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||
<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-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()}
|
||||
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ç
|
||||
</button>
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
|
@ -114,13 +124,16 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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"
|
||||
>
|
||||
<FaEye size={12} />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<a
|
||||
href={doc.url}
|
||||
|
|
@ -132,13 +145,16 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||
</a>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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"
|
||||
>
|
||||
<FaTrash size={12} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaUserTimes, FaExclamationTriangle } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface KickParticipantModalProps {
|
||||
participant: { id: string; name: string } | null
|
||||
|
|
@ -61,19 +62,25 @@ export const KickParticipantModal: React.FC<KickParticipantModalProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-4">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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} />
|
||||
<span>Çıkar</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||
import React from 'react'
|
||||
import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface LayoutPanelProps {
|
||||
layouts: VideoroomLayoutDto[]
|
||||
|
|
@ -37,22 +38,31 @@ const LayoutPanel: React.FC<LayoutPanelProps> = ({
|
|||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<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" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-3">
|
||||
{layouts.map((layout) => (
|
||||
<button
|
||||
<Button
|
||||
key={layout.id}
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50'
|
||||
? 'border-blue-500 !bg-blue-50'
|
||||
: 'border-gray-200 !bg-white hover:border-blue-300 hover:!bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
|
|
@ -101,7 +111,7 @@ const LayoutPanel: React.FC<LayoutPanelProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
FaVideoSlash,
|
||||
FaUserTimes,
|
||||
} from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface ParticipantsPanelProps {
|
||||
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">
|
||||
Katılımcılar ({participants.length + 1})
|
||||
</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" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* El kaldıranlar göstergesi */}
|
||||
|
|
@ -69,30 +76,36 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
|||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
? '!bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaUsers className="inline mr-1" size={14} />
|
||||
<FaUsers size={14} />
|
||||
Katılımcılar
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
? '!bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaClipboardList className="inline mr-1" size={14} />
|
||||
<FaClipboardList size={14} />
|
||||
Katılım Raporu
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -131,13 +144,16 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
|||
{/* Hand Raise Indicator & Teacher Control */}
|
||||
{participant.isHandRaised &&
|
||||
(user.role === 'teacher' && !participant.isTeacher ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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"
|
||||
>
|
||||
<FaHandPaper className="text-yellow-600" />
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<FaHandPaper className="text-yellow-600 ml-2" title="Parmak kaldırdı" />
|
||||
))}
|
||||
|
|
@ -148,19 +164,22 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
|||
|
||||
{/* Mute / Unmute Button */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() =>
|
||||
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
|
||||
? 'text-green-600 hover:bg-green-50'
|
||||
: 'text-yellow-600 hover:bg-yellow-50'
|
||||
? 'text-green-600 hover:!bg-green-50'
|
||||
: 'text-yellow-600 hover:!bg-yellow-50'
|
||||
}`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
{participant.isAudioMuted ? <FaMicrophone /> : <FaMicrophoneSlash />}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Video muted indicator */}
|
||||
|
|
@ -174,13 +193,16 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
|||
|
||||
{/* Kick Button (Teacher Only) */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import { ScreenSharePanel } from '@/views/admin/videoroom/ScreenSharePanel'
|
|||
import { RoomParticipant } from '@/views/admin/videoroom/RoomParticipant'
|
||||
import toast from '@/components/ui/toast/toast'
|
||||
import Notification from '@/components/ui/Notification'
|
||||
import { Button } from '@/components/ui'
|
||||
import {
|
||||
VideoroomDocumentDto,
|
||||
VideoroomAttendanceDto,
|
||||
|
|
@ -942,84 +943,102 @@ const RoomDetail: React.FC = () => {
|
|||
<div className="flex items-center space-x-1 sm:space-x-2">
|
||||
{/* Audio Control */}
|
||||
{user.role !== 'observer' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
||||
? '!bg-gray-700 hover:!bg-gray-600'
|
||||
: '!bg-red-600 hover:!bg-red-700'
|
||||
}`}
|
||||
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
||||
>
|
||||
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Video Control */}
|
||||
{user.role !== 'observer' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
||||
? '!bg-gray-700 hover:!bg-gray-600'
|
||||
: '!bg-red-600 hover:!bg-red-700'
|
||||
}`}
|
||||
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
||||
>
|
||||
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Screen Share */}
|
||||
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
? '!bg-blue-600 hover:!bg-blue-700'
|
||||
: '!bg-gray-700 hover:!bg-gray-600'
|
||||
}`}
|
||||
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
||||
>
|
||||
<FaDesktop size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Hand Raise (Students) */}
|
||||
{user.role === 'student' && classSettings.allowHandRaise && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={handleRaiseHand}
|
||||
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
|
||||
? 'bg-yellow-600 text-white cursor-not-allowed'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
|
||||
? '!bg-yellow-600 cursor-not-allowed'
|
||||
: '!bg-gray-700 hover:!bg-yellow-600'
|
||||
}`}
|
||||
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
||||
>
|
||||
<FaHandPaper size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Leave Call */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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"
|
||||
>
|
||||
<FaPhone size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Panel Controls */}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white"
|
||||
<Button
|
||||
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)}
|
||||
aria-label="Menüyü Aç"
|
||||
>
|
||||
<FaBars size={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Hamburger Menu Modal */}
|
||||
|
|
@ -1040,20 +1059,26 @@ const RoomDetail: React.FC = () => {
|
|||
>
|
||||
<div className="flex items-center justify-between px-4 py-4 border-b">
|
||||
<span className="font-semibold text-gray-800 text-lg">Menü</span>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<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={() => {
|
||||
setMobileMenuOpen(false)
|
||||
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>
|
||||
{chatMessages.length > 0 && (
|
||||
|
|
@ -1061,13 +1086,16 @@ const RoomDetail: React.FC = () => {
|
|||
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false)
|
||||
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 />
|
||||
<span>Katılımcılar</span>
|
||||
|
|
@ -1077,47 +1105,59 @@ const RoomDetail: React.FC = () => {
|
|||
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false)
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false)
|
||||
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>
|
||||
</button>
|
||||
</Button>
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false)
|
||||
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 />}{' '}
|
||||
<span>{isAllMuted ? 'Hepsinin Sesini Aç' : 'Hepsini Sustur'}</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false)
|
||||
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 />}{' '}
|
||||
<span>{isFullscreen ? 'Tam Ekrandan Çık' : 'Tam Ekran'}</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
|
|
@ -1141,95 +1181,116 @@ const RoomDetail: React.FC = () => {
|
|||
<div className="flex items-center space-x-2">
|
||||
{/* Audio Control */}
|
||||
{user.role !== 'observer' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
||||
? '!bg-gray-700 hover:!bg-gray-600'
|
||||
: '!bg-red-600 hover:!bg-red-700'
|
||||
}`}
|
||||
title={isAudioEnabled ? 'Mikrofonu Kapat' : 'Mikrofonu Aç'}
|
||||
>
|
||||
{isAudioEnabled ? <FaMicrophone size={16} /> : <FaMicrophoneSlash size={16} />}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Video Control */}
|
||||
{user.role !== 'observer' && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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
|
||||
? 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
: 'bg-red-600 hover:bg-red-700 text-white'
|
||||
? '!bg-gray-700 hover:!bg-gray-600'
|
||||
: '!bg-red-600 hover:!bg-red-700'
|
||||
}`}
|
||||
title={isVideoEnabled ? 'Kamerayı Kapat' : 'Kamerayı Aç'}
|
||||
>
|
||||
{isVideoEnabled ? <FaVideo size={16} /> : <FaVideoSlash size={16} />}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Screen Share */}
|
||||
{(user.role === 'teacher' || classSettings.allowStudentScreenShare) && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
? '!bg-blue-600 hover:!bg-blue-700'
|
||||
: '!bg-gray-700 hover:!bg-gray-600'
|
||||
}`}
|
||||
title={isScreenSharing ? 'Paylaşımı Durdur' : 'Ekranı Paylaş'}
|
||||
>
|
||||
<FaDesktop size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Hand Raise (Students) */}
|
||||
{user.role === 'student' && classSettings.allowHandRaise && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={handleRaiseHand}
|
||||
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
|
||||
? 'bg-yellow-600 text-white cursor-not-allowed'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
|
||||
? '!bg-yellow-600 cursor-not-allowed'
|
||||
: '!bg-gray-700 hover:!bg-yellow-600'
|
||||
}`}
|
||||
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
|
||||
>
|
||||
<FaHandPaper size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Leave Call */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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"
|
||||
>
|
||||
<FaPhone size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Panel Controls */}
|
||||
<div className="flex items-center space-x-2 absolute right-0">
|
||||
{/* Fullscreen Toggle */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'}
|
||||
>
|
||||
{isFullscreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Chat */}
|
||||
{((user.role !== 'observer' && classSettings.allowStudentChat) ||
|
||||
user.role === 'teacher') && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
? '!bg-blue-600 text-white'
|
||||
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||
}`}
|
||||
title="Sohbet"
|
||||
>
|
||||
|
|
@ -1239,16 +1300,19 @@ const RoomDetail: React.FC = () => {
|
|||
{chatMessages.length > 9 ? '9+' : chatMessages.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Participants */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
? '!bg-blue-600 text-white'
|
||||
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||
}`}
|
||||
title="Katılımcılar"
|
||||
>
|
||||
|
|
@ -1259,47 +1323,56 @@ const RoomDetail: React.FC = () => {
|
|||
{raisedHandsCount > 9 ? '9+' : raisedHandsCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Teacher Only Options */}
|
||||
{user.role === 'teacher' && (
|
||||
<>
|
||||
{/* Documents Button */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
? '!bg-blue-600 text-white'
|
||||
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||
}`}
|
||||
title="Dokümanlar"
|
||||
>
|
||||
<FaFile size={14} />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Mute All Button */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'}
|
||||
>
|
||||
{isAllMuted ? <FaVolumeUp size={14} /> : <FaVolumeMute size={14} />}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Layout Button */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 hover:bg-gray-600 text-white'
|
||||
? '!bg-blue-600 text-white'
|
||||
: '!bg-gray-700 hover:!bg-gray-600 text-white'
|
||||
}`}
|
||||
title="Layout"
|
||||
>
|
||||
<FaLayerGroup size={14} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ const RoomList: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* 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 ? (
|
||||
<div className="text-center py-12">
|
||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
||||
|
|
@ -481,7 +481,7 @@ const RoomList: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* 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">
|
||||
<FaCalendarAlt size={11} className="text-blue-400" />
|
||||
{showDbDateAsIs(classSession.scheduledStartTime)}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
|||
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
|
||||
import { VideoPlayer } from './VideoPlayer'
|
||||
import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface RoomParticipantProps {
|
||||
participants: VideoroomParticipantDto[]
|
||||
|
|
@ -240,28 +241,34 @@ export const RoomParticipant: React.FC<RoomParticipantProps> = ({
|
|||
{/* Teacher controls for students */}
|
||||
{isTeacher && participant.id !== currentUserId && (
|
||||
<div className="absolute top-2 left-2 flex space-x-1 z-10">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
|
||||
}}
|
||||
className={`p-1 rounded-full text-white text-xs ${
|
||||
participant.isAudioMuted ? 'bg-red-600' : 'bg-gray-600 hover:bg-gray-700'
|
||||
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'
|
||||
} transition-colors`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
<FaMicrophoneSlash size={12} />
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { FaDesktop, FaStop, FaPlay } from 'react-icons/fa';
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
interface ScreenSharePanelProps {
|
||||
isSharing: boolean;
|
||||
|
|
@ -25,21 +26,27 @@ export const ScreenSharePanel: React.FC<ScreenSharePanelProps> = ({
|
|||
</div>
|
||||
|
||||
{isSharing ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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} />
|
||||
<span>Paylaşımı Durdur</span>
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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} />
|
||||
<span>Ekranı Paylaş</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -73,4 +80,4 @@ export const ScreenSharePanel: React.FC<ScreenSharePanelProps> = ({
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -593,14 +593,16 @@ const Assistant = () => {
|
|||
<div className="mt-3 max-h-[calc(100vh-140px)] overflow-y-auto overscroll-contain">
|
||||
<div className="space-y-1">
|
||||
{conversations.map((conversation) => (
|
||||
<button
|
||||
<Button
|
||||
key={conversation.id}
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
? '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-blue-50 text-blue-700 dark:!bg-blue-900/40 dark:text-blue-200'
|
||||
: '!bg-transparent text-gray-700 hover:!bg-gray-100 dark:text-gray-200 dark:hover:!bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FaComment className="shrink-0" />
|
||||
|
|
@ -617,7 +619,7 @@ const Assistant = () => {
|
|||
>
|
||||
<FaTrash className="text-xs" />
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -631,14 +633,16 @@ const Assistant = () => {
|
|||
<ScrollBar autoHide className="mt-3 flex-1 min-h-0">
|
||||
<div className="space-y-1 pr-2">
|
||||
{conversations.map((conversation) => (
|
||||
<button
|
||||
<Button
|
||||
key={conversation.id}
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
? '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-blue-50 text-blue-700 dark:!bg-blue-900/40 dark:text-blue-200'
|
||||
: '!bg-transparent text-gray-700 hover:!bg-gray-100 dark:text-gray-200 dark:hover:!bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FaComment className="shrink-0" />
|
||||
|
|
@ -663,7 +667,7 @@ const Assistant = () => {
|
|||
>
|
||||
<FaTrash className="text-xs" />
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollBar>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import PropertyPanel from '../../components/codeLayout/PropertyPanel'
|
|||
import ComponentSelector from '../../components/codeLayout/ComponentSelector'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useComponents } from '../../contexts/ComponentContext'
|
||||
import { toast } from '../../components/ui'
|
||||
import { Button, toast } from '../../components/ui'
|
||||
import Notification from '../../components/ui/Notification/Notification'
|
||||
import { PanelState } from '../../components/codeLayout/data/componentDefinitions'
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
||||
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
|
||||
interface ValidationError {
|
||||
|
|
@ -202,227 +202,225 @@ export default ${pascalCaseName}Component;`
|
|||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
|
||||
<>
|
||||
{/* 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="px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3 pl-3">
|
||||
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
|
||||
<FaCode className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
|
||||
{isEditing
|
||||
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
|
||||
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
|
||||
{isEditing ? 'Modify your React component' : 'Create a new React component'}
|
||||
</p>
|
||||
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
|
||||
return (
|
||||
<>
|
||||
{/* 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="px-1 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3 pl-3">
|
||||
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
|
||||
<FaCode className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
|
||||
{isEditing
|
||||
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
|
||||
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
|
||||
{isEditing ? 'Modify your React component' : 'Create a new React component'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button in Header */}
|
||||
<div className="flex items-center gap-3 pr-3">
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.components}
|
||||
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 text-black dark:text-white px-4 py-2 rounded-lg hover:text-slate-700 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<FaArrowLeft className="w-3.5 h-3.5" />
|
||||
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
||||
</Link>
|
||||
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||
{/* Save Button in Header */}
|
||||
<div className="flex items-center gap-3 pr-3">
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.components}
|
||||
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 text-black dark:text-white px-4 py-2 rounded-lg hover:text-slate-700 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<FaArrowLeft className="w-3.5 h-3.5" />
|
||||
{translate('::App.DeveloperKit.ComponentEditor.Back')}
|
||||
</Link>
|
||||
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitForm}
|
||||
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"
|
||||
>
|
||||
<FaRegSave className="w-4 h-4" />
|
||||
{isSubmitting
|
||||
? translate('::App.DeveloperKit.ComponentEditor.Saving')
|
||||
: translate('::App.DeveloperKit.ComponentEditor.Save')}
|
||||
</button>
|
||||
<Button
|
||||
type="button"
|
||||
variant='solid'
|
||||
size="sm"
|
||||
color='green'
|
||||
onClick={submitForm}
|
||||
disabled={isSubmitting || !values.name.trim() || !isValid}
|
||||
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" />
|
||||
{isSubmitting
|
||||
? translate('::App.DeveloperKit.ComponentEditor.Saving')
|
||||
: translate('::App.DeveloperKit.ComponentEditor.Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 py-3">
|
||||
<div className="space-y-3 col-span-1">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/20 p-1.5 rounded-lg">
|
||||
<FaCog className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 py-3">
|
||||
<div className="space-y-3 col-span-1">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/20 p-1.5 rounded-lg">
|
||||
<FaCog className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
||||
Component Settings
|
||||
</h2>
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
||||
Component Settings
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<FormContainer size="sm">
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.ComponentEditor.ComponentName')}
|
||||
invalid={!!(errors.name && touched.name)}
|
||||
errorMessage={errors.name as string}
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
type="text"
|
||||
component={Input}
|
||||
autoFocus
|
||||
placeholder="e.g., Button, Card, Modal"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newName = e.target.value
|
||||
setFieldValue('name', newName)
|
||||
<FormContainer size="sm">
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.ComponentEditor.ComponentName')}
|
||||
invalid={!!(errors.name && touched.name)}
|
||||
errorMessage={errors.name as string}
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
type="text"
|
||||
component={Input}
|
||||
autoFocus
|
||||
placeholder="e.g., Button, Card, Modal"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newName = e.target.value
|
||||
setFieldValue('name', newName)
|
||||
|
||||
// Auto-generate code template if this is a new component (not editing)
|
||||
// and only if the code field is empty or contains the default template
|
||||
if (!isEditing && newName.trim()) {
|
||||
const currentCode = values.code.trim()
|
||||
const isCodeEmpty = !currentCode
|
||||
const isCodeDefaultTemplate =
|
||||
currentCode.includes('Component = ({') &&
|
||||
currentCode.includes('export default') &&
|
||||
currentCode.includes('<span>{title}</span>')
|
||||
// Auto-generate code template if this is a new component (not editing)
|
||||
// and only if the code field is empty or contains the default template
|
||||
if (!isEditing && newName.trim()) {
|
||||
const currentCode = values.code.trim()
|
||||
const isCodeEmpty = !currentCode
|
||||
const isCodeDefaultTemplate = currentCode.includes('Component = ({') &&
|
||||
currentCode.includes('export default') &&
|
||||
currentCode.includes('<span>{title}</span>')
|
||||
|
||||
if (isCodeEmpty || isCodeDefaultTemplate) {
|
||||
const template = generateComponentTemplate(newName)
|
||||
setFieldValue('code', template)
|
||||
parseAndUpdateComponents(template)
|
||||
if (isCodeEmpty || isCodeDefaultTemplate) {
|
||||
const template = generateComponentTemplate(newName)
|
||||
setFieldValue('code', template)
|
||||
parseAndUpdateComponents(template)
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
} } />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
|
||||
invalid={!!(errors.description && touched.description)}
|
||||
errorMessage={errors.description as string}
|
||||
>
|
||||
<Field
|
||||
name="description"
|
||||
type="text"
|
||||
component={Input}
|
||||
placeholder="Brief description of the component"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
|
||||
invalid={!!(errors.description && touched.description)}
|
||||
errorMessage={errors.description as string}
|
||||
>
|
||||
<Field
|
||||
name="description"
|
||||
type="text"
|
||||
component={Input}
|
||||
placeholder="Brief description of the component" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::App.Platform.Code')}
|
||||
invalid={!!(errors.code && touched.code)}
|
||||
errorMessage={errors.code as string}
|
||||
>
|
||||
<Field
|
||||
name="code"
|
||||
type="text"
|
||||
component={Input}
|
||||
placeholder="React component code goes here"
|
||||
textArea={true}
|
||||
rows={10}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setFieldValue('code', e.target.value)
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::App.Platform.Code')}
|
||||
invalid={!!(errors.code && touched.code)}
|
||||
errorMessage={errors.code as string}
|
||||
>
|
||||
<Field
|
||||
name="code"
|
||||
type="text"
|
||||
component={Input}
|
||||
placeholder="React component code goes here"
|
||||
textArea={true}
|
||||
rows={10}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setFieldValue('code', e.target.value)
|
||||
} } />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
|
||||
invalid={!!(errors.dependencies && touched.dependencies)}
|
||||
errorMessage={errors.dependencies as string}
|
||||
>
|
||||
<Field name="dependencies">
|
||||
{({ field }: FieldProps) => (
|
||||
<Input
|
||||
type="text"
|
||||
value={(values.dependencies || []).join(', ')}
|
||||
onChange={(e) =>
|
||||
setFieldValue(
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
|
||||
invalid={!!(errors.dependencies && touched.dependencies)}
|
||||
errorMessage={errors.dependencies as string}
|
||||
>
|
||||
<Field name="dependencies">
|
||||
{({ field }: FieldProps) => (
|
||||
<Input
|
||||
type="text"
|
||||
value={(values.dependencies || []).join(', ')}
|
||||
onChange={(e) => setFieldValue(
|
||||
'dependencies',
|
||||
e.target.value
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
}
|
||||
placeholder="MyComponent, AnotherComponent, etc."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label={translate('::App.Status.Active')}>
|
||||
<Field name="isActive" component={Checkbox} />
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Preview and Validation */}
|
||||
<div className="space-y-4 col-span-2">
|
||||
{/* Validation Errors */}
|
||||
{validationErrors.length > 0 && (
|
||||
<div className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-700 rounded-lg p-3 shadow-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-red-100 dark:bg-red-900 rounded-full p-1.5">
|
||||
<FaExclamationCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-red-800 dark:text-red-300 mb-1">
|
||||
Validation Issues
|
||||
</h3>
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mb-3">
|
||||
{validationErrors.length} issue
|
||||
{validationErrors.length !== 1 ? 's' : ''} found in your code
|
||||
</p>
|
||||
<div className="space-y-1.5 max-h-32 overflow-y-auto">
|
||||
{validationErrors.slice(0, 5).map((error, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white dark:bg-gray-900 p-2 rounded border border-red-100 dark:border-red-700"
|
||||
>
|
||||
<div className="text-xs text-red-800 dark:text-red-300">
|
||||
<span className="font-medium bg-red-100 dark:bg-red-900 px-1.5 py-0.5 rounded text-xs">
|
||||
Line {error.startLineNumber}
|
||||
</span>
|
||||
<span className="ml-2">{error.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{validationErrors.length > 5 && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 italic text-center py-1">
|
||||
... and {validationErrors.length - 5} more issue
|
||||
{validationErrors.length - 5 !== 1 ? 's' : ''}
|
||||
</div>
|
||||
.filter(Boolean)
|
||||
)}
|
||||
placeholder="MyComponent, AnotherComponent, etc." />
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label={translate('::App.Status.Active')}>
|
||||
<Field name="isActive" component={Checkbox} />
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Preview and Validation */}
|
||||
<div className="space-y-4 col-span-2">
|
||||
{/* Validation Errors */}
|
||||
{validationErrors.length > 0 && (
|
||||
<div className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-700 rounded-lg p-3 shadow-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-red-100 dark:bg-red-900 rounded-full p-1.5">
|
||||
<FaExclamationCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-red-800 dark:text-red-300 mb-1">
|
||||
Validation Issues
|
||||
</h3>
|
||||
<p className="text-xs text-red-700 dark:text-red-400 mb-3">
|
||||
{validationErrors.length} issue
|
||||
{validationErrors.length !== 1 ? 's' : ''} found in your code
|
||||
</p>
|
||||
<div className="space-y-1.5 max-h-32 overflow-y-auto">
|
||||
{validationErrors.slice(0, 5).map((error, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white dark:bg-gray-900 p-2 rounded border border-red-100 dark:border-red-700"
|
||||
>
|
||||
<div className="text-xs text-red-800 dark:text-red-300">
|
||||
<span className="font-medium bg-red-100 dark:bg-red-900 px-1.5 py-0.5 rounded text-xs">
|
||||
Line {error.startLineNumber}
|
||||
</span>
|
||||
<span className="ml-2">{error.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{validationErrors.length > 5 && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 italic text-center py-1">
|
||||
... and {validationErrors.length - 5} more issue
|
||||
{validationErrors.length - 5 !== 1 ? 's' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Component Preview */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="bg-purple-100 dark:bg-purple-900/20 p-1.5 rounded-lg">
|
||||
<FaEye className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
{/* Component Preview */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="bg-purple-100 dark:bg-purple-900/20 p-1.5 rounded-lg">
|
||||
<FaEye className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
||||
Preview
|
||||
</h2>
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
||||
Preview
|
||||
</h2>
|
||||
<ComponentPreview componentName={values.name} />
|
||||
</div>
|
||||
<ComponentPreview componentName={values.name} />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import { Loading } from '../../components/shared'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
const ComponentManager: React.FC = () => {
|
||||
const { components, loading, updateComponent, deleteComponent } = useComponents()
|
||||
|
|
@ -208,12 +209,15 @@ const ComponentManager: React.FC = () => {
|
|||
{/* Actions */}
|
||||
<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">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
? '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-slate-100 dark:bg-gray-800 text-slate-600 dark:text-gray-400 hover:bg-slate-200 dark:hover:bg-gray-700'
|
||||
? '!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 text-slate-600 hover:!bg-slate-200 dark:!bg-gray-800 dark:text-gray-400 dark:hover:!bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{component.isActive ? (
|
||||
|
|
@ -227,7 +231,7 @@ const ComponentManager: React.FC = () => {
|
|||
{translate('::App.DeveloperKit.Component.Inactive')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
|
|
@ -251,13 +255,16 @@ const ComponentManager: React.FC = () => {
|
|||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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')}
|
||||
>
|
||||
<FaTrashAlt className="w-4 h-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
|
|||
import type { CrudEndpoint } from '@/proxy/developerKit/models'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface TestResult {
|
||||
success: boolean
|
||||
|
|
@ -348,7 +349,7 @@ const CrudEndpointManager: React.FC = () => {
|
|||
color="teal"
|
||||
icon="FaBolt"
|
||||
valueClassName="text-3xl"
|
||||
subTitle={translate('::App.DeveloperKit.CrudEndpoints.ActiveEndpointDescription')}
|
||||
subTitle={translate('::App.DeveloperKit.CrudEndpoints.ActiveEndpointDescription')}
|
||||
/>
|
||||
<Widget
|
||||
title={translate('::App.DeveloperKit.CrudEndpoints.DataSource')}
|
||||
|
|
@ -379,7 +380,9 @@ const CrudEndpointManager: React.FC = () => {
|
|||
{ds.code}
|
||||
</option>
|
||||
))}
|
||||
{dataSources.length === 0 && <option value="">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</option>}
|
||||
{dataSources.length === 0 && (
|
||||
<option value="">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -404,21 +407,24 @@ const CrudEndpointManager: React.FC = () => {
|
|||
}
|
||||
const active = crudFilter === f
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
key={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
|
||||
? f === 'with'
|
||||
? 'bg-green-500 text-white'
|
||||
? '!bg-green-500 text-white'
|
||||
: f === 'without'
|
||||
? 'bg-slate-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-slate-500 text-white'
|
||||
: '!bg-blue-500 text-white'
|
||||
: '!bg-white text-slate-500 hover:!bg-slate-50 dark:!bg-gray-900 dark:text-gray-400 dark:hover:!bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{labels[f]}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -429,11 +435,15 @@ const CrudEndpointManager: React.FC = () => {
|
|||
{loadingTables ? (
|
||||
<div className="flex items-center justify-center p-8 text-slate-400">
|
||||
<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>
|
||||
) : filteredTables.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
Object.entries(tablesBySchema).map(([schema, tables]) => (
|
||||
|
|
@ -447,36 +457,47 @@ const CrudEndpointManager: React.FC = () => {
|
|||
const isSelected = selectedTable?.fullName === table.fullName
|
||||
const hasEndpoints = total > 0
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
key={table.fullName}
|
||||
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 ${
|
||||
isSelected ? 'bg-blue-50 dark:bg-blue-900/20 border-l-2 border-l-blue-500 dark:border-l-blue-400' : ''
|
||||
variant="plain"
|
||||
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">
|
||||
<FaTable
|
||||
className={`flex-shrink-0 text-xs ${
|
||||
hasEndpoints ? 'text-green-500' : 'text-slate-300'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-slate-700 dark:text-gray-100 truncate">{table.tableName}</span>
|
||||
</div>
|
||||
{hasEndpoints && (
|
||||
<span
|
||||
className={`flex-shrink-0 text-xs px-1.5 py-0.5 rounded-full font-medium ${
|
||||
active > 0
|
||||
? 'bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-400'
|
||||
: 'bg-slate-100 dark:bg-gray-800 text-slate-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{active}/{total}
|
||||
<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
|
||||
className={`flex-shrink-0 text-xs ${
|
||||
hasEndpoints ? 'text-green-500' : 'text-slate-300'
|
||||
}`}
|
||||
/>
|
||||
<span className="truncate text-sm text-slate-700 dark:text-gray-100">
|
||||
{table.tableName}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{isSelected && (
|
||||
<FaChevronRight className="flex-shrink-0 text-blue-400 text-xs ml-1" />
|
||||
)}
|
||||
</button>
|
||||
<span className="flex flex-shrink-0 items-center gap-1">
|
||||
{hasEndpoints && (
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5 text-xs font-medium ${
|
||||
active > 0
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400'
|
||||
: 'bg-slate-100 text-slate-500 dark:bg-gray-800 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{active}/{total}
|
||||
</span>
|
||||
)}
|
||||
{isSelected && (
|
||||
<FaChevronRight className="flex-shrink-0 text-xs text-blue-400" />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -490,7 +511,9 @@ const CrudEndpointManager: React.FC = () => {
|
|||
{!selectedTable ? (
|
||||
<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" />
|
||||
<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">
|
||||
{translate('::App.DeveloperKit.CrudEndpoints.SelectTableDescription')}
|
||||
</p>
|
||||
|
|
@ -499,43 +522,55 @@ const CrudEndpointManager: React.FC = () => {
|
|||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* Table header */}
|
||||
<div className="flex flex-col gap-3 p-4 border-b border-slate-200 dark:border-gray-700 bg-slate-50 dark:bg-gray-800 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 p-2 rounded-lg">
|
||||
<FaTable />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-slate-900 dark:text-gray-100">{selectedTable.schemaName}.{selectedTable.tableName}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 p-2 rounded-lg">
|
||||
<FaTable />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-slate-900 dark:text-gray-100">
|
||||
{selectedTable.schemaName}.{selectedTable.tableName}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{selectedTableEndpoints.length > 0 && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteAll(selectedTable)}
|
||||
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"
|
||||
icon={
|
||||
deletingAll === selectedTable.fullName ? (
|
||||
<FaSyncAlt className="animate-spin" />
|
||||
) : (
|
||||
<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"
|
||||
>
|
||||
{deletingAll === selectedTable.fullName ? (
|
||||
<FaSyncAlt className="animate-spin" />
|
||||
) : (
|
||||
<FaTrash />
|
||||
)}
|
||||
{translate('::App.DeveloperKit.CrudEndpoints.DeleteAll')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleGenerate(selectedTable)}
|
||||
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"
|
||||
icon={
|
||||
generatingFor === selectedTable.fullName ? (
|
||||
<FaSyncAlt className="animate-spin" />
|
||||
) : (
|
||||
<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"
|
||||
>
|
||||
{generatingFor === selectedTable.fullName ? (
|
||||
<FaSyncAlt className="animate-spin" />
|
||||
) : (
|
||||
<FaBolt />
|
||||
)}
|
||||
{selectedTableEndpoints.length > 0
|
||||
? translate('::App.DeveloperKit.CrudEndpoints.Regenerate')
|
||||
: translate('::App.DeveloperKit.CrudEndpoints.CreateCrudEndpoint')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -544,7 +579,9 @@ const CrudEndpointManager: React.FC = () => {
|
|||
{selectedTableEndpoints.length === 0 ? (
|
||||
<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" />
|
||||
<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">
|
||||
{translate('::App.DeveloperKit.CrudEndpoints.ClickToCreate')}
|
||||
</p>
|
||||
|
|
@ -562,11 +599,18 @@ const CrudEndpointManager: React.FC = () => {
|
|||
{/* 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">
|
||||
{/* Toggle */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleToggle(ep.id)}
|
||||
disabled={togglingId === ep.id}
|
||||
title={ep.isActive ? translate('::App.DeveloperKit.CrudEndpoints.Disable') : translate('::App.DeveloperKit.CrudEndpoints.Enable')}
|
||||
className={`flex-shrink-0 text-xl transition-colors ${
|
||||
title={
|
||||
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
|
||||
? 'text-green-500 hover:text-green-700'
|
||||
: 'text-slate-300 hover:text-slate-500'
|
||||
|
|
@ -579,7 +623,7 @@ const CrudEndpointManager: React.FC = () => {
|
|||
) : (
|
||||
<FaToggleOff />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Method badge */}
|
||||
<span
|
||||
|
|
@ -602,9 +646,12 @@ const CrudEndpointManager: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Expand */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
{isExpanded ? (
|
||||
|
|
@ -612,7 +659,7 @@ const CrudEndpointManager: React.FC = () => {
|
|||
) : (
|
||||
<FaChevronRight className="text-xs" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Expanded detail + test */}
|
||||
|
|
@ -680,20 +727,28 @@ const CrudEndpointManager: React.FC = () => {
|
|||
|
||||
{/* Test button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => testEndpoint(ep)}
|
||||
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"
|
||||
icon={
|
||||
loadingEndpoints.has(ep.id) ? (
|
||||
<FaSyncAlt className="animate-spin" />
|
||||
) : (
|
||||
<FaPaperPlane />
|
||||
)
|
||||
}
|
||||
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) ? (
|
||||
<FaSyncAlt className="animate-spin" />
|
||||
) : (
|
||||
<FaPaperPlane />
|
||||
)}
|
||||
{loadingEndpoints.has(ep.id) ? translate('::App.DeveloperKit.CrudEndpoints.Sending') : translate('::App.DeveloperKit.CrudEndpoints.Test')}
|
||||
</button>
|
||||
{loadingEndpoints.has(ep.id)
|
||||
? translate('::App.DeveloperKit.CrudEndpoints.Sending')
|
||||
: translate('::App.DeveloperKit.CrudEndpoints.Test')}
|
||||
</Button>
|
||||
{testResult && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setTestResults((prev) => {
|
||||
const n = { ...prev }
|
||||
|
|
@ -701,10 +756,12 @@ const CrudEndpointManager: React.FC = () => {
|
|||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -738,7 +795,8 @@ const CrudEndpointManager: React.FC = () => {
|
|||
2,
|
||||
)}
|
||||
</pre>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
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"
|
||||
>
|
||||
<FaCopy className="text-xs" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="none"
|
||||
icon={<FaCopy className="text-xs" />}
|
||||
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>
|
||||
)}
|
||||
|
|
@ -760,13 +819,19 @@ const CrudEndpointManager: React.FC = () => {
|
|||
{ep.csharpCode && (
|
||||
<div>
|
||||
<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>
|
||||
<button
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-gray-300">
|
||||
{translate('::App.DeveloperKit.CrudEndpoints.CsharpCode')}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
{translate('::App.DeveloperKit.CrudEndpoints.Copy')}
|
||||
</Button>
|
||||
</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">
|
||||
{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">
|
||||
<span className="flex items-center gap-1">
|
||||
<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>{selectedTableEndpoints.filter((e) => !e.isActive).length} {translate('::App.DeveloperKit.CrudEndpoints.InactiveCount')}</span>
|
||||
<span className="ml-auto">
|
||||
{translate('::App.DeveloperKit.CrudEndpoints.EndpointSummary')}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { Helmet } from 'react-helmet'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
const defaultTemplate = `using System;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -227,43 +228,55 @@ const DynamicServiceEditor: React.FC = () => {
|
|||
{translate('::App.DeveloperKit.DynamicServices.Editor.BackToServices')}
|
||||
</Link>
|
||||
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleTestCompile}
|
||||
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 ? (
|
||||
<FaSpinner className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<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 ? (
|
||||
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<FaPlay className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{isCompiling
|
||||
? translate('::App.DeveloperKit.DynamicServices.Editor.Compiling')
|
||||
: translate('::App.DeveloperKit.DynamicServices.Editor.TestCompile')}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handlePublish}
|
||||
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"
|
||||
icon={
|
||||
isPublishing ? (
|
||||
<FaSpinner className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<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 ? (
|
||||
<FaSpinner className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<FaSave className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{isPublishing
|
||||
? translate('::App.DeveloperKit.DynamicServices.Editor.Publishing')
|
||||
: translate('::App.DeveloperKit.DynamicServices.Editor.Publish')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { dynamicServiceService, type DynamicServiceDto } from '@/services/dynami
|
|||
import { Helmet } from 'react-helmet'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
const DynamicServiceManager: React.FC = () => {
|
||||
const { translate } = useLocalization()
|
||||
|
|
@ -160,13 +161,16 @@ const DynamicServiceManager: React.FC = () => {
|
|||
</select>
|
||||
</div>
|
||||
<div className="w-full sm:w-auto">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full sm:w-auto">
|
||||
<Link
|
||||
|
|
@ -244,13 +248,15 @@ const DynamicServiceManager: React.FC = () => {
|
|||
>
|
||||
<FaRegEdit className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<FaTrashAlt className="w-4 h-4" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-man
|
|||
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
type FolderKey = 'tables' | 'views' | 'procedures' | 'functions'
|
||||
|
||||
|
|
@ -49,10 +50,10 @@ export interface SqlExplorerSelectedObject {
|
|||
}
|
||||
|
||||
const FOLDER_META: Record<FolderKey, { label: string; color: string }> = {
|
||||
tables: { label: 'Tables', color: 'text-teal-500' },
|
||||
views: { label: 'Views', color: 'text-purple-500' },
|
||||
procedures: { label: 'Stored Procedures', color: 'text-green-600' },
|
||||
functions: { label: 'Functions', color: 'text-orange-500' },
|
||||
tables: { label: 'Tables', color: 'text-teal-500' },
|
||||
views: { label: 'Views', color: 'text-purple-500' },
|
||||
procedures: { label: 'Stored Procedures', color: 'text-green-600' },
|
||||
functions: { label: 'Functions', color: 'text-orange-500' },
|
||||
}
|
||||
|
||||
const SqlObjectExplorer = ({
|
||||
|
|
@ -76,7 +77,10 @@ const SqlObjectExplorer = ({
|
|||
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
|
||||
const [dropping, setDropping] = useState(false)
|
||||
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 })
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -108,47 +112,49 @@ const SqlObjectExplorer = ({
|
|||
data: obj,
|
||||
})
|
||||
|
||||
const tree: TreeNode[] = [{
|
||||
id: 'root',
|
||||
label: dataSource,
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
id: 'tables',
|
||||
label: `Tables (${data.tables.length})`,
|
||||
type: 'folder',
|
||||
folder: 'tables',
|
||||
children: data.tables.map((t) => ({
|
||||
id: `tables-${t.schemaName}-${t.tableName}`,
|
||||
label: t.fullName ?? `[${t.schemaName}].[${t.tableName}]`,
|
||||
type: 'object' as const,
|
||||
folder: 'tables' as FolderKey,
|
||||
data: t,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: 'views',
|
||||
label: `Views (${data.views.length})`,
|
||||
type: 'folder',
|
||||
folder: 'views',
|
||||
children: data.views.map((v) => makeObjectNode('views', v)),
|
||||
},
|
||||
{
|
||||
id: 'procedures',
|
||||
label: `Stored Procedures (${data.storedProcedures.length})`,
|
||||
type: 'folder',
|
||||
folder: 'procedures',
|
||||
children: data.storedProcedures.map((p) => makeObjectNode('procedures', p)),
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
label: `Functions (${data.functions.length})`,
|
||||
type: 'folder',
|
||||
folder: 'functions',
|
||||
children: data.functions.map((f) => makeObjectNode('functions', f)),
|
||||
},
|
||||
],
|
||||
}]
|
||||
const tree: TreeNode[] = [
|
||||
{
|
||||
id: 'root',
|
||||
label: dataSource,
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
id: 'tables',
|
||||
label: `Tables (${data.tables.length})`,
|
||||
type: 'folder',
|
||||
folder: 'tables',
|
||||
children: data.tables.map((t) => ({
|
||||
id: `tables-${t.schemaName}-${t.tableName}`,
|
||||
label: t.fullName ?? `[${t.schemaName}].[${t.tableName}]`,
|
||||
type: 'object' as const,
|
||||
folder: 'tables' as FolderKey,
|
||||
data: t,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: 'views',
|
||||
label: `Views (${data.views.length})`,
|
||||
type: 'folder',
|
||||
folder: 'views',
|
||||
children: data.views.map((v) => makeObjectNode('views', v)),
|
||||
},
|
||||
{
|
||||
id: 'procedures',
|
||||
label: `Stored Procedures (${data.storedProcedures.length})`,
|
||||
type: 'folder',
|
||||
folder: 'procedures',
|
||||
children: data.storedProcedures.map((p) => makeObjectNode('procedures', p)),
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
label: `Functions (${data.functions.length})`,
|
||||
type: 'folder',
|
||||
folder: 'functions',
|
||||
children: data.functions.map((f) => makeObjectNode('functions', f)),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
setTreeData(tree)
|
||||
} catch (error: any) {
|
||||
|
|
@ -181,12 +187,17 @@ const SqlObjectExplorer = ({
|
|||
}
|
||||
|
||||
const handleNodeClick = (node: TreeNode) => {
|
||||
if (node.type !== 'object') { toggleNode(node.id); return }
|
||||
if (node.type !== 'object') {
|
||||
toggleNode(node.id)
|
||||
return
|
||||
}
|
||||
|
||||
if (node.folder === 'tables') {
|
||||
// Generate SELECT template for tables
|
||||
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?.(
|
||||
isPostgreSql
|
||||
? `SELECT *\nFROM ${fullName}\nLIMIT 10;`
|
||||
|
|
@ -216,11 +227,7 @@ const SqlObjectExplorer = ({
|
|||
|
||||
const obj = node.data as SqlNativeObjectDto
|
||||
const objectType =
|
||||
node.folder === 'views'
|
||||
? 'view'
|
||||
: node.folder === 'procedures'
|
||||
? 'procedure'
|
||||
: 'function'
|
||||
node.folder === 'views' ? 'view' : node.folder === 'procedures' ? 'procedure' : 'function'
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
|
|
@ -260,15 +267,19 @@ const SqlObjectExplorer = ({
|
|||
const buildDropSql = (node: TreeNode): string => {
|
||||
if (node.folder === 'tables') {
|
||||
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};`
|
||||
}
|
||||
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 =
|
||||
node.folder === 'views' ? 'VIEW' :
|
||||
node.folder === 'procedures' ? 'PROCEDURE' :
|
||||
'FUNCTION'
|
||||
node.folder === 'views' ? 'VIEW' : node.folder === 'procedures' ? 'PROCEDURE' : 'FUNCTION'
|
||||
return isPostgreSql ? `DROP ${keyword} IF EXISTS ${fullName};` : `DROP ${keyword} ${fullName};`
|
||||
}
|
||||
|
||||
|
|
@ -317,14 +328,12 @@ const SqlObjectExplorer = ({
|
|||
if (node.type === 'folder') {
|
||||
const open = expandedNodes.has(node.id)
|
||||
const cls = FOLDER_META[node.folder!]?.color ?? 'text-blue-500'
|
||||
return open
|
||||
? <FaRegFolderOpen className={cls} />
|
||||
: <FaRegFolder className={cls} />
|
||||
return open ? <FaRegFolderOpen className={cls} /> : <FaRegFolder className={cls} />
|
||||
}
|
||||
if (node.folder === 'tables') return <FaTable className="text-teal-500" />
|
||||
if (node.folder === 'views') return <FaEye className="text-purple-500" />
|
||||
if (node.folder === 'procedures') return <FaCog className="text-green-600" />
|
||||
if (node.folder === 'functions') return <FaCode className="text-orange-500" />
|
||||
if (node.folder === 'tables') return <FaTable className="text-teal-500" />
|
||||
if (node.folder === 'views') return <FaEye className="text-purple-500" />
|
||||
if (node.folder === 'procedures') return <FaCog className="text-green-600" />
|
||||
if (node.folder === 'functions') return <FaCode className="text-orange-500" />
|
||||
return <FaColumns className="text-gray-400" />
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
||||
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' && (
|
||||
<input
|
||||
|
|
@ -351,13 +363,18 @@ const SqlObjectExplorer = ({
|
|||
{getIcon(node)}
|
||||
<span className="text-sm flex-1 truncate">{node.label}</span>
|
||||
{node.type === 'object' && (
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
onClick={(e) => { e.stopPropagation(); setDropConfirm({ node }) }}
|
||||
>
|
||||
<FaTrash className="text-xs" />
|
||||
</button>
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTrash className="text-xs" />}
|
||||
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>
|
||||
{isExpanded && node.children && (
|
||||
|
|
@ -387,12 +404,12 @@ const SqlObjectExplorer = ({
|
|||
|
||||
// Context menu items per folder
|
||||
const ctxNode = contextMenu.node
|
||||
const isTableObj = ctxNode?.type === 'object' && ctxNode.folder === 'tables'
|
||||
const isNativeObj = ctxNode?.type === 'object' && ctxNode.folder !== 'tables'
|
||||
const isTablesDir = ctxNode?.id === 'tables'
|
||||
const isViewsDir = ctxNode?.id === 'views'
|
||||
const isProcsDir = ctxNode?.id === 'procedures'
|
||||
const isFuncsDir = ctxNode?.id === 'functions'
|
||||
const isTableObj = ctxNode?.type === 'object' && ctxNode.folder === 'tables'
|
||||
const isNativeObj = ctxNode?.type === 'object' && ctxNode.folder !== 'tables'
|
||||
const isTablesDir = ctxNode?.id === 'tables'
|
||||
const isViewsDir = ctxNode?.id === 'views'
|
||||
const isProcsDir = ctxNode?.id === 'procedures'
|
||||
const isFuncsDir = ctxNode?.id === 'functions'
|
||||
const isFolderNode = ctxNode?.type === 'folder'
|
||||
|
||||
return (
|
||||
|
|
@ -406,22 +423,38 @@ const SqlObjectExplorer = ({
|
|||
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"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
onClick={loadObjects}
|
||||
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')}
|
||||
>
|
||||
<FaSyncAlt className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tree */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading && <div className="text-center py-8 text-gray-500 text-sm">{translate('::App.Platform.Loading')}</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>}
|
||||
{loading && (
|
||||
<div className="text-center py-8 text-gray-500 text-sm">
|
||||
{translate('::App.Platform.Loading')}
|
||||
</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>
|
||||
|
||||
{/* Context menu */}
|
||||
|
|
@ -434,8 +467,10 @@ const SqlObjectExplorer = ({
|
|||
>
|
||||
{/* TABLE object <20> Design */}
|
||||
{isTableObj && (
|
||||
<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={() => {
|
||||
const t = ctxNode!.data as DatabaseTableDto
|
||||
onGenerateTableScript?.(t.schemaName, t.tableName)
|
||||
|
|
@ -443,12 +478,14 @@ const SqlObjectExplorer = ({
|
|||
}}
|
||||
>
|
||||
<FaCode className="text-blue-600" /> Script olustur
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isTableObj && (
|
||||
<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={() => {
|
||||
const t = ctxNode!.data as DatabaseTableDto
|
||||
onDesignTable?.(t.schemaName, t.tableName)
|
||||
|
|
@ -456,53 +493,92 @@ const SqlObjectExplorer = ({
|
|||
}}
|
||||
>
|
||||
<FaTable className="text-teal-600" /> Design Table
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* NATIVE object <20> View Definition */}
|
||||
{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={() => {
|
||||
const obj = ctxNode!.data as SqlNativeObjectDto
|
||||
onViewDefinition?.(obj.schemaName, obj.objectName)
|
||||
closeCtx()
|
||||
}}>
|
||||
{ctxNode!.folder === 'views' && <FaEye className="text-purple-500" />}
|
||||
{ctxNode!.folder === 'procedures' && <FaCog className="text-green-600" />}
|
||||
{ctxNode!.folder === 'functions' && <FaCode className="text-orange-500" />}
|
||||
}}
|
||||
>
|
||||
{ctxNode!.folder === 'views' && <FaEye className="text-purple-500" />}
|
||||
{ctxNode!.folder === 'procedures' && <FaCog className="text-green-600" />}
|
||||
{ctxNode!.folder === 'functions' && <FaCode className="text-orange-500" />}
|
||||
View Definition
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* FOLDER <20> New ... */}
|
||||
{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"
|
||||
onClick={() => { onNewTable?.(); closeCtx() }}>
|
||||
<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={() => {
|
||||
onNewTable?.()
|
||||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaPlus className="text-teal-600" /> New Table
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{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"
|
||||
onClick={() => { onTemplateSelect?.('', 'create-view'); closeCtx() }}>
|
||||
<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={() => {
|
||||
onTemplateSelect?.('', 'create-view')
|
||||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaPlus className="text-purple-500" /> New View
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{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"
|
||||
onClick={() => { onTemplateSelect?.('', 'create-procedure'); closeCtx() }}>
|
||||
<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={() => {
|
||||
onTemplateSelect?.('', 'create-procedure')
|
||||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaPlus className="text-green-600" /> New Stored Procedure
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{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"
|
||||
onClick={() => { onTemplateSelect?.('', 'create-scalar-function'); closeCtx() }}>
|
||||
<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={() => {
|
||||
onTemplateSelect?.('', 'create-scalar-function')
|
||||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaPlus className="text-orange-500" /> New Scalar Function
|
||||
</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"
|
||||
onClick={() => { onTemplateSelect?.('', 'create-table-function'); closeCtx() }}>
|
||||
</Button>
|
||||
<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={() => {
|
||||
onTemplateSelect?.('', 'create-table-function')
|
||||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaPlus className="text-orange-500" /> New Table-Valued Function
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -510,10 +586,17 @@ const SqlObjectExplorer = ({
|
|||
{isFolderNode && (
|
||||
<>
|
||||
<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"
|
||||
onClick={() => { loadObjects(); closeCtx() }}>
|
||||
<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={() => {
|
||||
loadObjects()
|
||||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaSyncAlt /> {translate('::App.Platform.Refresh')}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -523,8 +606,14 @@ const SqlObjectExplorer = ({
|
|||
{/* Drop Confirm Dialog */}
|
||||
{dropConfirm && (
|
||||
<>
|
||||
<div 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="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">
|
||||
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
|
||||
<h6 className="font-semibold text-gray-900 dark:text-gray-100">Drop Object</h6>
|
||||
|
|
@ -536,21 +625,26 @@ const SqlObjectExplorer = ({
|
|||
{buildDropSql(dropConfirm.node)}
|
||||
</code>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
<Button
|
||||
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)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
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}
|
||||
>
|
||||
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
|
||||
Drop
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1629,28 +1629,32 @@ GO`,
|
|||
|
||||
{/* Mode Tabs */}
|
||||
<div className="flex gap-2 mb-2 border-b flex-shrink-0">
|
||||
<button
|
||||
<Button
|
||||
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'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
? 'border-blue-500 !text-blue-600'
|
||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
disabled={isCopyingObjects}
|
||||
>
|
||||
{translate('::App.Platform.CopyObjects') || 'Nesneleri Kopyala'}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
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'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
? 'border-blue-500 !text-blue-600'
|
||||
: 'border-transparent text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
disabled={isCopyingObjects}
|
||||
>
|
||||
{translate('::App.Platform.DirectSqlScript') || 'Direkt SQL Script'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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 }[] = [
|
||||
{ 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: 'Index', label: 'Index', desc: 'App.SqlQueryManager.IndexType_Index_Desc' },
|
||||
]
|
||||
|
|
@ -345,7 +349,10 @@ function generateCreateTableSql(
|
|||
}
|
||||
} else if (allInnerLines.length > 0) {
|
||||
// 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
|
||||
|
|
@ -358,16 +365,13 @@ function generateCreateTableSql(
|
|||
if (idx.indexType === 'UniqueKey') {
|
||||
extraIndexLines.push(`-- 🔒 Unique Key: [${idx.indexName}]`)
|
||||
extraIndexLines.push(`ALTER TABLE ${fullTableName}`)
|
||||
extraIndexLines.push(` ADD CONSTRAINT [${idx.indexName}] UNIQUE ${clustered} (${colsSql});`)
|
||||
extraIndexLines.push(
|
||||
` ADD CONSTRAINT [${idx.indexName}] UNIQUE ${clustered} (${colsSql});`,
|
||||
)
|
||||
} else {
|
||||
extraIndexLines.push(`-- 📋 Index: [${idx.indexName}]`)
|
||||
extraIndexLines.push(
|
||||
...buildCreateIndexIfNotExistsSql(
|
||||
fullTableName,
|
||||
idx.indexName,
|
||||
idx.isClustered,
|
||||
colsSql,
|
||||
),
|
||||
...buildCreateIndexIfNotExistsSql(fullTableName, idx.indexName, idx.isClustered, colsSql),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -499,7 +503,10 @@ function unwrapDefaultExpression(value: string): string {
|
|||
return result
|
||||
}
|
||||
|
||||
function mapSqlTypeToDesigner(dataTypeRaw: string, lengthRaw: string): {
|
||||
function mapSqlTypeToDesigner(
|
||||
dataTypeRaw: string,
|
||||
lengthRaw: string,
|
||||
): {
|
||||
dataType: SqlDataType
|
||||
maxLength: string
|
||||
} {
|
||||
|
|
@ -562,9 +569,7 @@ function parseCreateTableColumns(script: string): ColumnDefinition[] {
|
|||
const trimmed = def.trim()
|
||||
if (!trimmed) continue
|
||||
|
||||
if (
|
||||
/^(CONSTRAINT|PRIMARY\s+KEY|UNIQUE\s+KEY|UNIQUE|FOREIGN\s+KEY|CHECK)\b/i.test(trimmed)
|
||||
) {
|
||||
if (/^(CONSTRAINT|PRIMARY\s+KEY|UNIQUE\s+KEY|UNIQUE|FOREIGN\s+KEY|CHECK)\b/i.test(trimmed)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -780,12 +785,7 @@ function generateAlterTableSql(
|
|||
} else {
|
||||
lines.push(`-- 📋 Index: [${ix.indexName}]`)
|
||||
lines.push(
|
||||
...buildCreateIndexIfNotExistsSql(
|
||||
fullTableName,
|
||||
ix.indexName,
|
||||
ix.isClustered,
|
||||
colsSql,
|
||||
),
|
||||
...buildCreateIndexIfNotExistsSql(fullTableName, ix.indexName, ix.isClustered, colsSql),
|
||||
)
|
||||
}
|
||||
lines.push('')
|
||||
|
|
@ -830,15 +830,17 @@ function generateAlterTableSql(
|
|||
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
|
||||
|
||||
function StepContentWrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-[420px] flex flex-col">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className="min-h-[420px] flex flex-col">{children}</div>
|
||||
}
|
||||
|
||||
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
||||
|
|
@ -1321,9 +1323,7 @@ const SqlTableDesignerDialog = ({
|
|||
|
||||
setColumns((prev) => {
|
||||
const existingNonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
||||
const existingNames = new Set(
|
||||
existingNonEmpty.map((c) => c.columnName.trim().toLowerCase()),
|
||||
)
|
||||
const existingNames = new Set(existingNonEmpty.map((c) => c.columnName.trim().toLowerCase()))
|
||||
|
||||
const toAdd: ColumnDefinition[] = []
|
||||
for (const col of parsedColumns) {
|
||||
|
|
@ -1433,13 +1433,7 @@ const SqlTableDesignerDialog = ({
|
|||
|
||||
setSettings((s) => ({ ...s, tableName: recalculatedTableName }))
|
||||
syncAutoPkName(recalculatedTableName)
|
||||
}, [
|
||||
isEditMode,
|
||||
columns,
|
||||
settings.menuPrefix,
|
||||
settings.entityName,
|
||||
settings.tableName,
|
||||
])
|
||||
}, [isEditMode, columns, settings.menuPrefix, settings.entityName, settings.tableName])
|
||||
|
||||
// ── FK Relationship handlers ───────────────────────────────────────────────
|
||||
|
||||
|
|
@ -1498,7 +1492,11 @@ const SqlTableDesignerDialog = ({
|
|||
}
|
||||
|
||||
const saveFk = () => {
|
||||
if (!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim() || !fkForm.referencedColumn.trim()) {
|
||||
if (
|
||||
!fkForm.fkColumnName.trim() ||
|
||||
!fkForm.referencedTable.trim() ||
|
||||
!fkForm.referencedColumn.trim()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1693,7 +1691,7 @@ const SqlTableDesignerDialog = ({
|
|||
const STEP_LABELS = [
|
||||
translate('::App.SqlQueryManager.ColumnDesign'),
|
||||
translate('::App.SqlQueryManager.EntitySettings'),
|
||||
translate('::App.SqlQueryManager.IndexKeys'),
|
||||
translate('::App.SqlQueryManager.IndexKeys'),
|
||||
translate('::App.SqlQueryManager.Relationships'),
|
||||
translate('::App.SqlQueryManager.TSqlPreview'),
|
||||
]
|
||||
|
|
@ -1906,29 +1904,35 @@ const SqlTableDesignerDialog = ({
|
|||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-center gap-1">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => moveColumn(col.id, 'up')}
|
||||
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')}
|
||||
>
|
||||
<FaArrowUp className="text-xs" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
onClick={() => moveColumn(col.id, 'down')}
|
||||
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')}
|
||||
>
|
||||
<FaArrowDown className="text-xs" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
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')}
|
||||
>
|
||||
<FaTrash className="text-xs" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -1959,27 +1963,32 @@ const SqlTableDesignerDialog = ({
|
|||
{translate('::App.SqlQueryManager.MenuName')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isEditMode}
|
||||
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" />{' '}
|
||||
{translate('::ListForms.Wizard.Step1.AddNewMenu') || 'Add Menu'}
|
||||
</button>
|
||||
</Button>
|
||||
{settings.menuValue && !isEditMode && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedMenuCode('')
|
||||
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" />{' '}
|
||||
{translate('::ListForms.Wizard.ClearSelection') || 'Clear'}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2047,8 +2056,8 @@ const SqlTableDesignerDialog = ({
|
|||
{/* Warning: no Id column */}
|
||||
{!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">
|
||||
Full Audited kolonları seçmediğiniz için PK kolonu manuel tanımlanmalıdır. Bu kolon
|
||||
int veya uniqueidentifier olabilir.
|
||||
Full Audited kolonları seçmediğiniz için PK kolonu manuel tanımlanmalıdır. Bu kolon int
|
||||
veya uniqueidentifier olabilir.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -2074,13 +2083,16 @@ const SqlTableDesignerDialog = ({
|
|||
? translate('::App.SqlQueryManager.NoRelationshipsDefined')
|
||||
: `${relationships.length} ${translate('::App.SqlQueryManager.Relationship')}`}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
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" />{' '}
|
||||
{translate('::App.SqlQueryManager.AddRelationship')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
|
|
@ -2137,20 +2149,24 @@ const SqlTableDesignerDialog = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => openEditFk(rel)}
|
||||
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"
|
||||
>
|
||||
<FaEdit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaEdit className="w-3.5 h-3.5" />}
|
||||
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)}
|
||||
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"
|
||||
>
|
||||
<FaTrash className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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>
|
||||
))}
|
||||
|
|
@ -2170,12 +2186,14 @@ const SqlTableDesignerDialog = ({
|
|||
? translate('::App.SqlQueryManager.EditRelationship')
|
||||
: translate('::App.SqlQueryManager.AddNewRelationship')}
|
||||
</h2>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setFkModalOpen(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
size="sm"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="w-4 h-4" />}
|
||||
className="!h-8 !w-8 !px-0 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
|
|
@ -2187,11 +2205,13 @@ const SqlTableDesignerDialog = ({
|
|||
</label>
|
||||
<div className="flex gap-2">
|
||||
{REL_TYPES.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.value}
|
||||
type="button"
|
||||
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
|
||||
? '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'
|
||||
|
|
@ -2199,7 +2219,7 @@ const SqlTableDesignerDialog = ({
|
|||
>
|
||||
{t.label}
|
||||
<span className="block text-xs opacity-70">{t.desc}</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2215,9 +2235,7 @@ const SqlTableDesignerDialog = ({
|
|||
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"
|
||||
>
|
||||
<option value="">
|
||||
{translate('::App.Select')}
|
||||
</option>
|
||||
<option value="">{translate('::App.Select')}</option>
|
||||
{columns
|
||||
.filter((c) => c.columnName.trim())
|
||||
.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"
|
||||
>
|
||||
<option value="">
|
||||
{translate('::App.Select')}
|
||||
</option>
|
||||
<option value="">{translate('::App.Select')}</option>
|
||||
{dbTables.map((t) => (
|
||||
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
|
||||
{t.tableName}
|
||||
|
|
@ -2270,7 +2286,9 @@ const SqlTableDesignerDialog = ({
|
|||
<option
|
||||
key={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())
|
||||
? `${col} (PK/UNIQUE)`
|
||||
|
|
@ -2278,11 +2296,13 @@ const SqlTableDesignerDialog = ({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
{fkForm.referencedTable && !targetColsLoading && targetTableKeyColumns.length === 0 && (
|
||||
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı.
|
||||
</p>
|
||||
)}
|
||||
{fkForm.referencedTable &&
|
||||
!targetColsLoading &&
|
||||
targetTableKeyColumns.length === 0 && (
|
||||
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cascade */}
|
||||
|
|
@ -2362,13 +2382,15 @@ const SqlTableDesignerDialog = ({
|
|||
|
||||
{/* 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">
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveFk}
|
||||
disabled={
|
||||
!fkForm.fkColumnName.trim() ||
|
||||
|
|
@ -2378,10 +2400,13 @@ const SqlTableDesignerDialog = ({
|
|||
(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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
|
@ -2423,20 +2448,27 @@ const SqlTableDesignerDialog = ({
|
|||
? translate('::App.SqlQueryManager.NoIndexesDefined')
|
||||
: `${indexes.length} ${translate('::App.SqlQueryManager.IndexKey')}`}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{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">
|
||||
<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-xs text-gray-400 mt-1">{translate('::App.SqlQueryManager.StepIsOptional')}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{translate('::App.SqlQueryManager.NoIndexesDefined')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{translate('::App.SqlQueryManager.StepIsOptional')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -2481,20 +2513,24 @@ const SqlTableDesignerDialog = ({
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => openEditIndex(idx)}
|
||||
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"
|
||||
>
|
||||
<FaEdit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaEdit className="w-3.5 h-3.5" />}
|
||||
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)}
|
||||
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"
|
||||
>
|
||||
<FaTrash className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
size="xs"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
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>
|
||||
)
|
||||
|
|
@ -2511,14 +2547,18 @@ const SqlTableDesignerDialog = ({
|
|||
{/* Modal Header */}
|
||||
<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">
|
||||
{editingIndexId ? translate('::App.SqlQueryManager.EditIndexKey') : translate('::App.SqlQueryManager.AddNewIndexKey')}
|
||||
{editingIndexId
|
||||
? translate('::App.SqlQueryManager.EditIndexKey')
|
||||
: translate('::App.SqlQueryManager.AddNewIndexKey')}
|
||||
</h2>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setIndexModalOpen(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
size="sm"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="w-4 h-4" />}
|
||||
className="!h-8 !w-8 !px-0 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
|
|
@ -2530,7 +2570,7 @@ const SqlTableDesignerDialog = ({
|
|||
</label>
|
||||
<div className="flex gap-2">
|
||||
{INDEX_TYPES.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.value}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
|
@ -2540,7 +2580,9 @@ const SqlTableDesignerDialog = ({
|
|||
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
|
||||
? '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'
|
||||
|
|
@ -2548,7 +2590,7 @@ const SqlTableDesignerDialog = ({
|
|||
>
|
||||
{t.label}
|
||||
<span className="block text-xs opacity-70">{translate('::' + t.desc)}</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2562,7 +2604,9 @@ const SqlTableDesignerDialog = ({
|
|||
type="text"
|
||||
value={indexForm.indexName}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2590,8 +2634,12 @@ const SqlTableDesignerDialog = ({
|
|||
<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="col-span-1" />
|
||||
<div className="col-span-7">{translate('::App.Listform.ListformField.Column')}</div>
|
||||
<div className="col-span-4">{translate('::App.SqlQueryManager.SortOrder')}</div>
|
||||
<div className="col-span-7">
|
||||
{translate('::App.Listform.ListformField.Column')}
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
{translate('::App.SqlQueryManager.SortOrder')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-44 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{columns
|
||||
|
|
@ -2681,9 +2729,7 @@ const SqlTableDesignerDialog = ({
|
|||
</label>
|
||||
<textarea
|
||||
value={indexForm.description}
|
||||
onChange={(e) =>
|
||||
setIndexForm((f) => ({ ...f, description: e.target.value }))
|
||||
}
|
||||
onChange={(e) => setIndexForm((f) => ({ ...f, description: e.target.value }))}
|
||||
rows={2}
|
||||
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"
|
||||
|
|
@ -2693,19 +2739,24 @@ const SqlTableDesignerDialog = ({
|
|||
|
||||
{/* 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">
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveIndex}
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
|
@ -2722,12 +2773,14 @@ const SqlTableDesignerDialog = ({
|
|||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{translate('::App.SqlQueryManager.GeneratedSqlDescription')}
|
||||
</p>
|
||||
<button
|
||||
className="text-xs text-blue-500 hover:underline"
|
||||
<Button
|
||||
size="xs"
|
||||
variant="plain"
|
||||
className="!h-auto !px-1 !py-0 text-xs text-blue-500 hover:underline"
|
||||
onClick={() => navigator.clipboard.writeText(generatedSql)}
|
||||
>
|
||||
{translate('::App.SqlQueryManager.Copy')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="bg-gray-900 text-green-300 rounded-lg p-4 text-xs overflow-auto max-h-96 leading-relaxed whitespace-pre">
|
||||
{generatedSql}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
||||
import { ReactElement, useRef, useState } from 'react'
|
||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
import {
|
||||
hideImageHoverPreview,
|
||||
openImageInNewTab,
|
||||
|
|
@ -117,10 +118,13 @@ const ImageUploadEditorComponent = ({
|
|||
}}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
title="Sil"
|
||||
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={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
|
|
@ -139,7 +143,7 @@ const ImageUploadEditorComponent = ({
|
|||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -154,11 +158,14 @@ const ImageUploadEditorComponent = ({
|
|||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
title="Görsel Yükle"
|
||||
disabled={uploading}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
variant="solid"
|
||||
shape="none"
|
||||
className="!h-auto !rounded !px-2.5 !py-1 !text-[13px]"
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: uploading ? '#aaa' : '#0078d4',
|
||||
|
|
@ -177,7 +184,7 @@ const ImageUploadEditorComponent = ({
|
|||
) : (
|
||||
<FaUpload style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
||||
import { ReactElement } from 'react'
|
||||
import { Button } from '@/components/ui'
|
||||
import {
|
||||
hideImageHoverPreview,
|
||||
openImageInNewTab,
|
||||
|
|
@ -112,11 +113,14 @@ const ImageViewerEditorComponent = ({
|
|||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
||||
{urls.map((url, index) => (
|
||||
<button
|
||||
<Button
|
||||
key={`${url}-${index}`}
|
||||
type="button"
|
||||
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
|
||||
src={url}
|
||||
|
|
@ -136,7 +140,7 @@ const ImageViewerEditorComponent = ({
|
|||
currentTarget.src = '/img/others/no-image.png'
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,9 +59,15 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
|
|||
? translate('::App.Forum.PostManagement.ReplytoTopic')
|
||||
: translate('::App.Forum.PostManagement.NewPost')}
|
||||
</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" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
|
|
|
|||
|
|
@ -43,9 +43,15 @@ export function CreateTopicModal({ onClose, onSubmit }: CreateTopicModalProps) {
|
|||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{translate('::App.Forum.TopicManagement.NewTopic')}
|
||||
</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" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ForumPost } from '@/proxy/forum/forum'
|
|||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { showDbDateAsIs } from '@/utils/dateUtils'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface PostCardProps {
|
||||
post: ForumPost
|
||||
|
|
@ -64,26 +65,32 @@ export function ForumPostCard({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
? 'bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-200 hover:bg-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-red-100 text-red-600 hover:!bg-red-200 dark:!bg-red-900 dark:text-red-200 dark:hover:!bg-red-800'
|
||||
: '!bg-gray-100 text-gray-600 hover:!bg-gray-200 dark:!bg-gray-800 dark:text-gray-300 dark:hover:!bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
|
||||
<span>{post.likeCount}</span>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{!isFirst && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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" />
|
||||
<span>{translate('::App.Forum.PostManagement.PostReply')}</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</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">
|
||||
{selectedCategory && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => handleBreadcrumbClick('forum')}
|
||||
className={`transition-colors ${
|
||||
className={`!inline-flex !h-auto items-center !bg-transparent !p-0 transition-colors ${
|
||||
viewState === 'categories'
|
||||
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
||||
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Forum</div>
|
||||
</button>
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Forum</div>
|
||||
</Button>
|
||||
<span>/</span>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
onClick={() => handleBreadcrumbClick('category')}
|
||||
className={`transition-colors ${
|
||||
className={`!inline-flex !h-auto items-center !bg-transparent !p-0 transition-colors ${
|
||||
viewState === 'topics'
|
||||
? 'text-gray-900 dark:text-gray-100 font-medium cursor-default'
|
||||
: 'hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
{selectedCategory.name}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{selectedTopic && (
|
||||
|
|
@ -350,7 +356,7 @@ export function ForumView({
|
|||
icon={<FaPlus className="w-4 h-4" />}
|
||||
variant="solid"
|
||||
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>
|
||||
</Button>
|
||||
|
|
@ -361,7 +367,7 @@ export function ForumView({
|
|||
icon={<FaPlus className="w-4 h-4" />}
|
||||
variant="solid"
|
||||
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>
|
||||
</Button>
|
||||
|
|
@ -373,7 +379,7 @@ export function ForumView({
|
|||
icon={<FaSearch className="w-4 h-4" />}
|
||||
onClick={() => setIsSearchModalOpen(true)}
|
||||
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">
|
||||
{translate('::App.Forum.TopicManagement.Searchtopics')}
|
||||
|
|
@ -385,7 +391,7 @@ export function ForumView({
|
|||
icon={<FaSearch className="w-5 h-5" />}
|
||||
onClick={() => setIsSearchModalOpen(true)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { FaTimes, FaSearch, FaFolder, FaRegComment, FaFileAlt } from 'react-icon
|
|||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||
import { showDbDateAsIs } from '@/utils/dateUtils'
|
||||
import { useForumSearch } from '@/utils/hooks/useForumSearch'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface SearchModalProps {
|
||||
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"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors ml-3"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="h-5 w-5" />}
|
||||
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 className="overflow-y-auto max-h-96">
|
||||
|
|
@ -136,14 +138,18 @@ export function SearchModal({
|
|||
Categories ({searchResults.categories.length})
|
||||
</div>
|
||||
{searchResults.categories.map((category, index) => (
|
||||
<button
|
||||
<Button
|
||||
key={`category-${category.id}`}
|
||||
onClick={() => {
|
||||
onCategorySelect(category)
|
||||
onClose()
|
||||
}}
|
||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
||||
selectedIndex === index ? 'bg-blue-50 dark:bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400' : ''
|
||||
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 === 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">
|
||||
|
|
@ -153,14 +159,18 @@ export function SearchModal({
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{category.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">{category.topicCount} topics</div>
|
||||
</button>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{category.topicCount} topics
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -174,15 +184,17 @@ export function SearchModal({
|
|||
{searchResults.topics.map((topic, index) => {
|
||||
const globalIndex = searchResults.categories.length + index
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
key={`topic-${topic.id}`}
|
||||
onClick={() => {
|
||||
onTopicSelect(topic)
|
||||
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
|
||||
? '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 className="text-xs text-gray-400 dark:text-gray-500">{topic.replyCount} replies</div>
|
||||
</button>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{topic.replyCount} replies
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -218,15 +232,17 @@ export function SearchModal({
|
|||
const globalIndex =
|
||||
searchResults.categories.length + searchResults.topics.length + index
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
key={`post-${post.id}`}
|
||||
onClick={() => {
|
||||
onPostSelect(post)
|
||||
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
|
||||
? '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 className="text-xs text-gray-400">{post.likeCount} likes</div>
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import useLocale from '@/utils/hooks/useLocale'
|
|||
import { currentLocalDate } from '@/utils/dateUtils'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import UpcomingEvents from './widgets/UpcomingEvents'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(isBetween)
|
||||
|
|
@ -245,7 +246,12 @@ const IntranetDashboard: React.FC = () => {
|
|||
const renderWidgetComponent = (widgetId: string) => {
|
||||
switch (widgetId) {
|
||||
case 'upcoming-events':
|
||||
return <UpcomingEvents events={intranetDashboard?.events || []} onEventClick={setSelectedEvent} />
|
||||
return (
|
||||
<UpcomingEvents
|
||||
events={intranetDashboard?.events || []}
|
||||
onEventClick={setSelectedEvent}
|
||||
/>
|
||||
)
|
||||
case 'today-birthdays':
|
||||
return <TodayBirthdays employees={intranetDashboard?.birthdays || []} />
|
||||
case 'documents':
|
||||
|
|
@ -422,12 +428,14 @@ const IntranetDashboard: React.FC = () => {
|
|||
>
|
||||
🎨 {translate('::App.Platform.Intranet.Dashboard.DesignMode')}
|
||||
</label>
|
||||
<button
|
||||
<Button
|
||||
id="design-mode-toggle"
|
||||
onClick={() => setIsDesignMode(!isDesignMode)}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
${isDesignMode ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}
|
||||
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 hover:!bg-blue-600' : '!bg-gray-300 hover:!bg-gray-300 dark:!bg-gray-600 dark:hover:!bg-gray-600'}
|
||||
`}
|
||||
role="switch"
|
||||
aria-checked={isDesignMode}
|
||||
|
|
@ -438,21 +446,23 @@ const IntranetDashboard: React.FC = () => {
|
|||
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Reset Button - Sadece design mode aktifken görünsün */}
|
||||
{isDesignMode && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
localStorage.removeItem(WIDGET_ORDER_KEY)
|
||||
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"
|
||||
>
|
||||
🔄 {translate('::App.Platform.Intranet.Dashboard.Reset')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -630,10 +640,7 @@ const IntranetDashboard: React.FC = () => {
|
|||
|
||||
<AnimatePresence>
|
||||
{selectedEvent && (
|
||||
<EventModal
|
||||
event={selectedEvent}
|
||||
onClose={() => setSelectedEvent(null)}
|
||||
/>
|
||||
<EventModal event={selectedEvent} onClose={() => setSelectedEvent(null)} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import LocationPicker from './LocationPicker'
|
|||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { Avatar } from '@/components/ui'
|
||||
import { Avatar, Button } from '@/components/ui'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
|
||||
interface CreatePostProps {
|
||||
|
|
@ -189,18 +189,20 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} (
|
||||
{mediaItems.length})
|
||||
</h4>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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.RemoveAllMediaTitle',
|
||||
)}
|
||||
>
|
||||
{translate('::Cancel')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{mediaItems.map((item) => (
|
||||
|
|
@ -224,25 +226,31 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
variant="solid"
|
||||
color="red-600"
|
||||
shape="circle"
|
||||
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">
|
||||
{item.type === 'image' ? '📷' : '🎥'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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" />
|
||||
</button>
|
||||
<span className="flex h-full w-full items-center justify-center">
|
||||
<FaImages className="h-6 w-6 text-gray-400" />
|
||||
</span>
|
||||
</Button>
|
||||
</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">
|
||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}
|
||||
</h4>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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(
|
||||
'::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle',
|
||||
)}
|
||||
>
|
||||
{translate('::Cancel')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<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">
|
||||
|
|
@ -296,16 +306,18 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}
|
||||
</h4>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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')}
|
||||
>
|
||||
{translate('::Cancel')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
{pollOptions.length > 2 && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => removePollOption(index)}
|
||||
className="p-2 text-gray-500 hover:text-red-600"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="h-5 w-5" />}
|
||||
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>
|
||||
{pollOptions.length < 6 && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</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"
|
||||
>
|
||||
<div className="flex gap-2 relative">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (mediaType === 'media' && mediaItems.length > 0) {
|
||||
|
|
@ -376,11 +391,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
setShowMediaManager(true)
|
||||
}
|
||||
}}
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
className={classNames(
|
||||
'p-2 rounded-full transition-colors',
|
||||
'relative !h-9 !w-9 !px-0 overflow-visible transition-colors',
|
||||
mediaType === 'media'
|
||||
? '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',
|
||||
? '!bg-blue-100 text-blue-600 dark:!bg-blue-900 dark:text-blue-400'
|
||||
: 'text-gray-600 hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700',
|
||||
)}
|
||||
title={
|
||||
mediaType === 'media'
|
||||
|
|
@ -388,14 +405,16 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
: 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 && (
|
||||
<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}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Başka bir tip seçiliyse temizle
|
||||
|
|
@ -404,49 +423,55 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
}
|
||||
setMediaType(mediaType === 'poll' ? null : 'poll')
|
||||
}}
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChartBar className="h-5 w-5" />}
|
||||
className={classNames(
|
||||
'p-2 rounded-full transition-colors',
|
||||
'!h-9 !w-9 !px-0 transition-colors',
|
||||
mediaType === 'poll'
|
||||
? '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',
|
||||
? '!bg-blue-100 text-blue-600 dark:!bg-blue-900 dark:text-blue-400'
|
||||
: 'text-gray-600 hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700',
|
||||
)}
|
||||
title={
|
||||
mediaType === 'poll'
|
||||
? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')
|
||||
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')
|
||||
}
|
||||
>
|
||||
<FaChartBar className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<FaSmile className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setShowLocationPicker(true)}
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaMapMarkerAlt className="h-5 w-5" />}
|
||||
className={classNames(
|
||||
'p-2 rounded-full transition-colors',
|
||||
'!h-9 !w-9 !px-0 transition-colors',
|
||||
location
|
||||
? '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',
|
||||
? '!bg-blue-100 text-blue-600 dark:!bg-blue-900 dark:text-blue-400'
|
||||
: 'text-gray-600 hover:!bg-gray-100 dark:text-gray-400 dark:hover:!bg-gray-700',
|
||||
)}
|
||||
title={
|
||||
location
|
||||
? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle')
|
||||
: translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')
|
||||
}
|
||||
>
|
||||
<FaMapMarkerAlt className="w-5 h-5" />
|
||||
</button>
|
||||
/>
|
||||
|
||||
{/* Emoji Picker */}
|
||||
{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
|
||||
searchDisabled
|
||||
theme={theme.mode === 'dark' ? Theme.DARK : Theme.LIGHT}
|
||||
|
|
@ -457,13 +482,15 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { FaExternalLinkAlt, FaMapMarkerAlt } from 'react-icons/fa'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface LocationData {
|
||||
id: string
|
||||
|
|
@ -17,13 +18,13 @@ interface LocationMapProps {
|
|||
showDirections?: boolean
|
||||
}
|
||||
|
||||
const LocationMap: React.FC<LocationMapProps> = ({
|
||||
location,
|
||||
const LocationMap: React.FC<LocationMapProps> = ({
|
||||
location,
|
||||
className = '',
|
||||
showDirections = true
|
||||
showDirections = true,
|
||||
}) => {
|
||||
const locationData: LocationData = JSON.parse(location)
|
||||
|
||||
|
||||
const handleOpenGoogleMaps = () => {
|
||||
const url = `https://www.google.com/maps/search/?api=1&query=${locationData.lat},${locationData.lng}&query_place_id=${locationData.placeId || ''}`
|
||||
window.open(url, '_blank')
|
||||
|
|
@ -35,18 +36,20 @@ const LocationMap: React.FC<LocationMapProps> = ({
|
|||
const zoom = 15
|
||||
const size = '600x300'
|
||||
const marker = `color:red|${lat},${lng}`
|
||||
|
||||
|
||||
// Production'da gerçek API key kullanılacak
|
||||
// const apiKey = 'YOUR_GOOGLE_MAPS_API_KEY'
|
||||
// return `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=${size}&markers=${marker}&key=${apiKey}`
|
||||
|
||||
|
||||
// Demo için OpenStreetMap kullanıyoruz
|
||||
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 (
|
||||
<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 */}
|
||||
<div className="relative w-full h-64 group">
|
||||
{/* OpenStreetMap iframe for demo */}
|
||||
|
|
@ -57,10 +60,10 @@ const LocationMap: React.FC<LocationMapProps> = ({
|
|||
allowFullScreen
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
|
||||
{/* Overlay with location info */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
|
||||
|
||||
|
||||
{/* Location Info */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 text-white pointer-events-none">
|
||||
<div className="flex items-start gap-2">
|
||||
|
|
@ -75,13 +78,18 @@ const LocationMap: React.FC<LocationMapProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Click to open overlay - invisible but clickable */}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<span className="sr-only">{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}</span>
|
||||
</button>
|
||||
<span className="sr-only">
|
||||
{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* Hover Effect */}
|
||||
<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 */}
|
||||
{showDirections && (
|
||||
<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}
|
||||
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>{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}</span>
|
||||
</button>
|
||||
<span>
|
||||
{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
||||
</span>
|
||||
</Button>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
|
||||
{translate('::App.Platform.Intranet.SocialWall.LocationMap.ClickForDirections')}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import { motion } from 'framer-motion'
|
||||
import { FaTimes, FaSearch, FaMapMarkerAlt } from 'react-icons/fa'
|
||||
import classNames from 'classnames'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface LocationPickerProps {
|
||||
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">
|
||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.AddLocation')}
|
||||
</h2>
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
||||
>
|
||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="h-5 w-5 text-gray-500 dark:text-gray-400" />}
|
||||
className="!h-9 !w-9 !px-0 transition-colors hover:!bg-gray-100 dark:hover:!bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
|
|
@ -330,14 +332,16 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
|||
) : (
|
||||
<div className="space-y-2">
|
||||
{locations.map((location) => (
|
||||
<button
|
||||
<Button
|
||||
key={location.id}
|
||||
onClick={() => handleSelect(location)}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 border-2 border-blue-500'
|
||||
: 'border-2 border-transparent',
|
||||
? '!bg-blue-50 border-blue-500 dark:!bg-blue-900/30'
|
||||
: 'border-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
@ -385,7 +389,7 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -408,19 +412,22 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) =>
|
|||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { motion } from 'framer-motion'
|
|||
import { FaTimes, FaLink, FaUpload } from 'react-icons/fa'
|
||||
import classNames from 'classnames'
|
||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface MediaManagerProps {
|
||||
media: SocialMediaDto[]
|
||||
|
|
@ -12,7 +13,7 @@ interface MediaManagerProps {
|
|||
}
|
||||
|
||||
const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose }) => {
|
||||
const { translate } = useLocalization();
|
||||
const { translate } = useLocalization()
|
||||
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
|
||||
const [urlInput, setUrlInput] = useState('')
|
||||
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
||||
|
|
@ -50,7 +51,7 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
const newMedia: SocialMediaDto = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
type: mediaType,
|
||||
urls: [urlInput]
|
||||
urls: [urlInput],
|
||||
}
|
||||
|
||||
onChange([...media, newMedia])
|
||||
|
|
@ -72,45 +73,50 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
>
|
||||
{/* Header */}
|
||||
<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>
|
||||
<button
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddMedia')}
|
||||
</h2>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
||||
>
|
||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="h-5 w-5 text-gray-500 dark:text-gray-400" />}
|
||||
className="!h-9 !w-9 !px-0 transition-colors hover:!bg-gray-100 dark:hover:!bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700 px-4">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setActiveTab('upload')}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
icon={<FaUpload className="h-5 w-5" />}
|
||||
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'
|
||||
? '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-blue-600 !text-blue-600'
|
||||
: '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">
|
||||
<FaUpload className="w-5 h-5" />
|
||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
<span>
|
||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveTab('url')}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
icon={<FaLink className="h-5 w-5" />}
|
||||
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'
|
||||
? '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-blue-600 !text-blue-600'
|
||||
: '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>
|
||||
</div>
|
||||
</button>
|
||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddByUrl')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
|
@ -124,7 +130,9 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.ClickToSelectFile')}
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -143,28 +151,32 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.MediaType')}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setMediaType('image')}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? '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-blue-600 !text-white'
|
||||
: '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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setMediaType('video')}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'
|
||||
? '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-blue-600 !text-white'
|
||||
: '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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -173,16 +185,22 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleUrlAdd}
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -191,7 +209,8 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
{media.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<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>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{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">
|
||||
<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="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>
|
||||
|
|
@ -212,14 +234,18 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
variant="solid"
|
||||
color="red-600"
|
||||
shape="circle"
|
||||
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">
|
||||
{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>
|
||||
))}
|
||||
|
|
@ -230,19 +256,24 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
|
|||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
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 })}
|
||||
</button>
|
||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Done', {
|
||||
count: media.length,
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import UserProfileCard from './UserProfileCard'
|
|||
import { SocialPostDto } from '@/proxy/intranet/models'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { Avatar } from '@/components/ui'
|
||||
import { Avatar, Button } from '@/components/ui'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('tr')
|
||||
|
|
@ -202,15 +202,19 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
const isSelected = pollUserVoteId === option.id
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
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}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500':
|
||||
'!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':
|
||||
!isSelected && !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>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -294,13 +298,14 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
</div>
|
||||
</div>
|
||||
{post.isOwnPost && (
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
>
|
||||
<FaTrash className="w-3 h-3" />
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -312,26 +317,30 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
|
|||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-6 pt-3 border-t border-gray-100 dark:border-gray-700">
|
||||
<button
|
||||
<Button
|
||||
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(
|
||||
'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
|
||||
? 'text-red-600 hover:text-red-700'
|
||||
: '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>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
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>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
<FaPaperPlane className="w-5 h-5" />
|
||||
</button>
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
shape="circle"
|
||||
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>
|
||||
</form>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import PostItem from './PostItem'
|
|||
import CreatePost from './CreatePost'
|
||||
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
|
||||
import { intranetService } from '@/services/intranet.service'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
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.type === 'mixed' && postData.media.mediaItems) {
|
||||
|
|
@ -174,26 +182,30 @@ const SocialWall: React.FC = () => {
|
|||
<div className="mx-auto px-4">
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-4 mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
<Button
|
||||
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'
|
||||
? '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-b-blue-600 !text-blue-600'
|
||||
: '!border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{translate('::App.Platform.Intranet.SocialWall.AllPosts')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
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'
|
||||
? '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-b-blue-600 !text-blue-600'
|
||||
: '!border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{translate('::App.Platform.Intranet.SocialWall.MyPosts')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Create Post */}
|
||||
|
|
@ -234,7 +246,8 @@ const SocialWall: React.FC = () => {
|
|||
)}
|
||||
{!hasMore && posts.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
FaPaperPlane,
|
||||
FaHeart,
|
||||
} from 'react-icons/fa'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import { Button, Dialog } from '@/components/ui'
|
||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||
import { AnnouncementCommentDto, AnnouncementDto } from '@/proxy/intranet/models'
|
||||
import useLocale from '@/utils/hooks/useLocale'
|
||||
|
|
@ -197,11 +197,13 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
{announcement.viewCount}{' '}
|
||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleLike}
|
||||
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
|
||||
? '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'
|
||||
|
|
@ -209,7 +211,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
>
|
||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
||||
{likes > 0 && likes}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -244,47 +246,52 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
className="w-full h-56 object-contain cursor-zoom-in"
|
||||
onClick={() => openLightbox(activePhoto)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => openLightbox(activePhoto)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-black/50 hover:bg-black/70 rounded-lg text-white transition-colors"
|
||||
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"
|
||||
>
|
||||
<FaExpand className="w-4 h-4" />
|
||||
</button>
|
||||
/>
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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"
|
||||
>
|
||||
<FaChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronLeft className="h-4 w-4" />}
|
||||
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
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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"
|
||||
>
|
||||
<FaChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronRight className="h-4 w-4" />}
|
||||
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>
|
||||
{images.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
{images.map((img, idx) => (
|
||||
<button
|
||||
<Button
|
||||
key={idx}
|
||||
type="button"
|
||||
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
|
||||
? 'border-blue-500 opacity-100'
|
||||
: 'border-transparent opacity-60 hover:opacity-90'
|
||||
|
|
@ -295,7 +302,7 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
alt={`${announcement.title} thumbnail ${idx + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -405,26 +412,32 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
autoFocus
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={translate('::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder')}
|
||||
placeholder={translate(
|
||||
'::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder',
|
||||
)}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmitComment}
|
||||
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"
|
||||
>
|
||||
<FaPaperPlane className="w-4 h-4" />
|
||||
</button>
|
||||
variant="solid"
|
||||
color="blue-500"
|
||||
shape="none"
|
||||
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>
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
|
||||
{/* Lightbox */}
|
||||
|
|
@ -440,26 +453,29 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
onClick={closeLightbox}
|
||||
/>
|
||||
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
||||
<button
|
||||
<Button
|
||||
onClick={closeLightbox}
|
||||
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
||||
>
|
||||
<FaTimes className="w-8 h-8" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="h-8 w-8" />}
|
||||
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 && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
onClick={prevLightbox}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<button
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronLeft className="h-6 w-6" />}
|
||||
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
|
||||
onClick={nextLightbox}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronRight className="w-6 h-6" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronRight className="h-6 w-6" />}
|
||||
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
|
||||
|
|
@ -475,13 +491,15 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr
|
|||
{images.length > 1 && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
{images.map((_, idx) => (
|
||||
<button
|
||||
<Button
|
||||
key={idx}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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'
|
||||
}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
FaPaperPlane,
|
||||
FaHeart,
|
||||
} from 'react-icons/fa'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import { Button, Dialog } from '@/components/ui'
|
||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||
import { EventCommentDto, EventDto } from '@/proxy/intranet/models'
|
||||
import useLocale from '@/utils/hooks/useLocale'
|
||||
|
|
@ -165,10 +165,12 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
</span>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleLike}
|
||||
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
|
||||
? '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'
|
||||
|
|
@ -176,7 +178,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
>
|
||||
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
|
||||
{likes > 0 && likes}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -209,33 +211,36 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
className="w-full h-56 object-contain cursor-zoom-in"
|
||||
onClick={() => openLightbox(activePhoto)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => openLightbox(activePhoto)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-black/50 hover:bg-black/70 rounded-lg text-white transition-colors"
|
||||
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"
|
||||
>
|
||||
<FaExpand className="w-4 h-4" />
|
||||
</button>
|
||||
/>
|
||||
{photos.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setActivePhoto((i) => (i - 1 + photos.length) % photos.length)
|
||||
}}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
|
||||
>
|
||||
<FaChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronLeft className="h-4 w-4" />}
|
||||
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
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setActivePhoto((i) => (i + 1) % photos.length)
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
|
||||
>
|
||||
<FaChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronRight className="h-4 w-4" />}
|
||||
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>
|
||||
|
|
@ -243,10 +248,12 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
{photos.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
{photos.map((photo, idx) => (
|
||||
<button
|
||||
<Button
|
||||
key={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
|
||||
? 'border-green-500 opacity-100'
|
||||
: 'border-transparent opacity-60 hover:opacity-90'
|
||||
|
|
@ -257,7 +264,7 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
alt={`Thumbnail ${idx + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -331,22 +338,26 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
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"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmitComment}
|
||||
disabled={!commentText.trim() || submitting}
|
||||
className="p-2.5 bg-green-500 hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-colors flex-shrink-0"
|
||||
>
|
||||
<FaPaperPlane className="w-4 h-4" />
|
||||
</button>
|
||||
variant="solid"
|
||||
color="green-500"
|
||||
shape="none"
|
||||
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>
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
|
||||
{/* Lightbox */}
|
||||
|
|
@ -362,26 +373,29 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
onClick={closeLightbox}
|
||||
/>
|
||||
<div className="fixed inset-0 z-[1110] flex items-center justify-center">
|
||||
<button
|
||||
<Button
|
||||
onClick={closeLightbox}
|
||||
className="absolute top-4 right-4 p-2 text-white hover:text-gray-300 transition-colors z-10"
|
||||
>
|
||||
<FaTimes className="w-8 h-8" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaTimes className="h-8 w-8" />}
|
||||
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 && (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
onClick={prevLightbox}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<button
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronLeft className="h-6 w-6" />}
|
||||
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
|
||||
onClick={nextLightbox}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-colors z-10"
|
||||
>
|
||||
<FaChevronRight className="w-6 h-6" />
|
||||
</button>
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
icon={<FaChevronRight className="h-6 w-6" />}
|
||||
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
|
||||
|
|
@ -397,13 +411,15 @@ function EventModalContent({ event, onClose }: EventModalProps) {
|
|||
{photos.length > 1 && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
{photos.map((_, idx) => (
|
||||
<button
|
||||
<Button
|
||||
key={idx}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
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'
|
||||
}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { DocumentDto } from '@/proxy/intranet/models'
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { getFileIcon, getFileType } from '@/proxy/intranet/utils'
|
||||
import { FILE_URL } from '@/constants/app.constant'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
|
@ -15,7 +16,7 @@ const formatFileSize = (bytes: number): string => {
|
|||
}
|
||||
|
||||
const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents }) => {
|
||||
const { translate } = useLocalization();
|
||||
const { translate } = useLocalization()
|
||||
return (
|
||||
<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">
|
||||
|
|
@ -51,12 +52,14 @@ const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents })
|
|||
{doc.isReadOnly && (
|
||||
<>
|
||||
<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>
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const url = FILE_URL(doc.path, doc.tenantId)
|
||||
|
|
@ -69,11 +72,15 @@ const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents })
|
|||
link.click()
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import { Button, Dialog } from '@/components/ui'
|
||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||
import { SurveyAnswerDto, SurveyDto, SurveyQuestionDto } from '@/proxy/intranet/models'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
|
@ -95,18 +95,20 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
|||
</label>
|
||||
<div className="flex gap-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
<Button
|
||||
key={star}
|
||||
type="button"
|
||||
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
|
||||
? 'text-yellow-400'
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-yellow-300'
|
||||
}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{hasError && (
|
||||
|
|
@ -273,21 +275,24 @@ function SurveyModalContent({ survey, onClose, onSubmit }: SurveyModalProps) {
|
|||
</Dialog.Body>
|
||||
|
||||
<Dialog.Footer className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
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
|
||||
? translate('::App.Platform.Intranet.SurveyModal.Update')
|
||||
: translate('::App.Platform.Intranet.SurveyModal.Submit')}
|
||||
</button>
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { SurveyDto } from '@/proxy/intranet/models'
|
|||
import useLocale from '@/utils/hooks/useLocale'
|
||||
import { currentLocalDate } from '@/utils/dateUtils'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface SurveysProps {
|
||||
surveys?: SurveyDto[]
|
||||
|
|
@ -43,11 +44,13 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
|||
}`}
|
||||
>
|
||||
{/* Background gradient on hover */}
|
||||
<div className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none ${
|
||||
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-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20'
|
||||
}`}></div>
|
||||
<div
|
||||
className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none ${
|
||||
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-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20'
|
||||
}`}
|
||||
></div>
|
||||
|
||||
<div className="relative">
|
||||
{/* 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">
|
||||
{isCompleted && (
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
<h4 className={`text-base font-semibold transition-colors ${
|
||||
isCompleted
|
||||
? 'text-green-800 dark:text-green-300 group-hover:text-green-700 dark:group-hover:text-green-200'
|
||||
: 'text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300'
|
||||
}`}>
|
||||
<h4
|
||||
className={`text-base font-semibold transition-colors ${
|
||||
isCompleted
|
||||
? 'text-green-800 dark:text-green-300 group-hover:text-green-700 dark:group-hover:text-green-200'
|
||||
: 'text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300'
|
||||
}`}
|
||||
>
|
||||
{survey.title}
|
||||
</h4>
|
||||
</div>
|
||||
|
|
@ -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'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
|
||||
|
|
@ -88,7 +103,9 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
|||
<FaQuestionCircle className="w-3 h-3 text-blue-600 dark:text-blue-300" />
|
||||
</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">
|
||||
{survey.questions.length}
|
||||
</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" />
|
||||
</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">
|
||||
{survey.responses}
|
||||
</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" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -121,7 +142,9 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
|||
{/* Progress Bar */}
|
||||
<div className="mb-4">
|
||||
<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">
|
||||
{Math.round((survey.responses / 100) * 100)}%
|
||||
</span>
|
||||
|
|
@ -141,16 +164,21 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
|
|||
</p>
|
||||
|
||||
{/* 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 ${
|
||||
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-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'
|
||||
}`}>
|
||||
<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
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
{isCompleted
|
||||
? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses')
|
||||
: translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
|
||||
<FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ReactElement, useRef, useState } from 'react'
|
||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
import {
|
||||
hideImageHoverPreview,
|
||||
openImageInNewTab,
|
||||
|
|
@ -119,10 +120,13 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
|||
cursor: 'zoom-in',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
title="Sil"
|
||||
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={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
|
|
@ -141,7 +145,7 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
|||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
|
@ -153,11 +157,14 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
|||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
title="Görsel Yükle"
|
||||
disabled={uploading}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
variant="solid"
|
||||
shape="none"
|
||||
className="!h-auto !rounded !px-2.5 !py-1 !text-[13px]"
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: uploading ? '#aaa' : '#0078d4',
|
||||
|
|
@ -176,7 +183,7 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
|||
) : (
|
||||
<FaUpload style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ReactElement } from 'react'
|
||||
import { Button } from '@/components/ui'
|
||||
import {
|
||||
hideImageHoverPreview,
|
||||
openImageInNewTab,
|
||||
|
|
@ -156,11 +157,14 @@ const ImageViewerEditorComponent = (templateData: any): ReactElement => {
|
|||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
||||
{urls.map((url, index) => (
|
||||
<button
|
||||
<Button
|
||||
key={`${url}-${index}`}
|
||||
type="button"
|
||||
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
|
||||
src={url}
|
||||
|
|
@ -180,7 +184,7 @@ const ImageViewerEditorComponent = (templateData: any): ReactElement => {
|
|||
currentTarget.src = '/img/others/no-image.png'
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -149,35 +149,61 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
|||
>
|
||||
{isDesignMode && (
|
||||
<div className="flex gap-2 items-center mr-2">
|
||||
<button onClick={openCreateModal} title="New Item">
|
||||
<FaPlus size={16} className="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300" />
|
||||
</button>
|
||||
<button onClick={handleDelete} title="Delete Item">
|
||||
<FaTrashAlt size={16} className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300" />
|
||||
</button>
|
||||
<Button
|
||||
onClick={openCreateModal}
|
||||
title="New Item"
|
||||
size="xs"
|
||||
variant="plain"
|
||||
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 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">
|
||||
{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" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={openEditModal}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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'}
|
||||
${isDesignMode ? 'hover:text-blue-600 dark:hover:text-blue-400' : ''}
|
||||
`}
|
||||
>
|
||||
{translate('::' + item.displayName)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
|
|
@ -221,7 +247,9 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
|||
}
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
{modalMode === 'edit' ? translate('::KayitGuncellendi') : translate('::KayitEklendi')}
|
||||
{modalMode === 'edit'
|
||||
? translate('::KayitGuncellendi')
|
||||
: translate('::KayitEklendi')}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
|
|
@ -271,11 +299,21 @@ export const MenuItemComponent: React.FC<MenuItemComponentProps> = ({
|
|||
</FormItem>
|
||||
|
||||
<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 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 label="Parent Code" className="mb-2">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Container } from '@/components/shared'
|
|||
import { Helmet } from 'react-helmet'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
export const MenuManager = () => {
|
||||
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>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6">{error}</p>
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -94,8 +98,12 @@ export const MenuManager = () => {
|
|||
{/* Sol kısım: Başlık */}
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">({menuItems.length} root items)</span>
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
Menu Manager
|
||||
</h2>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({menuItems.length} root items)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Sağ kısım: Design Mode + Save butonu */}
|
||||
|
|
@ -106,29 +114,33 @@ export const MenuManager = () => {
|
|||
>
|
||||
Design Mode
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleToggleDesignMode}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
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
|
||||
${isDesignMode ? 'bg-blue-600 dark:bg-blue-700' : 'bg-gray-200 dark:bg-gray-700'}
|
||||
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 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
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white dark:bg-gray-200 transition-transform duration-200 ease-in-out
|
||||
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!isDesignMode || isSaving}
|
||||
size="sm"
|
||||
variant="plain"
|
||||
className={`
|
||||
flex items-center gap-2 px-2 py-1 rounded-lg 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'}
|
||||
!inline-flex !h-auto !items-center !justify-center gap-2 !rounded-lg !px-2 !py-1 transition-colors
|
||||
${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' : ''}
|
||||
`}
|
||||
>
|
||||
|
|
@ -143,7 +155,7 @@ export const MenuManager = () => {
|
|||
Save Changes
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { useLocalization } from "@/utils/hooks/useLocalization";
|
||||
import { createDemoAsync } from "@/services/demo.service";
|
||||
import { DemoDto } from "@/proxy/demo/models";
|
||||
import { Button } from "@/components/ui";
|
||||
|
||||
interface DemoModalProps {
|
||||
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="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 */}
|
||||
<button
|
||||
<Button
|
||||
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">
|
||||
<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">
|
||||
{translate('::Public.demo.resultMessage')}
|
||||
</p>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSubmitted(false);
|
||||
setFormData({
|
||||
|
|
@ -143,10 +146,12 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
|||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -160,14 +165,16 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
|||
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="mx-auto bg-white rounded-xl shadow-lg max-w-2xl w-full relative dark:bg-gray-900">
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
title="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
|
||||
onSubmit={handleSubmit}
|
||||
|
|
@ -364,13 +371,14 @@ const Demo: React.FC<DemoModalProps> = ({ isOpen, onClose }) => {
|
|||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
<Button
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import SelectableBlock from './designer/SelectableBlock'
|
|||
import { DesignerSelection } from './designer/types'
|
||||
import { useDesignerState } from './designer/useDesignerState'
|
||||
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
||||
import { Notification, toast } from '@/components/ui'
|
||||
import { Button, Notification, toast } from '@/components/ui'
|
||||
|
||||
interface HomeSlideServiceContent {
|
||||
icon: string
|
||||
|
|
@ -926,13 +926,14 @@ const Home: React.FC = () => {
|
|||
)}
|
||||
|
||||
{isDesignMode && !isPanelVisible && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Hero Carousel */}
|
||||
|
|
@ -1030,29 +1031,33 @@ const Home: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<button
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaChevronLeft size={24} />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<FaChevronRight size={24} />
|
||||
</button>
|
||||
/>
|
||||
|
||||
{/* Slide Indicators */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-3 z-10">
|
||||
{(content?.slides || []).map((_, index) => (
|
||||
<button
|
||||
<Button
|
||||
key={index}
|
||||
onClick={() => setCurrentSlide(index)}
|
||||
className={`w-3 h-3 rounded-full transition-all ${
|
||||
index === currentSlide ? 'bg-white w-8' : 'bg-white/50 hover:bg-white/70'
|
||||
variant="plain"
|
||||
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}`}
|
||||
/>
|
||||
|
|
@ -1062,19 +1067,20 @@ const Home: React.FC = () => {
|
|||
{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">
|
||||
{(content?.slides || []).map((_, index) => (
|
||||
<button
|
||||
<Button
|
||||
key={`editor-slide-${index}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentSlide(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'
|
||||
}`}
|
||||
>
|
||||
Slide {index + 1}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,12 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
|||
|
||||
return (
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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'}
|
||||
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
|
||||
className={`text-gray-400 text-xs transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{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">
|
||||
|
|
@ -84,7 +86,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
|||
</div>
|
||||
<div className="grid grid-cols-10 gap-0.5 p-2 max-h-[208px] overflow-y-auto">
|
||||
{displayed.map(([key, Icon]) => (
|
||||
<button
|
||||
<Button
|
||||
key={key}
|
||||
type="button"
|
||||
title={key}
|
||||
|
|
@ -93,11 +95,13 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
|||
setOpen(false)
|
||||
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'}`}
|
||||
>
|
||||
<Icon />
|
||||
</button>
|
||||
<Icon className="h-5 w-5" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{hasMore && (
|
||||
|
|
@ -105,13 +109,16 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
|||
<span className="text-xs text-gray-400">
|
||||
{displayed.length} / {filtered.length}
|
||||
</span>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -265,10 +272,14 @@ export function MenuAddDialog({
|
|||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className={labelCls}>
|
||||
{translate('::App.Platform.Code') || 'Code'}{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
{translate('::App.Platform.Code') || 'Code'} <span className="text-red-500">*</span>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue