About ve Services Designer komponenti tasarlandı
This commit is contained in:
parent
5cc3dc5ab3
commit
4304229079
15 changed files with 2008 additions and 576 deletions
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sozsoft.Platform.Public;
|
||||
|
||||
public class SaveAboutPageInput
|
||||
{
|
||||
public string CultureName { get; set; }
|
||||
public string HeroTitleKey { get; set; }
|
||||
public string HeroTitleValue { get; set; }
|
||||
public string HeroSubtitleKey { get; set; }
|
||||
public string HeroSubtitleValue { get; set; }
|
||||
public string HeroImageKey { get; set; }
|
||||
public string HeroImageValue { get; set; }
|
||||
public List<SaveAboutStatInput> Stats { get; set; } = [];
|
||||
public List<SaveLocalizedTextInput> Descriptions { get; set; } = [];
|
||||
public List<SaveAboutSectionInput> Sections { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SaveAboutStatInput
|
||||
{
|
||||
public string Icon { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string LabelKey { get; set; }
|
||||
public string LabelValue { get; set; }
|
||||
public bool? UseCounter { get; set; }
|
||||
public string? CounterEnd { get; set; }
|
||||
public string CounterSuffix { get; set; }
|
||||
public int? CounterDuration { get; set; }
|
||||
}
|
||||
|
||||
public class SaveAboutSectionInput
|
||||
{
|
||||
public string TitleKey { get; set; }
|
||||
public string TitleValue { get; set; }
|
||||
public string DescriptionKey { get; set; }
|
||||
public string DescriptionValue { get; set; }
|
||||
}
|
||||
|
||||
public class SaveServicesPageInput
|
||||
{
|
||||
public string CultureName { get; set; }
|
||||
public string HeroTitleKey { get; set; }
|
||||
public string HeroTitleValue { get; set; }
|
||||
public string HeroSubtitleKey { get; set; }
|
||||
public string HeroSubtitleValue { get; set; }
|
||||
public string HeroImageKey { get; set; }
|
||||
public string HeroImageValue { get; set; }
|
||||
public string SupportTitleKey { get; set; }
|
||||
public string SupportTitleValue { get; set; }
|
||||
public string SupportButtonLabelKey { get; set; }
|
||||
public string SupportButtonLabelValue { get; set; }
|
||||
public string CtaTitleKey { get; set; }
|
||||
public string CtaTitleValue { get; set; }
|
||||
public string CtaDescriptionKey { get; set; }
|
||||
public string CtaDescriptionValue { get; set; }
|
||||
public string CtaButtonLabelKey { get; set; }
|
||||
public string CtaButtonLabelValue { get; set; }
|
||||
public List<SaveServiceItemInput> ServiceItems { get; set; } = [];
|
||||
public List<SaveServiceItemInput> SupportItems { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SaveServiceItemInput
|
||||
{
|
||||
public string? Icon { get; set; }
|
||||
public string TitleKey { get; set; }
|
||||
public string TitleValue { get; set; }
|
||||
public string? DescriptionKey { get; set; }
|
||||
public string? DescriptionValue { get; set; }
|
||||
public string Type { get; set; }
|
||||
public List<SaveLocalizedTextInput> Features { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SaveLocalizedTextInput
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ using System.Linq;
|
|||
using Volo.Abp.Application.Dtos;
|
||||
using System.Text.Json;
|
||||
using Volo.Abp.Identity;
|
||||
using Sozsoft.Languages;
|
||||
using Sozsoft.Languages.Entities;
|
||||
|
||||
namespace Sozsoft.Platform.Public;
|
||||
|
||||
|
|
@ -31,6 +33,9 @@ public class PublicAppService : PlatformAppService
|
|||
private readonly IRepository<About, Guid> _aboutRepository;
|
||||
private readonly IRepository<Contact, Guid> _contactRepository;
|
||||
private readonly IIdentityUserRepository _identityUserRepository;
|
||||
private readonly IRepository<LanguageKey, Guid> _languageKeyRepository;
|
||||
private readonly IRepository<LanguageText, Guid> _languageTextRepository;
|
||||
private readonly LanguageTextAppService _languageTextAppService;
|
||||
|
||||
public PublicAppService(
|
||||
IRepository<Service, Guid> serviceRepository,
|
||||
|
|
@ -45,7 +50,10 @@ public class PublicAppService : PlatformAppService
|
|||
IRepository<Order, Guid> orderRepository,
|
||||
IRepository<About, Guid> aboutRepository,
|
||||
IRepository<Contact, Guid> contactRepository,
|
||||
IIdentityUserRepository identityUserRepository
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IRepository<LanguageKey, Guid> languageKeyRepository,
|
||||
IRepository<LanguageText, Guid> languageTextRepository,
|
||||
LanguageTextAppService languageTextAppService
|
||||
)
|
||||
{
|
||||
_serviceRepository = serviceRepository;
|
||||
|
|
@ -61,15 +69,114 @@ public class PublicAppService : PlatformAppService
|
|||
_aboutRepository = aboutRepository;
|
||||
_contactRepository = contactRepository;
|
||||
_identityUserRepository = identityUserRepository;
|
||||
_languageKeyRepository = languageKeyRepository;
|
||||
_languageTextRepository = languageTextRepository;
|
||||
_languageTextAppService = languageTextAppService;
|
||||
}
|
||||
|
||||
public async Task<List<ServiceDto>> GetServicesListAsync()
|
||||
{
|
||||
var entity = await _serviceRepository.GetListAsync();
|
||||
var queryable = await _serviceRepository.GetQueryableAsync();
|
||||
var entity = await AsyncExecuter.ToListAsync(queryable.OrderBy(a => a.CreationTime));
|
||||
|
||||
return ObjectMapper.Map<List<Service>, List<ServiceDto>>(entity);
|
||||
}
|
||||
|
||||
public async Task SaveAboutPageAsync(SaveAboutPageInput input)
|
||||
{
|
||||
var entity = await _aboutRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(About));
|
||||
|
||||
entity.StatsJson = JsonSerializer.Serialize(input.Stats.Select(stat => new StatDto
|
||||
{
|
||||
Icon = stat.Icon,
|
||||
Value = stat.Value,
|
||||
LabelKey = stat.LabelKey,
|
||||
UseCounter = stat.UseCounter,
|
||||
CounterEnd = stat.CounterEnd,
|
||||
CounterSuffix = stat.CounterSuffix,
|
||||
CounterDuration = stat.CounterDuration,
|
||||
}).ToList());
|
||||
|
||||
entity.DescriptionsJson = JsonSerializer.Serialize(input.Descriptions.Select(item => item.Key).ToList());
|
||||
|
||||
entity.SectionsJson = JsonSerializer.Serialize(input.Sections.Select(section => new SectionDto
|
||||
{
|
||||
Key = section.TitleKey,
|
||||
DescKey = section.DescriptionKey,
|
||||
}).ToList());
|
||||
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.HeroTitleKey, input.HeroTitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.HeroSubtitleKey, input.HeroSubtitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.HeroImageKey, input.HeroImageValue);
|
||||
|
||||
foreach (var stat in input.Stats)
|
||||
{
|
||||
await UpsertLanguageTextAsync(input.CultureName, stat.LabelKey, stat.LabelValue);
|
||||
}
|
||||
|
||||
foreach (var description in input.Descriptions)
|
||||
{
|
||||
await UpsertLanguageTextAsync(input.CultureName, description.Key, description.Value);
|
||||
}
|
||||
|
||||
foreach (var section in input.Sections)
|
||||
{
|
||||
await UpsertLanguageTextAsync(input.CultureName, section.TitleKey, section.TitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, section.DescriptionKey, section.DescriptionValue);
|
||||
}
|
||||
|
||||
await _aboutRepository.UpdateAsync(entity, autoSave: true);
|
||||
await _languageTextAppService.ClearRedisCacheAsync();
|
||||
}
|
||||
|
||||
public async Task SaveServicesPageAsync(SaveServicesPageInput input)
|
||||
{
|
||||
var existingEntities = await _serviceRepository.GetListAsync();
|
||||
|
||||
foreach (var entity in existingEntities)
|
||||
{
|
||||
await _serviceRepository.DeleteAsync(entity, autoSave: false);
|
||||
}
|
||||
|
||||
foreach (var item in input.ServiceItems.Concat(input.SupportItems))
|
||||
{
|
||||
var entity = new Service
|
||||
{
|
||||
Icon = item.Icon,
|
||||
Title = item.TitleKey,
|
||||
Description = item.DescriptionKey,
|
||||
Type = item.Type,
|
||||
Features = item.Features.Select(feature => feature.Key).ToArray(),
|
||||
};
|
||||
|
||||
await _serviceRepository.InsertAsync(entity, autoSave: false);
|
||||
|
||||
await UpsertLanguageTextAsync(input.CultureName, item.TitleKey, item.TitleValue);
|
||||
|
||||
if (!item.DescriptionKey.IsNullOrWhiteSpace())
|
||||
{
|
||||
await UpsertLanguageTextAsync(input.CultureName, item.DescriptionKey!, item.DescriptionValue ?? string.Empty);
|
||||
}
|
||||
|
||||
foreach (var feature in item.Features)
|
||||
{
|
||||
await UpsertLanguageTextAsync(input.CultureName, feature.Key, feature.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.HeroTitleKey, input.HeroTitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.HeroSubtitleKey, input.HeroSubtitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.HeroImageKey, input.HeroImageValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.SupportTitleKey, input.SupportTitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.SupportButtonLabelKey, input.SupportButtonLabelValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.CtaTitleKey, input.CtaTitleValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.CtaDescriptionKey, input.CtaDescriptionValue);
|
||||
await UpsertLanguageTextAsync(input.CultureName, input.CtaButtonLabelKey, input.CtaButtonLabelValue);
|
||||
|
||||
await CurrentUnitOfWork!.SaveChangesAsync();
|
||||
await _languageTextAppService.ClearRedisCacheAsync();
|
||||
}
|
||||
|
||||
public async Task CreateDemoAsync(DemoDto input)
|
||||
{
|
||||
var demo = ObjectMapper.Map<DemoDto, Demo>(input);
|
||||
|
|
@ -298,6 +405,58 @@ public class PublicAppService : PlatformAppService
|
|||
|
||||
return ObjectMapper.Map<Contact, ContactDto>(entity);
|
||||
}
|
||||
|
||||
private async Task UpsertLanguageTextAsync(string cultureName, string key, string value)
|
||||
{
|
||||
if (key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedCultureName = NormalizeCultureName(cultureName);
|
||||
var resourceName = PlatformConsts.AppName;
|
||||
|
||||
var languageKey = await _languageKeyRepository.FirstOrDefaultAsync(a => a.ResourceName == resourceName && a.Key == key);
|
||||
if (languageKey == null)
|
||||
{
|
||||
languageKey = await _languageKeyRepository.InsertAsync(new LanguageKey
|
||||
{
|
||||
ResourceName = resourceName,
|
||||
Key = key,
|
||||
}, autoSave: false);
|
||||
}
|
||||
|
||||
var languageText = await _languageTextRepository.FirstOrDefaultAsync(a =>
|
||||
a.ResourceName == resourceName &&
|
||||
a.Key == languageKey.Key &&
|
||||
a.CultureName == normalizedCultureName);
|
||||
|
||||
if (languageText == null)
|
||||
{
|
||||
await _languageTextRepository.InsertAsync(new LanguageText
|
||||
{
|
||||
ResourceName = resourceName,
|
||||
Key = languageKey.Key,
|
||||
CultureName = normalizedCultureName,
|
||||
Value = value ?? string.Empty,
|
||||
}, autoSave: false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
languageText.Value = value ?? string.Empty;
|
||||
await _languageTextRepository.UpdateAsync(languageText, autoSave: false);
|
||||
}
|
||||
|
||||
private static string NormalizeCultureName(string cultureName)
|
||||
{
|
||||
if (cultureName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return PlatformConsts.DefaultLanguage;
|
||||
}
|
||||
|
||||
return cultureName.Split('-')[0].ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7910,7 +7910,7 @@
|
|||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.services.support.sms.features.Api",
|
||||
"key": "Public.services.support.sms.features.api",
|
||||
"tr": "Api Desteği",
|
||||
"en": "Api Support"
|
||||
},
|
||||
|
|
@ -8058,6 +8058,84 @@
|
|||
"tr": "Müşteri Yorumları",
|
||||
"en": "Testimonials"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.title",
|
||||
"tr": "Tasarımcı",
|
||||
"en": "Designer"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.ikonAnahtari",
|
||||
"tr": "İkon Anahtarı",
|
||||
"en": "Icon Key"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.showPanel",
|
||||
"tr": "Paneli Göster",
|
||||
"en": "Show Panel"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.noSelection",
|
||||
"tr": "Bir alan seçin",
|
||||
"en": "Select a field"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.value",
|
||||
"tr": "Değer",
|
||||
"en": "Value"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.desc1",
|
||||
"tr": "İkon, Başlık, Açıklama ve Özellik listesini düzenleyin.",
|
||||
"en": "Edit the icon, title, description, and feature list."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.desc2",
|
||||
"tr": "Alt CTA metinlerini ve buton etiketini duzenleyin.",
|
||||
"en": "Edit the sub-CTA texts and button label."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.desc3",
|
||||
"tr": "Destek alanı başlığını ve buton metnini düzenleyin.",
|
||||
"en": "Edit the support area title and button text."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.desc4",
|
||||
"tr": "Başlık, alt başlık ve arka plan görselini düzenleyin.",
|
||||
"en": "Edit the title, subtitle, and background image."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.selectField",
|
||||
"tr": "Sayfa üzerinden bir blok seçerek özelliklerini buradan düzenleyin.",
|
||||
"en": "Select a block on the page to edit its properties here."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.close",
|
||||
"tr": "Gizle",
|
||||
"en": "Hide"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.noSelectionDetails",
|
||||
"tr": "Seçili bölümün düzenlenebilir alanları burada gösterilir.",
|
||||
"en": "Select a block on the page to edit its properties here."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Public.designer.save",
|
||||
"tr": "Kaydet",
|
||||
"en": "Save"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.Component.Description",
|
||||
|
|
|
|||
|
|
@ -5613,325 +5613,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region About Us
|
||||
listFormName = AppCodes.About;
|
||||
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||
{
|
||||
var listForm = await _listFormRepository.InsertAsync(
|
||||
new ListForm
|
||||
{
|
||||
ListFormType = ListFormTypeEnum.List,
|
||||
PageSize = 10,
|
||||
ExportJson = DefaultExportJson,
|
||||
IsSubForm = false,
|
||||
ShowNote = true,
|
||||
LayoutJson = DefaultLayoutJson(),
|
||||
CultureName = LanguageCodes.En,
|
||||
ListFormCode = listFormName,
|
||||
Name = listFormName,
|
||||
Title = listFormName,
|
||||
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||
IsTenant = false,
|
||||
IsBranch = false,
|
||||
IsOrganizationUnit = false,
|
||||
Description = listFormName,
|
||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.About)),
|
||||
KeyFieldName = "Id",
|
||||
DefaultFilter = DefaultFilterJson,
|
||||
KeyFieldDbSourceType = DbType.Guid,
|
||||
SortMode = GridOptions.SortModeSingle,
|
||||
FilterRowJson = DefaultFilterRowJson,
|
||||
HeaderFilterJson = DefaultHeaderFilterJson,
|
||||
SearchPanelJson = DefaultSearchPanelJson,
|
||||
GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }),
|
||||
SelectionJson = DefaultSelectionSingleJson,
|
||||
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||
PermissionJson = DefaultPermissionJson(listFormName),
|
||||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 800, 720, true, true, true, true, false),
|
||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
|
||||
{
|
||||
new() {
|
||||
Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group", Items =
|
||||
[
|
||||
new EditingFormItemDto { Order = 1, DataField = "StatsJson", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxTextArea, EditorOptions="{\"height\":200}" },
|
||||
new EditingFormItemDto { Order = 2, DataField = "DescriptionsJson", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxTextArea, EditorOptions="{\"height\":200}" },
|
||||
new EditingFormItemDto { Order = 3, DataField = "SectionsJson", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxTextArea, EditorOptions="{\"height\":200}" },
|
||||
]
|
||||
}
|
||||
}),
|
||||
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.About)),
|
||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
||||
});
|
||||
|
||||
#region About Fields
|
||||
await _listFormFieldRepository.InsertManyAsync([
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.Guid,
|
||||
FieldName = "Id",
|
||||
CaptionName = "App.Listform.ListformField.Id",
|
||||
Width = 100,
|
||||
ListOrderNo = 1,
|
||||
Visible = false,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "StatsJson",
|
||||
CaptionName = "App.Listform.ListformField.StatsJson",
|
||||
Width = 700,
|
||||
ListOrderNo = 2,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = true,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "DescriptionsJson",
|
||||
CaptionName = "App.Listform.ListformField.DescriptionsJson",
|
||||
Width = 400,
|
||||
ListOrderNo = 3,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = true,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "SectionsJson",
|
||||
CaptionName = "App.Listform.ListformField.SectionsJson",
|
||||
Width = 400,
|
||||
ListOrderNo = 4,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = true,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
]);
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Services
|
||||
listFormName = AppCodes.Services;
|
||||
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||
{
|
||||
var listForm = await _listFormRepository.InsertAsync(
|
||||
new ListForm
|
||||
{
|
||||
ListFormType = ListFormTypeEnum.List,
|
||||
PageSize = 10,
|
||||
ExportJson = DefaultExportJson,
|
||||
IsSubForm = false,
|
||||
ShowNote = true,
|
||||
LayoutJson = DefaultLayoutJson(),
|
||||
CultureName = LanguageCodes.En,
|
||||
ListFormCode = listFormName,
|
||||
Name = listFormName,
|
||||
Title = listFormName,
|
||||
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||
IsTenant = false,
|
||||
IsBranch = false,
|
||||
IsOrganizationUnit = false,
|
||||
Description = listFormName,
|
||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Service)),
|
||||
KeyFieldName = "Id",
|
||||
DefaultFilter = DefaultFilterJson,
|
||||
KeyFieldDbSourceType = DbType.Guid,
|
||||
SortMode = GridOptions.SortModeSingle,
|
||||
FilterRowJson = DefaultFilterRowJson,
|
||||
HeaderFilterJson = DefaultHeaderFilterJson,
|
||||
SearchPanelJson = DefaultSearchPanelJson,
|
||||
GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }),
|
||||
SelectionJson = DefaultSelectionSingleJson,
|
||||
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||
PermissionJson = DefaultPermissionJson(listFormName),
|
||||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 400, true, true, true, true, false),
|
||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
|
||||
{
|
||||
new() { Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group", Items =
|
||||
[
|
||||
new EditingFormItemDto { Order = 1, DataField = "Title", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxAutocomplete, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 2, DataField = "Icon", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
|
||||
new EditingFormItemDto { Order = 3, DataField = "Description", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxAutocomplete, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 4, DataField = "Type", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
|
||||
new EditingFormItemDto { Order = 5, DataField = "Features", ColSpan = 1, EditorType2 = EditorTypes.dxTextArea },
|
||||
]
|
||||
}
|
||||
}),
|
||||
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Service)),
|
||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
||||
});
|
||||
|
||||
#region Services Fields
|
||||
await _listFormFieldRepository.InsertManyAsync([
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.Guid,
|
||||
FieldName = "Id",
|
||||
CaptionName = "App.Listform.ListformField.Id",
|
||||
Width = 100,
|
||||
ListOrderNo = 1,
|
||||
Visible = false,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Title",
|
||||
CaptionName = "App.Listform.ListformField.Title",
|
||||
Width = 400,
|
||||
ListOrderNo = 2,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = false,
|
||||
LookupJson = JsonSerializer.Serialize(new LookupDto {
|
||||
DataSourceType = UiLookupDataSourceTypeEnum.Query,
|
||||
DisplayExpr = "Name",
|
||||
ValueExpr = "Key",
|
||||
LookupQuery = LookupQueryValues.LanguageKeyValues
|
||||
}),
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Icon",
|
||||
CaptionName = "App.Listform.ListformField.Icon",
|
||||
Width = 100,
|
||||
ListOrderNo = 3,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = true,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Description",
|
||||
CaptionName = "App.Listform.ListformField.Description",
|
||||
Width = 500,
|
||||
ListOrderNo = 4,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = false,
|
||||
LookupJson = JsonSerializer.Serialize(new LookupDto {
|
||||
DataSourceType = UiLookupDataSourceTypeEnum.Query,
|
||||
DisplayExpr = "Name",
|
||||
ValueExpr = "Key",
|
||||
LookupQuery = LookupQueryValues.LanguageKeyValues
|
||||
}),
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Type",
|
||||
CaptionName = "App.Listform.ListformField.Type",
|
||||
Width = 100,
|
||||
ListOrderNo = 5,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = true,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
LookupJson = JsonSerializer.Serialize(new LookupDto
|
||||
{
|
||||
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
|
||||
DisplayExpr = "name",
|
||||
ValueExpr = "key",
|
||||
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
|
||||
new () { Key="service", Name="Service" },
|
||||
new () { Key="support", Name="Support" },
|
||||
}),
|
||||
}),
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
},
|
||||
new()
|
||||
{
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Features",
|
||||
CaptionName = "App.Listform.ListformField.Features",
|
||||
Width = 500,
|
||||
ListOrderNo = 6,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
IsDeleted = false,
|
||||
AllowSearch = true,
|
||||
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||
}
|
||||
]);
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Products
|
||||
listFormName = AppCodes.Orders.Products;
|
||||
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@
|
|||
"routeType": "public",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "about",
|
||||
"path": "/about",
|
||||
"componentPath": "@/views/public/About",
|
||||
"routeType": "public",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "products",
|
||||
"path": "/products",
|
||||
|
|
@ -363,6 +370,20 @@
|
|||
"componentPath": "@/views/report/DevexpressReportDesigner",
|
||||
"routeType": "protected",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "servicesDesigner",
|
||||
"path": "/admin/public/services/designer",
|
||||
"componentPath": "@/views/public/Services",
|
||||
"routeType": "protected",
|
||||
"authority": ["App.Services"]
|
||||
},
|
||||
{
|
||||
"key": "aboutDesigner",
|
||||
"path": "/admin/public/about/designer",
|
||||
"componentPath": "@/views/public/About",
|
||||
"routeType": "protected",
|
||||
"authority": ["App.About"]
|
||||
}
|
||||
],
|
||||
"Menus": [
|
||||
|
|
@ -623,7 +644,7 @@
|
|||
"Code": "App.About",
|
||||
"DisplayName": "App.About",
|
||||
"Order": 1,
|
||||
"Url": "/admin/list/App.About",
|
||||
"Url": "/admin/public/about/designer",
|
||||
"Icon": "FcAbout",
|
||||
"RequiredPermissionName": "App.About",
|
||||
"IsDisabled": false
|
||||
|
|
@ -633,7 +654,7 @@
|
|||
"Code": "App.Services",
|
||||
"DisplayName": "App.Services",
|
||||
"Order": 2,
|
||||
"Url": "/admin/list/App.Services",
|
||||
"Url": "/admin/public/services/designer",
|
||||
"Icon": "FcServices",
|
||||
"RequiredPermissionName": "App.Services",
|
||||
"IsDisabled": false
|
||||
|
|
|
|||
|
|
@ -1560,60 +1560,6 @@
|
|||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Create",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Delete",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Export",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Import",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Note",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Note",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Update",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services",
|
||||
|
|
@ -1623,60 +1569,7 @@
|
|||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Create",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Delete",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Export",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Import",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Note",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Note",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Update",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Erp|Kurs"
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products",
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ export const ROUTES_ENUM = {
|
|||
public: {
|
||||
home: '/home',
|
||||
about: '/about',
|
||||
aboutDesigner: '/about/designer',
|
||||
products: '/products',
|
||||
checkout: '/checkout',
|
||||
payment: '/payment',
|
||||
success: '/success',
|
||||
services: '/services',
|
||||
servicesDesigner: '/services/designer',
|
||||
blog: '/blog',
|
||||
blogDetail: '/blog/:id',
|
||||
demo: '/demo',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,42 @@
|
|||
import { AboutDto } from '@/proxy/about/models'
|
||||
import apiService from './api.service'
|
||||
|
||||
export interface SaveLocalizedTextInput {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface SaveAboutStatInput {
|
||||
icon: string
|
||||
value: string
|
||||
labelKey: string
|
||||
labelValue: string
|
||||
useCounter?: boolean
|
||||
counterEnd?: string
|
||||
counterSuffix?: string
|
||||
counterDuration?: number
|
||||
}
|
||||
|
||||
export interface SaveAboutSectionInput {
|
||||
titleKey: string
|
||||
titleValue: string
|
||||
descriptionKey: string
|
||||
descriptionValue: string
|
||||
}
|
||||
|
||||
export interface SaveAboutPageInput {
|
||||
cultureName: string
|
||||
heroTitleKey: string
|
||||
heroTitleValue: string
|
||||
heroSubtitleKey: string
|
||||
heroSubtitleValue: string
|
||||
heroImageKey: string
|
||||
heroImageValue: string
|
||||
stats: SaveAboutStatInput[]
|
||||
descriptions: SaveLocalizedTextInput[]
|
||||
sections: SaveAboutSectionInput[]
|
||||
}
|
||||
|
||||
export function getAbout() {
|
||||
return apiService.fetchData<AboutDto>(
|
||||
{
|
||||
|
|
@ -10,3 +46,14 @@ export function getAbout() {
|
|||
{ apiName: 'Default' },
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAboutPage(input: SaveAboutPageInput) {
|
||||
return apiService.fetchData<void, SaveAboutPageInput>(
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/api/app/public/save-about-page',
|
||||
data: input,
|
||||
},
|
||||
{ apiName: 'Default' },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,43 @@
|
|||
import apiService from './api.service'
|
||||
import { ServiceDto } from '@/proxy/services/models'
|
||||
|
||||
export interface SaveLocalizedTextInput {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface SaveServiceItemInput {
|
||||
icon?: string
|
||||
titleKey: string
|
||||
titleValue: string
|
||||
descriptionKey?: string
|
||||
descriptionValue?: string
|
||||
type: 'service' | 'support'
|
||||
features: SaveLocalizedTextInput[]
|
||||
}
|
||||
|
||||
export interface SaveServicesPageInput {
|
||||
cultureName: string
|
||||
heroTitleKey: string
|
||||
heroTitleValue: string
|
||||
heroSubtitleKey: string
|
||||
heroSubtitleValue: string
|
||||
heroImageKey: string
|
||||
heroImageValue: string
|
||||
supportTitleKey: string
|
||||
supportTitleValue: string
|
||||
supportButtonLabelKey: string
|
||||
supportButtonLabelValue: string
|
||||
ctaTitleKey: string
|
||||
ctaTitleValue: string
|
||||
ctaDescriptionKey: string
|
||||
ctaDescriptionValue: string
|
||||
ctaButtonLabelKey: string
|
||||
ctaButtonLabelValue: string
|
||||
serviceItems: SaveServiceItemInput[]
|
||||
supportItems: SaveServiceItemInput[]
|
||||
}
|
||||
|
||||
export function getServices() {
|
||||
return apiService.fetchData<ServiceDto[]>(
|
||||
{
|
||||
|
|
@ -10,3 +47,14 @@ export function getServices() {
|
|||
{ apiName: 'Default' },
|
||||
)
|
||||
}
|
||||
|
||||
export function saveServicesPage(input: SaveServicesPageInput) {
|
||||
return apiService.fetchData<void, SaveServicesPageInput>(
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/api/app/public/save-services-page',
|
||||
data: input,
|
||||
},
|
||||
{ apiName: 'Default' },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,154 @@ import React, { useEffect, useState } from 'react'
|
|||
import { Helmet } from 'react-helmet'
|
||||
import navigationIcon from '@/proxy/menus/navigation-icon.config'
|
||||
import { AboutDto } from '@/proxy/about/models'
|
||||
import { getAbout } from '@/services/about'
|
||||
import { getAbout, saveAboutPage } from '@/services/about'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Loading from '@/components/shared/Loading'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { useStoreState } from '@/store'
|
||||
import { useStoreActions } from '@/store'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import DesignerDrawer from './designer/DesignerDrawer'
|
||||
import SelectableBlock from './designer/SelectableBlock'
|
||||
import { DesignerSelection } from './designer/types'
|
||||
import { useDesignerState } from './designer/useDesignerState'
|
||||
|
||||
interface AboutStatContent {
|
||||
icon: string
|
||||
value: string
|
||||
label: string
|
||||
labelKey: string
|
||||
useCounter?: boolean
|
||||
counterEnd?: string
|
||||
counterSuffix?: string
|
||||
counterDuration?: number
|
||||
}
|
||||
|
||||
interface AboutDescriptionContent {
|
||||
key: string
|
||||
text: string
|
||||
}
|
||||
|
||||
interface AboutSectionContent {
|
||||
title: string
|
||||
description: string
|
||||
titleKey: string
|
||||
descriptionKey: string
|
||||
}
|
||||
|
||||
interface AboutContent {
|
||||
heroTitle: string
|
||||
heroTitleKey: string
|
||||
heroSubtitle: string
|
||||
heroSubtitleKey: string
|
||||
heroImage: string
|
||||
heroImageKey: string
|
||||
stats: AboutStatContent[]
|
||||
descriptions: AboutDescriptionContent[]
|
||||
sections: AboutSectionContent[]
|
||||
}
|
||||
|
||||
const ABOUT_HERO_IMAGE =
|
||||
'https://images.pexels.com/photos/3183183/pexels-photo-3183183.jpeg?auto=compress&cs=tinysrgb&w=1920'
|
||||
const ABOUT_HERO_TITLE_KEY = 'App.About'
|
||||
const ABOUT_HERO_SUBTITLE_KEY = 'Public.about.subtitle'
|
||||
const ABOUT_HERO_IMAGE_KEY = 'Public.about.heroImage'
|
||||
|
||||
function isLikelyLocalizationKey(value?: string) {
|
||||
return Boolean(value && /^[A-Za-z0-9_.-]+$/.test(value) && value.includes('.'))
|
||||
}
|
||||
|
||||
function resolveLocalizedValue(
|
||||
translate: (key: string) => string,
|
||||
keyOrValue: string | undefined,
|
||||
fallback = '',
|
||||
) {
|
||||
if (!keyOrValue) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (!isLikelyLocalizationKey(keyOrValue)) {
|
||||
return keyOrValue
|
||||
}
|
||||
|
||||
const translatedValue = translate('::' + keyOrValue)
|
||||
return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue
|
||||
}
|
||||
|
||||
function buildAboutContent(
|
||||
about: AboutDto | undefined,
|
||||
translate: (key: string) => string,
|
||||
): AboutContent {
|
||||
return {
|
||||
heroTitle: resolveLocalizedValue(translate, ABOUT_HERO_TITLE_KEY, 'About'),
|
||||
heroTitleKey: ABOUT_HERO_TITLE_KEY,
|
||||
heroSubtitle: resolveLocalizedValue(translate, ABOUT_HERO_SUBTITLE_KEY),
|
||||
heroSubtitleKey: ABOUT_HERO_SUBTITLE_KEY,
|
||||
heroImage: resolveLocalizedValue(translate, ABOUT_HERO_IMAGE_KEY, ABOUT_HERO_IMAGE),
|
||||
heroImageKey: ABOUT_HERO_IMAGE_KEY,
|
||||
stats:
|
||||
about?.statsDto.map((stat) => ({
|
||||
icon: stat.icon || '',
|
||||
value: stat.value,
|
||||
label: resolveLocalizedValue(translate, stat.labelKey, stat.labelKey),
|
||||
labelKey:
|
||||
(isLikelyLocalizationKey(stat.labelKey) ? stat.labelKey : undefined) ||
|
||||
`Public.about.dynamic.stat.${stat.value}.label`,
|
||||
useCounter: stat.useCounter,
|
||||
counterEnd: stat.counterEnd,
|
||||
counterSuffix: stat.counterSuffix,
|
||||
counterDuration: stat.counterDuration,
|
||||
})) ?? [],
|
||||
descriptions:
|
||||
about?.descriptionsDto.map((item, index) => ({
|
||||
key:
|
||||
(isLikelyLocalizationKey(item) ? item : undefined) ||
|
||||
`Public.about.dynamic.description.${index + 1}`,
|
||||
text: resolveLocalizedValue(translate, item, item),
|
||||
})) ?? [],
|
||||
sections:
|
||||
about?.sectionsDto.map((section) => ({
|
||||
title: resolveLocalizedValue(translate, section.key, section.key),
|
||||
description: resolveLocalizedValue(translate, section.descKey, section.descKey),
|
||||
titleKey:
|
||||
(isLikelyLocalizationKey(section.key) ? section.key : undefined) ||
|
||||
`Public.about.dynamic.section.${section.key}.title`,
|
||||
descriptionKey:
|
||||
(isLikelyLocalizationKey(section.descKey) ? section.descKey : undefined) ||
|
||||
`Public.about.dynamic.section.${section.key}.description`,
|
||||
})) ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
const About: React.FC = () => {
|
||||
const { translate } = useLocalization()
|
||||
const navigate = useNavigate()
|
||||
const { setLang } = useStoreActions((actions) => actions.locale)
|
||||
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
|
||||
const configCultureName = useStoreState(
|
||||
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
||||
)
|
||||
const localeCurrentLang = useStoreState((state) => state.locale?.currentLang)
|
||||
const currentLanguage = configCultureName || localeCurrentLang || 'tr'
|
||||
const abpLanguages = useStoreState((state) => state.abpConfig.config?.localization.languages) || []
|
||||
const languageOptions = abpLanguages
|
||||
.filter((language) => Boolean(language.cultureName))
|
||||
.map((language) => {
|
||||
const cultureName = language.cultureName || 'tr'
|
||||
return {
|
||||
key: cultureName.toLowerCase().split('-')[0],
|
||||
cultureName,
|
||||
displayName: language.displayName || cultureName,
|
||||
}
|
||||
})
|
||||
const languagesFromConfig = languageOptions.map((language) => language.key)
|
||||
const editorLanguages = Array.from(
|
||||
new Set((languagesFromConfig.length > 0 ? languagesFromConfig : [currentLanguage]).filter(Boolean)),
|
||||
)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isPanelVisible, setIsPanelVisible] = useState(true)
|
||||
const [about, setAbout] = useState<AboutDto>()
|
||||
|
||||
const iconColors = [
|
||||
|
|
@ -21,10 +161,25 @@ const About: React.FC = () => {
|
|||
'text-indigo-600',
|
||||
]
|
||||
|
||||
function getRandomColor() {
|
||||
return iconColors[Math.floor(Math.random() * iconColors.length)]
|
||||
function getIconColor(index: number) {
|
||||
return iconColors[index % iconColors.length]
|
||||
}
|
||||
|
||||
const initialContent = !loading ? buildAboutContent(about, translate) : null
|
||||
const {
|
||||
content,
|
||||
isDesignMode,
|
||||
selectedBlockId,
|
||||
selectedLanguage,
|
||||
supportedLanguages,
|
||||
setContent,
|
||||
setSelectedBlockId,
|
||||
resetContent,
|
||||
} = useDesignerState<AboutContent>('about', initialContent, {
|
||||
currentLanguage,
|
||||
supportedLanguages: editorLanguages,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
const fetchServices = async () => {
|
||||
|
|
@ -40,6 +195,249 @@ const About: React.FC = () => {
|
|||
fetchServices()
|
||||
}, [])
|
||||
|
||||
const updateContent = (updater: (current: AboutContent) => AboutContent) => {
|
||||
setContent((current) => {
|
||||
if (!current) {
|
||||
return current
|
||||
}
|
||||
|
||||
return updater(current)
|
||||
})
|
||||
}
|
||||
|
||||
const handleFieldChange = (fieldKey: string, value: string | string[]) => {
|
||||
updateContent((current) => {
|
||||
if (fieldKey === 'heroTitle' || fieldKey === 'heroSubtitle' || fieldKey === 'heroImage') {
|
||||
return {
|
||||
...current,
|
||||
[fieldKey]: value as string,
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldKey.startsWith('description-')) {
|
||||
const index = Number(fieldKey.replace('description-', ''))
|
||||
const descriptions = [...current.descriptions]
|
||||
descriptions[index] = {
|
||||
...descriptions[index],
|
||||
text: value as string,
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
descriptions,
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId?.startsWith('stat-')) {
|
||||
const index = Number(selectedBlockId.replace('stat-', ''))
|
||||
const stats = [...current.stats]
|
||||
stats[index] = {
|
||||
...stats[index],
|
||||
[fieldKey]: value as string,
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId?.startsWith('section-')) {
|
||||
const index = Number(selectedBlockId.replace('section-', ''))
|
||||
const sections = [...current.sections]
|
||||
sections[index] = {
|
||||
...sections[index],
|
||||
[fieldKey]: value as string,
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
})
|
||||
}
|
||||
|
||||
const selectedSelection: DesignerSelection | null = React.useMemo(() => {
|
||||
if (!content || !selectedBlockId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (selectedBlockId === 'hero') {
|
||||
return {
|
||||
id: 'hero',
|
||||
title: content.heroTitleKey,
|
||||
description: 'Baslik, alt baslik ve arka plan gorselini guncelleyin.',
|
||||
fields: [
|
||||
{
|
||||
key: 'heroTitle',
|
||||
label: content.heroTitleKey,
|
||||
type: 'text',
|
||||
value: content.heroTitle,
|
||||
},
|
||||
{
|
||||
key: 'heroSubtitle',
|
||||
label: content.heroSubtitleKey,
|
||||
type: 'textarea',
|
||||
value: content.heroSubtitle,
|
||||
},
|
||||
{
|
||||
key: 'heroImage',
|
||||
label: content.heroImageKey,
|
||||
type: 'image',
|
||||
value: content.heroImage,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId === 'descriptions') {
|
||||
return {
|
||||
id: 'descriptions',
|
||||
title: 'Public.about.description.*',
|
||||
description: 'Orta bolumdeki aciklama metinlerini duzenleyin.',
|
||||
fields: content.descriptions.map((item, index) => ({
|
||||
key: `description-${index}`,
|
||||
label: item.key || `Public.about.dynamic.description.${index + 1}`,
|
||||
type: 'textarea',
|
||||
value: item.text,
|
||||
rows: index % 2 === 0 ? 4 : 3,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId.startsWith('stat-')) {
|
||||
const index = Number(selectedBlockId.replace('stat-', ''))
|
||||
const stat = content.stats[index]
|
||||
|
||||
if (!stat) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: selectedBlockId,
|
||||
title: stat.labelKey,
|
||||
description: translate('::Public.designer.desc1'),
|
||||
fields: [
|
||||
{
|
||||
key: 'icon',
|
||||
label: translate('::Public.designer.ikonAnahtari'),
|
||||
type: 'icon',
|
||||
value: stat.icon,
|
||||
placeholder: 'Ornek: FaUsers',
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
label: translate('::Public.designer.value'),
|
||||
type: 'text',
|
||||
value: stat.value
|
||||
},
|
||||
{
|
||||
key: 'label',
|
||||
label: translate('::' + stat.labelKey),
|
||||
type: 'text',
|
||||
value: stat.label,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId.startsWith('section-')) {
|
||||
const index = Number(selectedBlockId.replace('section-', ''))
|
||||
const section = content.sections[index]
|
||||
|
||||
if (!section) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: selectedBlockId,
|
||||
title: section.titleKey,
|
||||
description: 'Kart basligi ve aciklama metnini duzenleyin.',
|
||||
fields: [
|
||||
{
|
||||
key: 'title',
|
||||
label: section.titleKey,
|
||||
type: 'text',
|
||||
value: section.title,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: section.descriptionKey,
|
||||
type: 'textarea',
|
||||
value: section.description,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}, [content, selectedBlockId])
|
||||
|
||||
const handleSaveAndExit = async () => {
|
||||
if (!content || isSaving) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
await saveAboutPage({
|
||||
cultureName: selectedLanguage,
|
||||
heroTitleKey: content.heroTitleKey,
|
||||
heroTitleValue: content.heroTitle,
|
||||
heroSubtitleKey: content.heroSubtitleKey,
|
||||
heroSubtitleValue: content.heroSubtitle,
|
||||
heroImageKey: content.heroImageKey,
|
||||
heroImageValue: content.heroImage,
|
||||
stats: content.stats.map((stat, index) => ({
|
||||
icon: stat.icon,
|
||||
value: stat.value,
|
||||
labelKey: stat.labelKey || `Public.about.dynamic.stat.${index + 1}.label`,
|
||||
labelValue: stat.label,
|
||||
useCounter: stat.useCounter,
|
||||
counterEnd: stat.counterEnd,
|
||||
counterSuffix: stat.counterSuffix,
|
||||
counterDuration: stat.counterDuration,
|
||||
})),
|
||||
descriptions: content.descriptions.map((item, index) => ({
|
||||
key: item.key || `Public.about.dynamic.description.${index + 1}`,
|
||||
value: item.text,
|
||||
})),
|
||||
sections: content.sections.map((section, index) => ({
|
||||
titleKey: section.titleKey || `Public.about.dynamic.section.${index + 1}.title`,
|
||||
titleValue: section.title,
|
||||
descriptionKey:
|
||||
section.descriptionKey || `Public.about.dynamic.section.${index + 1}.description`,
|
||||
descriptionValue: section.description,
|
||||
})),
|
||||
})
|
||||
|
||||
await getConfig(false)
|
||||
setSelectedBlockId(null)
|
||||
navigate(ROUTES_ENUM.public.about, { replace: true })
|
||||
} catch (error) {
|
||||
console.error('About tasarimi kaydedilemedi:', error)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLanguageChange = (language: string) => {
|
||||
// Global locale changes asynchronously fetch fresh localization texts.
|
||||
// Keep designer language synced from store after that refresh.
|
||||
setLang(language)
|
||||
}
|
||||
|
||||
const handleSelectBlock = (blockId: string) => {
|
||||
setSelectedBlockId(blockId)
|
||||
if (!isPanelVisible) {
|
||||
setIsPanelVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
|
|
@ -58,44 +456,67 @@ const About: React.FC = () => {
|
|||
defaultTitle={APP_NAME}
|
||||
></Helmet>
|
||||
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className={`min-h-screen bg-gray-50 ${isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''}`}>
|
||||
{isDesignMode && !isPanelVisible && (
|
||||
<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"
|
||||
>
|
||||
{translate('::Public.designer.showPanel')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Hero Section */}
|
||||
<SelectableBlock
|
||||
id="hero"
|
||||
isActive={selectedBlockId === 'hero'}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="relative bg-blue-900 text-white py-12">
|
||||
<div
|
||||
className="absolute inset-0 opacity-20"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url("https://images.pexels.com/photos/3183183/pexels-photo-3183183.jpeg?auto=compress&cs=tinysrgb&w=1920")',
|
||||
backgroundImage: `url("${content?.heroImage ?? ABOUT_HERO_IMAGE}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
<div className="container mx-auto pt-20 relative">
|
||||
<h1 className="text-5xl font-bold ml-4 mt-3 mb-2 text-white">
|
||||
{translate('::App.About')}
|
||||
{content?.heroTitle}
|
||||
</h1>
|
||||
<p className="text-xl max-w-3xl ml-4">{translate('::Public.about.subtitle')}</p>
|
||||
<p className="text-xl max-w-3xl ml-4">{content?.heroSubtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
|
||||
{/* Stats Section */}
|
||||
<div className="py-10 bg-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
{about?.statsDto.map((stat, index) => {
|
||||
{content?.stats.map((stat, index) => {
|
||||
const IconComponent = navigationIcon[stat.icon || '']
|
||||
|
||||
let displayValue = stat.value
|
||||
let elementRef = undefined
|
||||
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<SelectableBlock
|
||||
key={index}
|
||||
id={`stat-${index}`}
|
||||
isActive={selectedBlockId === `stat-${index}`}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="text-center rounded-xl px-4 py-6">
|
||||
{IconComponent && (
|
||||
<IconComponent className={`w-12 h-12 mx-auto mb-4 ${getRandomColor()}`} />
|
||||
<IconComponent
|
||||
className={`w-12 h-12 mx-auto mb-4 ${getIconColor(index)}`}
|
||||
/>
|
||||
)}
|
||||
<div className="text-4xl font-bold text-gray-900 mb-2">{displayValue}</div>
|
||||
<div className="text-gray-600">{translate('::' + stat.labelKey)}</div>
|
||||
<div className="text-4xl font-bold text-gray-900 mb-2">{stat.value}</div>
|
||||
<div className="text-gray-600">{stat.label}</div>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -106,30 +527,60 @@ const About: React.FC = () => {
|
|||
<div className="py-6">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="mb-6">
|
||||
<SelectableBlock
|
||||
id="descriptions"
|
||||
isActive={selectedBlockId === 'descriptions'}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="p-5 mx-auto mx-auto text-gray-800 text-lg leading-relaxed shadow-md bg-white border-l-4 border-blue-600">
|
||||
<p>{translate('::' + about?.descriptionsDto[0])}</p>
|
||||
<p className="text-center p-5 text-blue-800">
|
||||
{translate('::' + about?.descriptionsDto[1])}
|
||||
</p>
|
||||
<p>{translate('::' + about?.descriptionsDto[2])}</p>
|
||||
<p className="text-center p-5 text-blue-800">
|
||||
{translate('::' + about?.descriptionsDto[3])}
|
||||
</p>
|
||||
<p>{content?.descriptions[0]?.text}</p>
|
||||
<p className="text-center p-5 text-blue-800">{content?.descriptions[1]?.text}</p>
|
||||
<p>{content?.descriptions[2]?.text}</p>
|
||||
<p className="text-center p-5 text-blue-800">{content?.descriptions[3]?.text}</p>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{about?.sectionsDto.map((section, index) => (
|
||||
<div key={index} className="bg-white p-8 rounded-xl shadow-lg">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{translate('::' + section.key)}
|
||||
</h3>
|
||||
<p className="text-gray-700">{translate('::' + section.descKey)}</p>
|
||||
{content?.sections.map((section, index) => (
|
||||
<SelectableBlock
|
||||
key={index}
|
||||
id={`section-${index}`}
|
||||
isActive={selectedBlockId === `section-${index}`}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{section.title}</h3>
|
||||
<p className="text-gray-700">{section.description}</p>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DesignerDrawer
|
||||
isOpen={isDesignMode && isPanelVisible}
|
||||
selection={selectedSelection}
|
||||
pageTitle="About"
|
||||
selectedLanguage={selectedLanguage}
|
||||
languages={
|
||||
languageOptions.length > 0
|
||||
? languageOptions
|
||||
: supportedLanguages.map((language) => ({
|
||||
key: language,
|
||||
cultureName: language,
|
||||
displayName: language.toUpperCase(),
|
||||
}))
|
||||
}
|
||||
onClose={() => setIsPanelVisible(false)}
|
||||
onSave={handleSaveAndExit}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onReset={resetContent}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,183 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
FaCode,
|
||||
FaGlobe,
|
||||
FaServer,
|
||||
FaUsers,
|
||||
FaShieldAlt,
|
||||
FaCog,
|
||||
FaCheckCircle,
|
||||
} from 'react-icons/fa'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FaCheckCircle } from 'react-icons/fa'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { ServiceDto } from '@/proxy/services/models'
|
||||
import { getServices } from '@/services/service.service'
|
||||
import { getServices, saveServicesPage } from '@/services/service.service'
|
||||
import navigationIcon from '@/proxy/menus/navigation-icon.config'
|
||||
import { Loading } from '@/components/shared'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { useStoreState } from '@/store'
|
||||
import { useStoreActions } from '@/store'
|
||||
import DesignerDrawer from './designer/DesignerDrawer'
|
||||
import SelectableBlock from './designer/SelectableBlock'
|
||||
import { DesignerSelection } from './designer/types'
|
||||
import { useDesignerState } from './designer/useDesignerState'
|
||||
|
||||
interface ServiceCardContent {
|
||||
icon: string
|
||||
title: string
|
||||
titleKey: string
|
||||
description: string
|
||||
descriptionKey?: string
|
||||
features: Array<{
|
||||
key: string
|
||||
value: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface SupportCardContent {
|
||||
icon: string
|
||||
title: string
|
||||
titleKey: string
|
||||
features: Array<{
|
||||
key: string
|
||||
value: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface ServicesContent {
|
||||
heroTitle: string
|
||||
heroTitleKey: string
|
||||
heroSubtitle: string
|
||||
heroSubtitleKey: string
|
||||
heroImage: string
|
||||
heroImageKey: string
|
||||
serviceItems: ServiceCardContent[]
|
||||
supportTitle: string
|
||||
supportTitleKey: string
|
||||
supportItems: SupportCardContent[]
|
||||
supportButtonLabel: string
|
||||
supportButtonLabelKey: string
|
||||
ctaTitle: string
|
||||
ctaTitleKey: string
|
||||
ctaDescription: string
|
||||
ctaDescriptionKey: string
|
||||
ctaButtonLabel: string
|
||||
ctaButtonLabelKey: string
|
||||
}
|
||||
|
||||
const SERVICES_HERO_IMAGE =
|
||||
'https://images.pexels.com/photos/3183173/pexels-photo-3183173.jpeg?auto=compress&cs=tinysrgb&w=1920'
|
||||
const SERVICES_HERO_TITLE_KEY = 'Public.services.title'
|
||||
const SERVICES_HERO_SUBTITLE_KEY = 'Public.services.subtitle'
|
||||
const SERVICES_HERO_IMAGE_KEY = 'Public.services.heroImage'
|
||||
const SERVICES_SUPPORT_TITLE_KEY = 'Public.services.support.title'
|
||||
const SERVICES_SUPPORT_BUTTON_KEY = 'Public.services.support.contactButton'
|
||||
const SERVICES_CTA_TITLE_KEY = 'Public.services.cta.title'
|
||||
const SERVICES_CTA_DESCRIPTION_KEY = 'Public.services.cta.description'
|
||||
const SERVICES_CTA_BUTTON_KEY = 'Public.services.support.contactButton'
|
||||
|
||||
function isLikelyLocalizationKey(value?: string) {
|
||||
return Boolean(value && /^[A-Za-z0-9_.-]+$/.test(value) && value.includes('.'))
|
||||
}
|
||||
|
||||
function resolveLocalizedValue(
|
||||
translate: (key: string) => string,
|
||||
keyOrValue: string | undefined,
|
||||
fallback = '',
|
||||
) {
|
||||
if (!keyOrValue) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (!isLikelyLocalizationKey(keyOrValue)) {
|
||||
return keyOrValue
|
||||
}
|
||||
|
||||
const translatedValue = translate('::' + keyOrValue)
|
||||
return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue
|
||||
}
|
||||
|
||||
function buildServicesContent(
|
||||
services: ServiceDto[],
|
||||
translate: (key: string) => string,
|
||||
): ServicesContent {
|
||||
return {
|
||||
heroTitle: resolveLocalizedValue(translate, SERVICES_HERO_TITLE_KEY, 'Services'),
|
||||
heroTitleKey: SERVICES_HERO_TITLE_KEY,
|
||||
heroSubtitle: resolveLocalizedValue(translate, SERVICES_HERO_SUBTITLE_KEY),
|
||||
heroSubtitleKey: SERVICES_HERO_SUBTITLE_KEY,
|
||||
heroImage: resolveLocalizedValue(translate, SERVICES_HERO_IMAGE_KEY, SERVICES_HERO_IMAGE),
|
||||
heroImageKey: SERVICES_HERO_IMAGE_KEY,
|
||||
serviceItems: services
|
||||
.filter((item) => item.type === 'service')
|
||||
.map((item, index) => ({
|
||||
icon: item.icon || '',
|
||||
title: resolveLocalizedValue(translate, item.title, item.title),
|
||||
titleKey:
|
||||
(isLikelyLocalizationKey(item.title) ? item.title : undefined) ||
|
||||
`Public.services.dynamic.service.${index + 1}.title`,
|
||||
description: resolveLocalizedValue(translate, item.description, item.description || ''),
|
||||
descriptionKey:
|
||||
(item.description && isLikelyLocalizationKey(item.description) ? item.description : undefined) ||
|
||||
`Public.services.dynamic.service.${index + 1}.description`,
|
||||
features: (item.features ?? []).map((feature, featureIndex) => ({
|
||||
key:
|
||||
(isLikelyLocalizationKey(feature) ? feature : undefined) ||
|
||||
`Public.services.dynamic.service.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: resolveLocalizedValue(translate, feature, feature),
|
||||
})),
|
||||
})),
|
||||
supportTitle: resolveLocalizedValue(translate, SERVICES_SUPPORT_TITLE_KEY),
|
||||
supportTitleKey: SERVICES_SUPPORT_TITLE_KEY,
|
||||
supportItems: services
|
||||
.filter((item) => item.type === 'support')
|
||||
.map((item, index) => ({
|
||||
icon: item.icon || '',
|
||||
title: resolveLocalizedValue(translate, item.title, item.title),
|
||||
titleKey:
|
||||
(isLikelyLocalizationKey(item.title) ? item.title : undefined) ||
|
||||
`Public.services.dynamic.support.${index + 1}.title`,
|
||||
features: (item.features ?? []).map((feature, featureIndex) => ({
|
||||
key:
|
||||
(isLikelyLocalizationKey(feature) ? feature : undefined) ||
|
||||
`Public.services.dynamic.support.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: resolveLocalizedValue(translate, feature, feature),
|
||||
})),
|
||||
})),
|
||||
supportButtonLabel: resolveLocalizedValue(translate, SERVICES_SUPPORT_BUTTON_KEY),
|
||||
supportButtonLabelKey: SERVICES_SUPPORT_BUTTON_KEY,
|
||||
ctaTitle: resolveLocalizedValue(translate, SERVICES_CTA_TITLE_KEY),
|
||||
ctaTitleKey: SERVICES_CTA_TITLE_KEY,
|
||||
ctaDescription: resolveLocalizedValue(translate, SERVICES_CTA_DESCRIPTION_KEY),
|
||||
ctaDescriptionKey: SERVICES_CTA_DESCRIPTION_KEY,
|
||||
ctaButtonLabel: resolveLocalizedValue(translate, SERVICES_CTA_BUTTON_KEY),
|
||||
ctaButtonLabelKey: SERVICES_CTA_BUTTON_KEY,
|
||||
}
|
||||
}
|
||||
|
||||
const Services: React.FC = () => {
|
||||
const { translate } = useLocalization()
|
||||
const navigate = useNavigate()
|
||||
const { setLang } = useStoreActions((actions) => actions.locale)
|
||||
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
|
||||
const configCultureName = useStoreState(
|
||||
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
||||
)
|
||||
const localeCurrentLang = useStoreState((state) => state.locale?.currentLang)
|
||||
const currentLanguage = configCultureName || localeCurrentLang || 'tr'
|
||||
const abpLanguages = useStoreState((state) => state.abpConfig.config?.localization.languages) || []
|
||||
const languageOptions = abpLanguages
|
||||
.filter((language) => Boolean(language.cultureName))
|
||||
.map((language) => {
|
||||
const cultureName = language.cultureName || 'tr'
|
||||
return {
|
||||
key: cultureName.toLowerCase().split('-')[0],
|
||||
cultureName,
|
||||
displayName: language.displayName || cultureName,
|
||||
}
|
||||
})
|
||||
const languagesFromConfig = languageOptions.map((language) => language.key)
|
||||
const editorLanguages = Array.from(
|
||||
new Set((languagesFromConfig.length > 0 ? languagesFromConfig : [currentLanguage]).filter(Boolean)),
|
||||
)
|
||||
const [services, setServices] = useState<ServiceDto[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isPanelVisible, setIsPanelVisible] = useState(true)
|
||||
|
||||
const iconColors = [
|
||||
'text-blue-600',
|
||||
|
|
@ -32,10 +188,25 @@ const Services: React.FC = () => {
|
|||
'text-indigo-600',
|
||||
]
|
||||
|
||||
function getRandomColor() {
|
||||
return iconColors[Math.floor(Math.random() * iconColors.length)]
|
||||
function getIconColor(index: number) {
|
||||
return iconColors[index % iconColors.length]
|
||||
}
|
||||
|
||||
const initialContent = !loading ? buildServicesContent(services, translate) : null
|
||||
const {
|
||||
content,
|
||||
isDesignMode,
|
||||
selectedBlockId,
|
||||
selectedLanguage,
|
||||
supportedLanguages,
|
||||
setContent,
|
||||
setSelectedBlockId,
|
||||
resetContent,
|
||||
} = useDesignerState<ServicesContent>('services', initialContent, {
|
||||
currentLanguage,
|
||||
supportedLanguages: editorLanguages,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
|
||||
|
|
@ -47,7 +218,7 @@ const Services: React.FC = () => {
|
|||
title: service.title,
|
||||
description: service.description,
|
||||
type: service.type,
|
||||
features: service.features,
|
||||
features: service.features ?? [],
|
||||
}))
|
||||
|
||||
setServices(items)
|
||||
|
|
@ -61,6 +232,356 @@ const Services: React.FC = () => {
|
|||
fetchServices()
|
||||
}, [])
|
||||
|
||||
const updateContent = (updater: (current: ServicesContent) => ServicesContent) => {
|
||||
setContent((current) => {
|
||||
if (!current) {
|
||||
return current
|
||||
}
|
||||
|
||||
return updater(current)
|
||||
})
|
||||
}
|
||||
|
||||
const handleFieldChange = (fieldKey: string, value: string | string[]) => {
|
||||
updateContent((current) => {
|
||||
if (
|
||||
fieldKey === 'heroTitle' ||
|
||||
fieldKey === 'heroSubtitle' ||
|
||||
fieldKey === 'heroImage' ||
|
||||
fieldKey === 'supportTitle' ||
|
||||
fieldKey === 'supportButtonLabel' ||
|
||||
fieldKey === 'ctaTitle' ||
|
||||
fieldKey === 'ctaDescription' ||
|
||||
fieldKey === 'ctaButtonLabel'
|
||||
) {
|
||||
return {
|
||||
...current,
|
||||
[fieldKey]: value as string,
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId?.startsWith('service-')) {
|
||||
const index = Number(selectedBlockId.replace('service-', ''))
|
||||
const serviceItems = [...current.serviceItems]
|
||||
const nextItem = { ...serviceItems[index] }
|
||||
|
||||
if (fieldKey.startsWith('feature-')) {
|
||||
const featureIndex = Number(fieldKey.replace('feature-', ''))
|
||||
const features = [...(nextItem.features ?? [])]
|
||||
const existingFeature = features[featureIndex]
|
||||
|
||||
if (!Number.isNaN(featureIndex)) {
|
||||
features[featureIndex] = {
|
||||
key:
|
||||
existingFeature?.key ||
|
||||
`Public.services.dynamic.service.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: value as string,
|
||||
}
|
||||
}
|
||||
|
||||
nextItem.features = features
|
||||
} else if (fieldKey === 'features') {
|
||||
nextItem.features = (value as string[]).map((feature, featureIndex) => ({
|
||||
key:
|
||||
nextItem.features[featureIndex]?.key ||
|
||||
`Public.services.dynamic.service.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: feature,
|
||||
}))
|
||||
} else {
|
||||
nextItem[fieldKey as keyof ServiceCardContent] = value as never
|
||||
}
|
||||
|
||||
serviceItems[index] = nextItem
|
||||
|
||||
return {
|
||||
...current,
|
||||
serviceItems,
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId?.startsWith('support-')) {
|
||||
const index = Number(selectedBlockId.replace('support-', ''))
|
||||
const supportItems = [...current.supportItems]
|
||||
const nextItem = { ...supportItems[index] }
|
||||
|
||||
if (fieldKey.startsWith('feature-')) {
|
||||
const featureIndex = Number(fieldKey.replace('feature-', ''))
|
||||
const features = [...(nextItem.features ?? [])]
|
||||
const existingFeature = features[featureIndex]
|
||||
|
||||
if (!Number.isNaN(featureIndex)) {
|
||||
features[featureIndex] = {
|
||||
key:
|
||||
existingFeature?.key ||
|
||||
`Public.services.dynamic.support.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: value as string,
|
||||
}
|
||||
}
|
||||
|
||||
nextItem.features = features
|
||||
} else if (fieldKey === 'features') {
|
||||
nextItem.features = (value as string[]).map((feature, featureIndex) => ({
|
||||
key:
|
||||
nextItem.features[featureIndex]?.key ||
|
||||
`Public.services.dynamic.support.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: feature,
|
||||
}))
|
||||
} else {
|
||||
nextItem[fieldKey as keyof SupportCardContent] = value as never
|
||||
}
|
||||
|
||||
supportItems[index] = nextItem
|
||||
|
||||
return {
|
||||
...current,
|
||||
supportItems,
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
})
|
||||
}
|
||||
|
||||
const selectedSelection: DesignerSelection | null = React.useMemo(() => {
|
||||
if (!content || !selectedBlockId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (selectedBlockId === 'hero') {
|
||||
return {
|
||||
id: 'hero',
|
||||
title: content.heroTitleKey,
|
||||
description: translate('::Public.designer.desc4'),
|
||||
fields: [
|
||||
{
|
||||
key: 'heroTitle',
|
||||
label: content.heroTitleKey,
|
||||
type: 'text',
|
||||
value: content.heroTitle,
|
||||
},
|
||||
{
|
||||
key: 'heroSubtitle',
|
||||
label: content.heroSubtitleKey,
|
||||
type: 'textarea',
|
||||
value: content.heroSubtitle,
|
||||
},
|
||||
{
|
||||
key: 'heroImage',
|
||||
label: content.heroImageKey,
|
||||
type: 'image',
|
||||
value: content.heroImage,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId === 'support-heading') {
|
||||
return {
|
||||
id: 'support-heading',
|
||||
title: content.supportTitleKey,
|
||||
description: translate('::Public.designer.desc3'),
|
||||
fields: [
|
||||
{
|
||||
key: 'supportTitle',
|
||||
label: content.supportTitleKey,
|
||||
type: 'text',
|
||||
value: content.supportTitle,
|
||||
},
|
||||
{
|
||||
key: 'supportButtonLabel',
|
||||
label: content.supportButtonLabelKey,
|
||||
type: 'text',
|
||||
value: content.supportButtonLabel,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId === 'cta') {
|
||||
return {
|
||||
id: 'cta',
|
||||
title: content.ctaTitleKey,
|
||||
description: translate('::Public.designer.desc2'),
|
||||
fields: [
|
||||
{
|
||||
key: 'ctaTitle',
|
||||
label: content.ctaTitleKey,
|
||||
type: 'text',
|
||||
value: content.ctaTitle,
|
||||
},
|
||||
{
|
||||
key: 'ctaDescription',
|
||||
label: content.ctaDescriptionKey,
|
||||
type: 'textarea',
|
||||
value: content.ctaDescription,
|
||||
},
|
||||
{
|
||||
key: 'ctaButtonLabel',
|
||||
label: content.ctaButtonLabelKey,
|
||||
type: 'text',
|
||||
value: content.ctaButtonLabel,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId.startsWith('service-')) {
|
||||
const index = Number(selectedBlockId.replace('service-', ''))
|
||||
const item = content.serviceItems[index]
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: selectedBlockId,
|
||||
title: item.titleKey,
|
||||
description: translate('::Public.designer.desc1'),
|
||||
fields: [
|
||||
{
|
||||
key: 'icon',
|
||||
label: translate('::Public.designer.ikonAnahtari'),
|
||||
type: 'icon',
|
||||
value: item.icon,
|
||||
placeholder: 'Ornek: FaServer',
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
label: item.titleKey,
|
||||
type: 'text',
|
||||
value: item.title,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: item.descriptionKey || `Public.services.dynamic.service.${index + 1}.description`,
|
||||
type: 'textarea',
|
||||
value: item.description,
|
||||
},
|
||||
...item.features.map((feature, featureIndex) => ({
|
||||
key: `feature-${featureIndex}`,
|
||||
label:
|
||||
feature.key || `Public.services.dynamic.service.${index + 1}.feature.${featureIndex + 1}`,
|
||||
type: 'text' as const,
|
||||
value: feature.value,
|
||||
})),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId.startsWith('support-')) {
|
||||
const index = Number(selectedBlockId.replace('support-', ''))
|
||||
const item = content.supportItems[index]
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: selectedBlockId,
|
||||
title: item.titleKey,
|
||||
description: translate('::Public.designer.desc1'),
|
||||
fields: [
|
||||
{
|
||||
key: 'icon',
|
||||
label: translate('::Public.designer.ikonAnahtari'),
|
||||
type: 'icon',
|
||||
value: item.icon,
|
||||
placeholder: 'Ornek: FaLifeRing',
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
label: item.titleKey,
|
||||
type: 'text',
|
||||
value: item.title,
|
||||
},
|
||||
...item.features.map((feature, featureIndex) => ({
|
||||
key: `feature-${featureIndex}`,
|
||||
label:
|
||||
feature.key || `Public.services.dynamic.support.${index + 1}.feature.${featureIndex + 1}`,
|
||||
type: 'text' as const,
|
||||
value: feature.value,
|
||||
})),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}, [content, selectedBlockId])
|
||||
|
||||
const handleSaveAndExit = async () => {
|
||||
if (!content || isSaving) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
await saveServicesPage({
|
||||
cultureName: selectedLanguage,
|
||||
heroTitleKey: content.heroTitleKey,
|
||||
heroTitleValue: content.heroTitle,
|
||||
heroSubtitleKey: content.heroSubtitleKey,
|
||||
heroSubtitleValue: content.heroSubtitle,
|
||||
heroImageKey: content.heroImageKey,
|
||||
heroImageValue: content.heroImage,
|
||||
supportTitleKey: content.supportTitleKey,
|
||||
supportTitleValue: content.supportTitle,
|
||||
supportButtonLabelKey: content.supportButtonLabelKey,
|
||||
supportButtonLabelValue: content.supportButtonLabel,
|
||||
ctaTitleKey: content.ctaTitleKey,
|
||||
ctaTitleValue: content.ctaTitle,
|
||||
ctaDescriptionKey: content.ctaDescriptionKey,
|
||||
ctaDescriptionValue: content.ctaDescription,
|
||||
ctaButtonLabelKey: content.ctaButtonLabelKey,
|
||||
ctaButtonLabelValue: content.ctaButtonLabel,
|
||||
serviceItems: content.serviceItems.map((item, index) => ({
|
||||
icon: item.icon,
|
||||
titleKey: item.titleKey || `Public.services.dynamic.service.${index + 1}.title`,
|
||||
titleValue: item.title,
|
||||
descriptionKey:
|
||||
item.descriptionKey || `Public.services.dynamic.service.${index + 1}.description`,
|
||||
descriptionValue: item.description,
|
||||
type: 'service' as const,
|
||||
features: item.features.map((feature, featureIndex) => ({
|
||||
key:
|
||||
feature.key || `Public.services.dynamic.service.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: feature.value,
|
||||
})),
|
||||
})),
|
||||
supportItems: content.supportItems.map((item, index) => ({
|
||||
icon: item.icon,
|
||||
titleKey: item.titleKey || `Public.services.dynamic.support.${index + 1}.title`,
|
||||
titleValue: item.title,
|
||||
type: 'support' as const,
|
||||
features: item.features.map((feature, featureIndex) => ({
|
||||
key:
|
||||
feature.key || `Public.services.dynamic.support.${index + 1}.feature.${featureIndex + 1}`,
|
||||
value: feature.value,
|
||||
})),
|
||||
})),
|
||||
})
|
||||
|
||||
await getConfig(false)
|
||||
setSelectedBlockId(null)
|
||||
} catch (error) {
|
||||
console.error('Services tasarimi kaydedilemedi:', error)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLanguageChange = (language: string) => {
|
||||
// Wait for global locale/config refresh; designer language follows store state.
|
||||
setLang(language)
|
||||
}
|
||||
|
||||
const handleSelectBlock = (blockId: string) => {
|
||||
setSelectedBlockId(blockId)
|
||||
if (!isPanelVisible) {
|
||||
setIsPanelVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
|
|
@ -72,63 +593,78 @@ const Services: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className={`min-h-screen bg-gray-50 ${isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''}`}>
|
||||
<Helmet
|
||||
titleTemplate={`%s | ${APP_NAME}`}
|
||||
title={translate('::App.Services')}
|
||||
defaultTitle={APP_NAME}
|
||||
></Helmet>
|
||||
|
||||
{isDesignMode && !isPanelVisible && (
|
||||
<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"
|
||||
>
|
||||
{translate('::Public.designer.showPanel')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Hero Section */}
|
||||
<SelectableBlock
|
||||
id="hero"
|
||||
isActive={selectedBlockId === 'hero'}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="relative bg-blue-900 text-white py-12">
|
||||
<div
|
||||
className="absolute inset-0 opacity-20"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url("https://images.pexels.com/photos/3183173/pexels-photo-3183173.jpeg?auto=compress&cs=tinysrgb&w=1920")',
|
||||
backgroundImage: `url("${content?.heroImage ?? SERVICES_HERO_IMAGE}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
></div>
|
||||
<div className="container mx-auto pt-20 relative">
|
||||
<h1 className="text-5xl font-bold ml-4 mt-3 mb-2 text-white">
|
||||
{translate('::Public.services.title')}
|
||||
</h1>
|
||||
<p className="text-xl max-w-3xl ml-4">{translate('::Public.services.subtitle')}</p>
|
||||
<h1 className="text-5xl font-bold ml-4 mt-3 mb-2 text-white">{content?.heroTitle}</h1>
|
||||
<p className="text-xl max-w-3xl ml-4">{content?.heroSubtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
|
||||
{/* Services Grid */}
|
||||
<div className="py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{services
|
||||
.filter((a) => a.type === 'service')
|
||||
.map((service, index) => {
|
||||
{content?.serviceItems.map((service, index) => {
|
||||
const IconComponent = navigationIcon[service.icon || '']
|
||||
return (
|
||||
<div
|
||||
<SelectableBlock
|
||||
key={index}
|
||||
className="bg-white rounded-xl shadow-lg p-8 hover:shadow-xl transition-shadow"
|
||||
id={`service-${index}`}
|
||||
isActive={selectedBlockId === `service-${index}`}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 hover:shadow-xl transition-shadow">
|
||||
<div className="mb-6">
|
||||
{IconComponent && (
|
||||
<IconComponent className={`w-12 h-12 ${getRandomColor()}`} />
|
||||
<IconComponent className={`w-12 h-12 ${getIconColor(index)}`} />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
{translate('::' + service.title)}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6">{translate('::' + service.description)}</p>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">{service.title}</h3>
|
||||
<p className="text-gray-600 mb-6">{service.description}</p>
|
||||
<ul className="space-y-2">
|
||||
{service.features.map((feature, fIndex) => (
|
||||
{(service.features ?? []).map((feature, fIndex) => (
|
||||
<li key={fIndex} className="flex items-center text-gray-700">
|
||||
<span className="w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
|
||||
{translate('::' + feature)}
|
||||
{feature.value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -138,31 +674,38 @@ const Services: React.FC = () => {
|
|||
{/* Support Plans */}
|
||||
<div className="bg-white py-10">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-3xl font-bold text-center mb-10">
|
||||
{translate('::Public.services.support.title')}
|
||||
</h2>
|
||||
<SelectableBlock
|
||||
id="support-heading"
|
||||
isActive={selectedBlockId === 'support-heading'}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<h2 className="text-3xl font-bold text-center mb-10">{content?.supportTitle}</h2>
|
||||
</SelectableBlock>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{services
|
||||
.filter((a) => a.type === 'support')
|
||||
.map((plan, index) => {
|
||||
{content?.supportItems.map((plan, index) => {
|
||||
const IconComponent = navigationIcon[plan.icon || '']
|
||||
|
||||
return (
|
||||
<div
|
||||
<SelectableBlock
|
||||
key={index}
|
||||
className="bg-white rounded-xl shadow-lg p-8 border border-gray-200"
|
||||
id={`support-${index}`}
|
||||
isActive={selectedBlockId === `support-${index}`}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 border border-gray-200">
|
||||
<div className="mb-6">
|
||||
{IconComponent && (
|
||||
<IconComponent className={`w-12 h-12 ${getRandomColor()}`} />
|
||||
<IconComponent className={`w-12 h-12 ${getIconColor(index)}`} />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4">{translate('::' + plan.title)}</h3>
|
||||
<h3 className="text-xl font-bold mb-4">{plan.title}</h3>
|
||||
<ul className="space-y-3 mb-8">
|
||||
{plan.features.map((feature, fIndex) => (
|
||||
{(plan.features ?? []).map((feature, fIndex) => (
|
||||
<li key={fIndex} className="flex items-center space-x-2 text-gray-700">
|
||||
<FaCheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
<span>{translate('::' + feature)}</span>
|
||||
<span>{feature.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -170,9 +713,10 @@ const Services: React.FC = () => {
|
|||
to={ROUTES_ENUM.public.contact}
|
||||
className="block text-center bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{translate('::Public.services.support.contactButton')}
|
||||
{content?.supportButtonLabel}
|
||||
</Link>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -180,22 +724,46 @@ const Services: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Call to Action */}
|
||||
<SelectableBlock
|
||||
id="cta"
|
||||
isActive={selectedBlockId === 'cta'}
|
||||
isDesignMode={isDesignMode}
|
||||
onSelect={handleSelectBlock}
|
||||
>
|
||||
<div className="bg-blue-900 text-white py-16">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h2 className="text-3xl font-bold mb-6 text-white">
|
||||
{translate('::Public.services.cta.title')}
|
||||
</h2>
|
||||
<p className="text-xl mb-8 max-w-2xl mx-auto">
|
||||
{translate('::Public.services.cta.description')}
|
||||
</p>
|
||||
<h2 className="text-3xl font-bold mb-6 text-white">{content?.ctaTitle}</h2>
|
||||
<p className="text-xl mb-8 max-w-2xl mx-auto">{content?.ctaDescription}</p>
|
||||
<Link
|
||||
to={ROUTES_ENUM.public.contact}
|
||||
className="bg-white text-blue-900 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
{translate('::Public.services.support.contactButton')}
|
||||
{content?.ctaButtonLabel}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBlock>
|
||||
|
||||
<DesignerDrawer
|
||||
isOpen={isDesignMode && isPanelVisible}
|
||||
selection={selectedSelection}
|
||||
pageTitle="Services"
|
||||
selectedLanguage={selectedLanguage}
|
||||
languages={
|
||||
languageOptions.length > 0
|
||||
? languageOptions
|
||||
: supportedLanguages.map((language) => ({
|
||||
key: language,
|
||||
cultureName: language,
|
||||
displayName: language.toUpperCase(),
|
||||
}))
|
||||
}
|
||||
onClose={() => setIsPanelVisible(false)}
|
||||
onSave={handleSaveAndExit}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onReset={resetContent}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
156
ui/src/views/public/designer/DesignerDrawer.tsx
Normal file
156
ui/src/views/public/designer/DesignerDrawer.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import Avatar from '@/components/ui/Avatar'
|
||||
import { Button, Input } from '@/components/ui'
|
||||
import { IconPickerField } from '@/views/shared/MenuAddDialog'
|
||||
import { DesignerSelection } from './types'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
||||
interface DesignerLanguageOption {
|
||||
key: string
|
||||
cultureName: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
function normalizeLanguageKey(language?: string) {
|
||||
if (!language) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return language.toLowerCase().split('-')[0]
|
||||
}
|
||||
|
||||
interface DesignerDrawerProps {
|
||||
isOpen: boolean
|
||||
selection: DesignerSelection | null
|
||||
pageTitle: string
|
||||
selectedLanguage: string
|
||||
languages: DesignerLanguageOption[]
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
onLanguageChange: (language: string) => void
|
||||
onReset: () => void
|
||||
onFieldChange: (fieldKey: string, value: string | string[]) => void
|
||||
}
|
||||
|
||||
const DesignerDrawer: React.FC<DesignerDrawerProps> = ({
|
||||
isOpen,
|
||||
selection,
|
||||
pageTitle,
|
||||
selectedLanguage,
|
||||
languages,
|
||||
onClose,
|
||||
onSave,
|
||||
onLanguageChange,
|
||||
onReset,
|
||||
onFieldChange,
|
||||
}) => {
|
||||
const { translate } = useLocalization()
|
||||
|
||||
if (!isOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-y-0 right-0 z-50 w-[420px] border-l border-slate-200 bg-white shadow-2xl">
|
||||
<div className="flex h-full flex-col bg-white">
|
||||
<div className="border-b border-slate-200 px-5 py-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-700">
|
||||
{pageTitle} {translate('::Public.designer.title')}
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-semibold text-slate-900">
|
||||
{selection?.title ?? translate('::Public.designer.noSelection')}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{selection?.description ?? translate('::Public.designer.selectField')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="plain" size="sm" onClick={onClose}>
|
||||
{translate('::Public.designer.close')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-1.5">
|
||||
{languages.map((language) => (
|
||||
<button
|
||||
key={`${language.cultureName}-${language.key}`}
|
||||
type="button"
|
||||
title={language.displayName}
|
||||
onClick={() => onLanguageChange(language.cultureName)}
|
||||
className={`flex h-9 w-9 items-center justify-center rounded-lg text-xl transition-all
|
||||
${
|
||||
normalizeLanguageKey(selectedLanguage) === language.key
|
||||
? 'border-sky-500 bg-sky-50 ring-2 ring-sky-200'
|
||||
: 'border-slate-200 hover:border-slate-400 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<Avatar
|
||||
size={22}
|
||||
shape="circle"
|
||||
src={`/img/countries/${language.cultureName}.png`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-y-auto px-5 py-5">
|
||||
{selection?.fields.map((field) => {
|
||||
const commonProps = {
|
||||
value: Array.isArray(field.value) ? field.value.join('\n') : field.value,
|
||||
placeholder: field.placeholder,
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const nextValue =
|
||||
field.type === 'list'
|
||||
? event.target.value
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
: event.target.value
|
||||
|
||||
onFieldChange(field.key, nextValue)
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<label className="mb-1 block text-sm font-semibold text-slate-700">
|
||||
{field.localizationKey || field.label}
|
||||
</label>
|
||||
{field.localizationKey && field.localizationKey !== field.label && (
|
||||
<p className="mb-2 text-xs text-slate-500">{field.label}</p>
|
||||
)}
|
||||
{field.type === 'icon' ? (
|
||||
<IconPickerField
|
||||
value={field.value as string}
|
||||
onChange={(iconKey) => onFieldChange(field.key, iconKey)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
{...commonProps}
|
||||
textArea={field.type === 'textarea' || field.type === 'list'}
|
||||
rows={field.rows ?? (field.type === 'list' ? 5 : 4)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{!selection && (
|
||||
<div className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-sm text-slate-500">
|
||||
{translate('::Public.designer.noSelectionDetails')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-slate-200 px-5 py-4">
|
||||
<Button variant="solid" block={true} onClick={onSave}>
|
||||
{translate('::Public.designer.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesignerDrawer
|
||||
49
ui/src/views/public/designer/SelectableBlock.tsx
Normal file
49
ui/src/views/public/designer/SelectableBlock.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import classNames from 'classnames'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
interface SelectableBlockProps extends PropsWithChildren {
|
||||
id: string
|
||||
isActive: boolean
|
||||
isDesignMode: boolean
|
||||
onSelect: (id: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SelectableBlock: React.FC<SelectableBlockProps> = ({
|
||||
id,
|
||||
isActive,
|
||||
isDesignMode,
|
||||
onSelect,
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
if (!isDesignMode) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative rounded-xl transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'ring-2 ring-sky-500 ring-offset-4 ring-offset-white'
|
||||
: 'ring-1 ring-sky-200/80 hover:ring-sky-400',
|
||||
className,
|
||||
)}
|
||||
onClick={() => onSelect(id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onSelect(id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="pointer-events-none absolute left-3 top-3 z-20 rounded-full"></div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectableBlock
|
||||
18
ui/src/views/public/designer/types.ts
Normal file
18
ui/src/views/public/designer/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export type DesignerFieldType = 'text' | 'textarea' | 'image' | 'list' | 'icon'
|
||||
|
||||
export interface DesignerField {
|
||||
key: string
|
||||
label: string
|
||||
type: DesignerFieldType
|
||||
value: string | string[]
|
||||
localizationKey?: string
|
||||
placeholder?: string
|
||||
rows?: number
|
||||
}
|
||||
|
||||
export interface DesignerSelection {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
fields: DesignerField[]
|
||||
}
|
||||
183
ui/src/views/public/designer/useDesignerState.ts
Normal file
183
ui/src/views/public/designer/useDesignerState.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useLocation, useSearchParams } from 'react-router-dom'
|
||||
|
||||
const DESIGNER_QUERY_KEY = 'design'
|
||||
|
||||
interface UseDesignerStateOptions {
|
||||
currentLanguage?: string
|
||||
supportedLanguages?: string[]
|
||||
}
|
||||
|
||||
function serializeValue<T>(value: T | null | undefined) {
|
||||
if (value === undefined) {
|
||||
return '__undefined__'
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return '__null__'
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return '__unserializable__'
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLanguageKey(language?: string) {
|
||||
if (!language) {
|
||||
return 'tr'
|
||||
}
|
||||
|
||||
return language.toLowerCase().split('-')[0]
|
||||
}
|
||||
|
||||
function cloneValue<T>(value: T): T {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value)) as T
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function useDesignerState<T>(
|
||||
pageKey: string,
|
||||
initialContent: T | null,
|
||||
options?: UseDesignerStateOptions,
|
||||
) {
|
||||
const location = useLocation()
|
||||
const [searchParams] = useSearchParams()
|
||||
const isDesignMode =
|
||||
searchParams.get(DESIGNER_QUERY_KEY) === '1' || location.pathname.endsWith('/designer')
|
||||
const currentLanguage = normalizeLanguageKey(options?.currentLanguage)
|
||||
const supportedLanguages = useMemo(() => {
|
||||
const base = (options?.supportedLanguages ?? [currentLanguage]).map((language) =>
|
||||
normalizeLanguageKey(language),
|
||||
)
|
||||
if (!base.includes(currentLanguage)) {
|
||||
return [currentLanguage, ...base]
|
||||
}
|
||||
|
||||
return Array.from(new Set(base))
|
||||
}, [currentLanguage, options?.supportedLanguages])
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(currentLanguage)
|
||||
const [byLanguage, setByLanguage] = useState<Record<string, T>>({})
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null)
|
||||
const seededDefaultsRef = useRef<Record<string, string>>({})
|
||||
|
||||
const selectedLanguagePrefix = normalizeLanguageKey(selectedLanguage)
|
||||
const content =
|
||||
byLanguage[selectedLanguage] ??
|
||||
byLanguage[selectedLanguagePrefix] ??
|
||||
byLanguage[currentLanguage] ??
|
||||
initialContent ??
|
||||
null
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedLanguage(normalizeLanguageKey(currentLanguage))
|
||||
}, [currentLanguage])
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialContent) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextSerializedInitial = serializeValue(initialContent)
|
||||
|
||||
setByLanguage((prev) => {
|
||||
const currentValue = prev[currentLanguage]
|
||||
const currentSerialized = serializeValue(currentValue)
|
||||
const previousSeed = seededDefaultsRef.current[currentLanguage]
|
||||
|
||||
if (currentValue !== undefined && previousSeed !== undefined && currentSerialized !== previousSeed) {
|
||||
return prev
|
||||
}
|
||||
|
||||
if (currentSerialized === nextSerializedInitial) {
|
||||
seededDefaultsRef.current[currentLanguage] = nextSerializedInitial
|
||||
return prev
|
||||
}
|
||||
|
||||
seededDefaultsRef.current[currentLanguage] = nextSerializedInitial
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[currentLanguage]: cloneValue(initialContent),
|
||||
}
|
||||
})
|
||||
|
||||
if (!isDesignMode && selectedBlockId !== null) {
|
||||
setSelectedBlockId(null)
|
||||
}
|
||||
}, [currentLanguage, initialContent, isDesignMode, selectedBlockId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesignMode || !initialContent) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedSelectedLanguage = normalizeLanguageKey(selectedLanguage)
|
||||
const hasSelectedLanguageContent =
|
||||
byLanguage[normalizedSelectedLanguage] !== undefined ||
|
||||
byLanguage[selectedLanguage] !== undefined
|
||||
|
||||
if (hasSelectedLanguageContent) {
|
||||
return
|
||||
}
|
||||
|
||||
// If selected language has no saved value, initialize it from the current translated defaults.
|
||||
seededDefaultsRef.current[normalizedSelectedLanguage] = serializeValue(initialContent)
|
||||
setByLanguage((prev) => ({
|
||||
...prev,
|
||||
[normalizedSelectedLanguage]: cloneValue(initialContent),
|
||||
}))
|
||||
}, [byLanguage, initialContent, isDesignMode, selectedLanguage])
|
||||
|
||||
const setContent = (updater: (current: T | null) => T | null) => {
|
||||
setByLanguage((prev) => {
|
||||
const current =
|
||||
prev[selectedLanguage] ??
|
||||
prev[normalizeLanguageKey(selectedLanguage)] ??
|
||||
prev[currentLanguage] ??
|
||||
initialContent ??
|
||||
null
|
||||
const next = updater(current)
|
||||
|
||||
if (!next) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[normalizeLanguageKey(selectedLanguage)]: cloneValue(next),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetContent = () => {
|
||||
if (!initialContent) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedSelectedLanguage = normalizeLanguageKey(selectedLanguage)
|
||||
seededDefaultsRef.current[normalizedSelectedLanguage] = serializeValue(initialContent)
|
||||
|
||||
setByLanguage((prev) => ({
|
||||
...prev,
|
||||
[normalizedSelectedLanguage]: cloneValue(initialContent),
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
isDesignMode,
|
||||
selectedBlockId,
|
||||
selectedLanguage,
|
||||
supportedLanguages,
|
||||
setContent,
|
||||
setSelectedBlockId,
|
||||
setSelectedLanguage,
|
||||
resetContent,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue