<button kaldırıldı <Button olarak değiştirildi.

This commit is contained in:
Sedat Öztürk 2026-06-26 01:55:44 +03:00
parent 84db5c672e
commit 5110349a6b
79 changed files with 2975 additions and 1944 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
>
&times;
</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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" />
)}

View file

@ -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" />
)}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'
}`}
/>

View file

@ -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'
}`}
/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
>
&times;
</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"
>
&times;
</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>

View file

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

View file

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