Public sayfalar Custom componente taşındı

This commit is contained in:
Sedat ÖZTÜRK 2026-09-08 18:00:23 +03:00
parent ad00e75911
commit 6742393271
94 changed files with 1959 additions and 8263 deletions

119
README.md
View file

@ -59,7 +59,7 @@ React/C# kodu yazmadan, yalnızca konfigürasyon ile mümkündür.
| **Arka plan işleri** | Hangfire + ABP Background Worker; zamanlanmış SQL, mail kuyruğu, yedekleme, oturum temizliği. | | **Arka plan işleri** | Hangfire + ABP Background Worker; zamanlanmış SQL, mail kuyruğu, yedekleme, oturum temizliği. |
| **Gerçek zamanlı** | SignalR tabanlı messenger ve video oda (WebRTC/TURN) altyapısı. | | **Gerçek zamanlı** | SignalR tabanlı messenger ve video oda (WebRTC/TURN) altyapısı. |
| **Intranet & Forum** | Duyuru, anket, sosyal duvar, etkinlik; kategori/konu/gönderi tabanlı forum. | | **Intranet & Forum** | Duyuru, anket, sosyal duvar, etkinlik; kategori/konu/gönderi tabanlı forum. |
| **Public web sitesi** | Ana sayfa, hakkımızda, hizmetler, ürün, ödeme, blog, demo ve iletişim içerikleri (tasarımcı ekranlarıyla düzenlenebilir). | | **Public web sitesi** | Menüsü ve sayfaları veritabanından yönetilen ziyaretçi sitesi: sayfalar Custom Component, menü `Pub` modülü (bkz. §10.4). Ürün/ödeme ve demo akışları fiziksel sayfalardır. |
| **PWA** | Service worker ile offline kabuk, kontrollü sürüm güncellemesi ve changelog bildirimi. | | **PWA** | Service worker ile offline kabuk, kontrollü sürüm güncellemesi ve changelog bildirimi. |
--- ---
@ -845,6 +845,21 @@ parametresi (`?RoleId=…`), diğerleri son ek taşır (`?Name.contains=…`). O
(`designerScriptDialect.ts` + `designerScriptRecipes.ts`); tarif grupları bileşen erişimi, API (`designerScriptDialect.ts` + `designerScriptRecipes.ts`); tarif grupları bileşen erişimi, API
çağrıları, form ve olay/sayfa başlıkları altında toplanır. çağrıları, form ve olay/sayfa başlıkları altında toplanır.
**Kanvas doğruluğu.** Tasarım zamanında görülen, canlıda görülendir; bunu iki mekanizma sağlar.
(1) *Yerleşim sınıflarının taşınması*: kanvas her düğümü seçim/sürükleme için bir sarmalayıcıya
alır ve üst öğe grid/flex olduğunda asıl öğe o sarmalayıcı olur. `splitLayoutClasses` düğümün
yerleşim sınıflarını sarmalayıcıya geçirir — `col-span`/`row-span`/`order`/`self`/`flex-1`/`basis`,
sabit ve kesirli `w-*`, kenar boşlukları taşınır; konum sınıfları (`absolute`, `inset-*`, `z-*`,
`w-full`) her ikisinde de kalır. Görünürlük sınıfları da sarmalayıcıya kopyalanır (`hidden lg:flex`
→ sarmalayıcıda `hidden lg:block`): aksi hâlde mobilde gizlenmesi gereken masaüstü şeridi yine bir
grid hücresi işgal eder ve komşularını yanlış hücreye iter. `fixed` kanvasta akışa çevrilir, yoksa
düğüm tasarım yüzeyinden taşardı. (2) *Cihaz çerçevesi*: araç çubuğundan masaüstü/tablet/mobil seçildiğinde kanvas kendi
belgesinde çizilir (`CanvasFrame`), böylece `md:`/`lg:` kuralları tarayıcı penceresine değil seçilen
genişliğe göre çözülür. Cihaz modunda tasarım yüzeyi kenar boşluğunu bırakır: kanvas o cihazın ekranıdır. `Responsive`
seçimi aynı sayfada, çerçevesiz ve boşluklu çizer. (3) *Gezinme kapalıdır*: kanvastaki bir bağlantıya
tıklamak seçim yapar, sayfayı değiştirmez — tıklama yakalama evresinde durdurulur, `react-router`'ın
`Link`'i de `defaultPrevented` görünce gezinmez.
**Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts` **Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts`
`Select.componentAs` için saklanan adı (`ReactSelect`, `CreatableSelect`, `AsyncSelect`) gerçek `Select.componentAs` için saklanan adı (`ReactSelect`, `CreatableSelect`, `AsyncSelect`) gerçek
bileşene çevirir — kanvas ve üretilen bileşenin aynı adı aynı şekilde yorumlamasını sağlayan tek bileşene çevirir — kanvas ve üretilen bileşenin aynı adı aynı şekilde yorumlamasını sağlayan tek
@ -875,6 +890,9 @@ Route.Authority ──┘ └► Permission
- Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel - Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel
view'lara veya runtime derlenmiş Custom Component'lere eşler, `DynamicRouter` React Router view'lara veya runtime derlenmiş Custom Component'lere eşler, `DynamicRouter` React Router
ağacını kurar; her korumalı route `ProtectedRoute` + `PermissionGuard` ile sarılır. ağacını kurar; her korumalı route `ProtectedRoute` + `PermissionGuard` ile sarılır.
- Aynı yolu bir fiziksel route ile bir Custom Component paylaşıyorsa **Custom Component kazanır**;
karşılığı olan dosyası bulunmayan rota kaydı hiç yüklenmez. Public site menüsü bu modelin
dışındadır: `Pub` modülü altında tutulur ve uygulama konfigürasyonuyla taşınır (bkz. §10.4).
**Yetki kodları** `PlatformConsts.AppCodes` içinde merkezî olarak tanımlıdır ve **Yetki kodları** `PlatformConsts.AppCodes` içinde merkezî olarak tanımlıdır ve
`PermissionsData.json` ile seed edilir. Ana gruplar: `App.Saas`, `App.Branches`, `App.Intranet.*`, `PermissionsData.json` ile seed edilir. Ana gruplar: `App.Saas`, `App.Branches`, `App.Intranet.*`,
@ -895,7 +913,6 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
| `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` | | `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` |
| `App.SqlQueryManager` | `.CrudEndpoints` | | `App.SqlQueryManager` | `.CrudEndpoints` |
| `App.Setup.Migrate` | Migration + seed tetikleme (host tarafı) | | `App.Setup.Migrate` | Migration + seed tetikleme (host tarafı) |
| `App.{Home,About,Services,Contact}.Design` | Public site sayfa tasarım modu (`?design=1`) |
> **Kural:** Yetki sözleşmesi olmayan menü/route önerilmez ve eklenmez. > **Kural:** Yetki sözleşmesi olmayan menü/route önerilmez ve eklenmez.
@ -943,9 +960,101 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
### 10.4. Public site ### 10.4. Public site
`/home` · `/about` · `/services` · `/products` · `/checkout` · `/payment` · `/success` · `/home` · `/about` · `/contact` (Custom Component) · `/products` · `/checkout` · `/payment` ·
`/blog` · `/blog/:id` · `/demo` · `/contact` — içerikler veritabanından yönetilir; `/success` · `/blog` · `/blog/:id` · `/demo` (fiziksel sayfalar).
`/about/designer` ve `/services/designer` sayfa tasarımcılarıdır.
Ziyaretçiye açık kısım iki parçadan oluşur ve ikisi de veritabanında yaşar:
**Menü.** Public menüler `Menu` tablosunda `ModuleId = "Pub"` modülü altında tutulur ve Menü
Yönetimi ekranından girilir (sözleşme: `PlatformConsts.PublicSite`). `App.Public.Header` kökü hem
üst navigasyonu hem de alt bilgideki hızlı bağlantıları besler — ikisi de `PublicNav` düğümüdür,
menü bir kez eklenir iki yerde birden görünür; alt kaydı olan bir menü üstte açılır liste olur.
Alt bilgide `cta` kayıtları (Giriş) yer almaz: oradaki düğüm `source="navigation"` ile çizilir. `Url` sayfa yolu, `Icon`
react-icons adı, `Order` sıra, `IsDisabled` yayın anahtarı, `DisplayName` dil anahtarıdır;
`CssClass` içinde `cta` geçen kayıt düz bağlantı yerine vurgulu buton olarak çizilir. Menü ucu
`[Authorize]` olduğu için menüler uygulama konfigürasyonunun `publicMenus` alanıyla taşınır
(`PlatformApplicationConfigurationContributor`), istemcide `usePublicMenu` okur. `Pub` modülü
yönetim navigasyonuna girmez (`abpConfig.model.ts` menüyü kurarken bu modülü eler; Menü Yönetimi
ekranı elemez, menüler oradan düzenlenir). Seed: `MenusData.json``Modules` + `Menus`.
**Sayfa.** Her public sayfa bir Custom Component'tir: `RoutePath` `/admin/` ile başlamadığı sürece
bileşen otomatik olarak public route olur ve `PublicLayout` altında yayınlanır. Tasarım yalnızca
DeveloperKit > Custom Components (Visual Designer) üzerinden değişir. Aynı yolu bir fiziksel route
ile paylaşan custom component kazanır; karşılığı olan dosyası bulunmayan rota kaydı ise hiç
yüklenmez (`hasPhysicalComponent`) — seeder yalnızca eklediği için eski kayıtlar veritabanında
kalsa bile ekran patlamaz. Seed: `configs/seeds/host/data/App.DeveloperKit.CustomComponents.json`;
bileşenin ayrı dosyası yoktur, her bileşen bu dosyada bir satırdır.
Runtime kapsamı sınırlıdır: bileşen kodundaki `import` satırları derlemede temizlenir,
kullanılabilenler `React`, `ui` kit, `PlatformViewHost`, `apiService`, `axios`, `DOMPurify`,
`translate`, `checkPermission`, `getCulture` ve **katalog düğümleri**
(`components/componentEditor/runtimeNodes.ts`). **`react-icons` ve `react-router` yoktur** — ikon
`<PlatformIcon name="FaUsers" size={40} className="..." />` ile ada göre çizilir (menü ikonlarıyla
aynı sözlük), gezinme `<a href>` ile yazılır. Kullanıcıya görünen metinler dil anahtarı üzerinden
gelir.
**Tailwind sınıf yüzeyi.** Bileşenin kodu veritabanında yaşar; derleme sırasında taranan tek
kaynak seed dosyasıdır (`configs/seeds/*/data/App.DeveloperKit.CustomComponents.json`). Bu dosya
depo kökünde, `ui/` dışındadır — derleme bağlamı yalnızca `ui/` klasörü olduğunda (docker, ayrı
checkout) glob boş döner ve seed'e özgü sınıflar (`md:col-span-4`, `bg-gray-950/80`, `opacity-20`)
CSS'e hiç girmez: uygulama yerelde doğru, sunucuda stilsiz görünür. Bu yüzden aynı sınıflar var
olan `ui/safelist.txt` dosyasına da yazılır. Dosyayı zaten `twSafelistGenerator` eklentisi üretiyor
(renk/ölçü listesi); eklenti artık seed dosyalarındaki `className` değerlerini de toplayıp
`# --- custom component classes ---` işaretinin altına ekliyor. `safelist.txt` `ui/` içinde ve
depoda olduğu için her ortamda taranır. Seed kökü görünmüyorsa işaretin altındaki liste olduğu gibi
korunur — eksik bir bağlam listeyi silip sorunu büyütmez. Ayrı bir betik ya da ikinci bir safelist
dosyası yoktur; liste her `npm run build` / `npm start` ile tazelenir.
**Katalog düğümleri.** Kanvasın doğrudan çizdiği, derlenen bileşen listesinde yer almayan
bileşenler tek bir sözlükte durur (`RUNTIME_NODE_COMPONENTS`): kanvas onu `VisualCanvas` içinde,
üretilen kod ise runtime kapsamı üzerinden kullanır — iki taraf aynı kaynaktan beslendiği için
tasarım ve çalışma zamanı ayrışamaz. Sözlükte `PlatformIcon` ve public site parçaları
(`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da
uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `LanguagesData.json`
ıklama anahtarı.
Elle yazılmış bir bileşen Visual Designer'da **kod modunda**ılır: tasarımcı yalnızca kendi
ürettiği kodu düğüm ağacına çevirir (`isDesignerGeneratedCode`), aksi hâlde tanımadığı JSX'i
düşürüp kaydederken kodu yok ederdi.
Public bileşenlerin tamamı`/home`, `/about`, `/contact` sayfaları ile üst ve alt bilgi
(`PublicHeader`, `PublicFooter`; rotaları yok) — **tasarımcı belgesi** olarak tutulur: `Props` alanındaki
`visualDesigner` düğüm ağacı kaynaktır (`sourceMode: "visual"`), `Code` ondan üretilir
(`generateDesignerCode`) ve runtime da bileşeni bu belgeden derler. Yani hepsi kanvasta görünür,
sürükle-bırak ve özellik paneliyle düzenlenir. Belgeler `div`/`h1`-`h5`/`p`/`span`/`img`/`a`
HTML düğümleri ve `PlatformIcon` ikon düğümüyle kurulmuştur; `::` ile başlayan metinler üretilen
kodda `translate()` çağrısına döner. `RoutePath` boş bırakılabilir; rotası olmayan bir bileşen
yalnızca başka bir ekranın içinde çizilir (alt bilgi gibi).
Üst ve alt bilgi de birer Custom Component'tir (`PublicHeader`, `PublicFooter`; rota yok):
`PublicLayout` ikisini de `renderComponent` ile çizer ve geriye yalnızca rota değişiminde başa
sarma ile demo penceresi kalır. Bileşen kayıtlı değilse o parça çizilmez.
Üst bilginin davranışı (menü listesi, aktif bağlantı, kaydırmada değişen görünüm, mobil panel,
tema/dil/demo kontrolleri) `components/publicSite` altındaki katalog düğümlerinde yaşar; yerleşim
ve sınıflar tasarımcıdadır: `PublicHeaderBar` (kaydırıldığında `scrolledClassName`'e geçen kabuk),
`PublicNav` (`source`: `navigation`/`actions`/`all`, `variant`: `horizontal`/`vertical`),
`PublicMobileMenu` (hamburger + içeriği tasarlanan panel), `PublicLogo`, `PublicThemeToggle`,
`PublicLanguageSelector`, `PublicDemoButton`.
Rotası olmayan bileşende `RoutePath` **NULL** yazılır; `Sas_H_CustomComponent`'in `RoutePath`
tekil indeksi yalnızca dolu değerleri kapsar (`[IsDeleted] = 0 AND [RoutePath] IS NOT NULL`), aksi
hâlde ikinci rotasız bileşen birincisiyle çakışırdı.
`/home`, `/about`, `/contact` bu yolla çalışır (`PublicHomePage`, `PublicAboutPage`,
`PublicContactPage`); üst ve alt bilgi rotasızdır (`PublicHeader`, `PublicFooter`).
Veriden beslenen public sayfalar (`/products`, `/checkout`, `/payment`, `/success`, `/blog`,
`/blog/:id`, `/demo`) fiziksel kalır; içerik sayfaları Custom Component'tir. Blog listesi ve detayı
`PublicAppService`'in `post-list` / `post-by-slug` uçlarından beslenir, yönetimi
`App.BlogManagement.*` ekranlarındadır.
Eski statik içerik sayfaları ve sayfa içi tasarım modu (`?design=1`) kaldırılmıştır:
`views/public/Home|About|Contact|Services`, `views/public/designer/`, bunlara özel istemci
servisleri; sunucuda `Home`/`About`/`Contact`/`Service` entity'leri, `PublicAppService` sayfa
Get/Save uçları ve DTO'ları, `TenantData.json` + `TenantDataSeeder`,
`App.{Home,About,Services,Contact}` yetkileri ve `.Design` alt yetkileri, `/admin/public/*/designer`
rota ve menüleri.
--- ---

View file

@ -7,7 +7,8 @@ namespace Sozsoft.Platform.DeveloperKit;
public class CustomComponentDto : FullAuditedEntityDto<Guid> public class CustomComponentDto : FullAuditedEntityDto<Guid>
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string RoutePath { get; set; } = string.Empty; /// <summary>Rotasi olmayan bilesende (ust bilgi, alt bilgi) bostur.</summary>
public string? RoutePath { get; set; }
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;
public string? Props { get; set; } public string? Props { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
@ -24,10 +25,13 @@ public class CustomComponentDto : FullAuditedEntityDto<Guid>
public class CreateUpdateCustomComponentDto public class CreateUpdateCustomComponentDto
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
[Required] /// <summary>
/// Bilesenin kendi rotasi. Bos birakilabilir: rotasi olmayan bir bilesen (ornegin
/// baska bir ekranin icinde cizilen alt bilgi) hicbir adrese baglanmaz.
/// </summary>
[StringLength(512)] [StringLength(512)]
[RegularExpression(@"^/.*", ErrorMessage = "RoutePath must start with '/'.")] [RegularExpression(@"^(/.*)?$", ErrorMessage = "RoutePath must start with '/'.")]
public string RoutePath { get; set; } = string.Empty; public string? RoutePath { get; set; }
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;
public string? Props { get; set; } public string? Props { get; set; }
public string? Description { get; set; } public string? Description { get; set; }

View file

@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Public;
public class AboutDto : EntityDto<Guid>
{
public Guid? TenantId { get; set; }
[JsonIgnore]
public string StatsJson { get; set; }
public List<StatDto> StatsDto
{
get
{
if (!string.IsNullOrEmpty(StatsJson))
return JsonSerializer.Deserialize<List<StatDto>>(StatsJson);
return new List<StatDto>();
}
set { StatsJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string DescriptionsJson { get; set; }
public List<string> DescriptionsDto
{
get
{
if (!string.IsNullOrEmpty(DescriptionsJson))
return JsonSerializer.Deserialize<List<string>>(DescriptionsJson);
return [];
}
set { DescriptionsJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string SectionsJson { get; set; }
public List<SectionDto> SectionsDto
{
get
{
if (!string.IsNullOrEmpty(SectionsJson))
return JsonSerializer.Deserialize<List<SectionDto>>(SectionsJson);
return new List<SectionDto>();
}
set { SectionsJson = JsonSerializer.Serialize(value); }
}
}

View file

@ -1,54 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Public;
public class ContactDto : EntityDto<Guid>
{
public string Address { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Location { get; set; }
public string TaxNumber { get; set; }
[JsonIgnore]
public string BankJson { get; set; }
public BankDto BankDto
{
get
{
if (!string.IsNullOrEmpty(BankJson))
return JsonSerializer.Deserialize<BankDto>(BankJson);
return new BankDto();
}
set { BankJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string WorkHoursJson { get; set; }
public WorkHoursDto WorkHoursDto
{
get
{
if (!string.IsNullOrEmpty(WorkHoursJson))
return JsonSerializer.Deserialize<WorkHoursDto>(WorkHoursJson);
return new WorkHoursDto();
}
set { WorkHoursJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string MapJson { get; set; }
public MapDto MapDto
{
get
{
if (!string.IsNullOrEmpty(MapJson))
return JsonSerializer.Deserialize<MapDto>(MapJson);
return new MapDto();
}
set { MapJson = JsonSerializer.Serialize(value); }
}
}

View file

@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Public;
public class HomeDto : EntityDto<Guid>
{
public string HeroBackgroundImageKey { get; set; }
public string HeroPrimaryCtaKey { get; set; }
public string HeroSecondaryCtaKey { get; set; }
public string FeaturesTitleKey { get; set; }
public string FeaturesSubtitleKey { get; set; }
public string SolutionsTitleKey { get; set; }
public string SolutionsSubtitleKey { get; set; }
public string CtaTitleKey { get; set; }
public string CtaSubtitleKey { get; set; }
public string CtaButtonLabelKey { get; set; }
[JsonIgnore]
public string SlidesJson { get; set; }
public List<HomeSlideDto> SlidesDto
{
get
{
if (!string.IsNullOrEmpty(SlidesJson))
return JsonSerializer.Deserialize<List<HomeSlideDto>>(SlidesJson);
return [];
}
set { SlidesJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string FeaturesJson { get; set; }
public List<HomeFeatureDto> FeaturesDto
{
get
{
if (!string.IsNullOrEmpty(FeaturesJson))
return JsonSerializer.Deserialize<List<HomeFeatureDto>>(FeaturesJson);
return [];
}
set { FeaturesJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string SolutionsJson { get; set; }
public List<HomeSolutionDto> SolutionsDto
{
get
{
if (!string.IsNullOrEmpty(SolutionsJson))
return JsonSerializer.Deserialize<List<HomeSolutionDto>>(SolutionsJson);
return [];
}
set { SolutionsJson = JsonSerializer.Serialize(value); }
}
}
public class HomeSlideDto
{
public string TitleKey { get; set; }
public string SubtitleKey { get; set; }
public string StyleClass { get; set; }
public List<HomeSlideServiceDto> Services { get; set; } = [];
}
public class HomeSlideServiceDto
{
public string Icon { get; set; }
public string TitleKey { get; set; }
public string DescriptionKey { get; set; }
public string StyleClass { get; set; }
}
public class HomeFeatureDto
{
public string Icon { get; set; }
public string TitleKey { get; set; }
public string DescriptionKey { get; set; }
public string StyleClass { get; set; }
}
public class HomeSolutionDto
{
public string Icon { get; set; }
public string ColorClass { get; set; }
public string TitleKey { get; set; }
public string DescriptionKey { get; set; }
public string StyleClass { get; set; }
}

View file

@ -1,217 +0,0 @@
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 List<SaveLocalizedTextInput> StyleTexts { 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 List<SaveLocalizedTextInput> StyleTexts { 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; }
}
public class SaveHomePageInput
{
public string CultureName { get; set; }
public string HeroBackgroundImageKey { get; set; }
public string HeroBackgroundImageValue { get; set; }
public string HeroPrimaryCtaKey { get; set; }
public string HeroPrimaryCtaValue { get; set; }
public string HeroPrimaryCtaStyleKey { get; set; }
public string HeroPrimaryCtaStyleValue { get; set; }
public string HeroSecondaryCtaKey { get; set; }
public string HeroSecondaryCtaValue { get; set; }
public string HeroSecondaryCtaStyleKey { get; set; }
public string HeroSecondaryCtaStyleValue { get; set; }
public string FeaturesTitleKey { get; set; }
public string FeaturesTitleValue { get; set; }
public string FeaturesTitleStyleKey { get; set; }
public string FeaturesTitleStyleValue { get; set; }
public string FeaturesSubtitleKey { get; set; }
public string FeaturesSubtitleValue { get; set; }
public string FeaturesSubtitleStyleKey { get; set; }
public string FeaturesSubtitleStyleValue { get; set; }
public string SolutionsTitleKey { get; set; }
public string SolutionsTitleValue { get; set; }
public string SolutionsTitleStyleKey { get; set; }
public string SolutionsTitleStyleValue { get; set; }
public string SolutionsSubtitleKey { get; set; }
public string SolutionsSubtitleValue { get; set; }
public string SolutionsSubtitleStyleKey { get; set; }
public string SolutionsSubtitleStyleValue { get; set; }
public string CtaTitleKey { get; set; }
public string CtaTitleValue { get; set; }
public string CtaTitleStyleKey { get; set; }
public string CtaTitleStyleValue { get; set; }
public string CtaSubtitleKey { get; set; }
public string CtaSubtitleValue { get; set; }
public string CtaSubtitleStyleKey { get; set; }
public string CtaSubtitleStyleValue { get; set; }
public string CtaButtonLabelKey { get; set; }
public string CtaButtonLabelValue { get; set; }
public string CtaButtonStyleKey { get; set; }
public string CtaButtonStyleValue { get; set; }
public List<SaveHomeSlideInput> Slides { get; set; } = [];
public List<SaveHomeFeatureInput> Features { get; set; } = [];
public List<SaveHomeSolutionInput> Solutions { get; set; } = [];
}
public class SaveHomeSlideInput
{
public string TitleKey { get; set; }
public string TitleValue { get; set; }
public string SubtitleKey { get; set; }
public string SubtitleValue { get; set; }
public string StyleClass { get; set; }
public List<SaveHomeSlideServiceInput> Services { get; set; } = [];
}
public class SaveHomeSlideServiceInput
{
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 StyleClass { get; set; }
}
public class SaveHomeFeatureInput
{
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 StyleClass { get; set; }
}
public class SaveHomeSolutionInput
{
public string Icon { get; set; }
public string ColorClass { get; set; }
public string TitleKey { get; set; }
public string TitleValue { get; set; }
public string DescriptionKey { get; set; }
public string DescriptionValue { get; set; }
public string StyleClass { get; set; }
}
public class SaveContactPageInput
{
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 ContactInfoTitleKey { get; set; }
public string ContactInfoTitleValue { get; set; }
public string AddressKey { get; set; }
public string AddressValue { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Location { get; set; }
public string TaxNumber { get; set; }
public string BankTitleKey { get; set; }
public string BankTitleValue { get; set; }
public string BankAccountHolder { get; set; }
public string BankBranch { get; set; }
public string BankAccountNumber { get; set; }
public string BankIban { get; set; }
public string BankStyleClass { get; set; }
public string WorkHoursTitleKey { get; set; }
public string WorkHoursTitleValue { get; set; }
public string WorkWeekdayKey { get; set; }
public string WorkWeekdayValue { get; set; }
public string WorkWeekendKey { get; set; }
public string WorkWeekendValue { get; set; }
public string WorkWhatsappKey { get; set; }
public string WorkWhatsappValue { get; set; }
public string WorkHoursStyleClass { get; set; }
public string MapTitleKey { get; set; }
public string MapTitleValue { get; set; }
public string MapSrc { get; set; }
public string MapWidth { get; set; }
public string MapHeight { get; set; }
public bool MapAllowFullScreen { get; set; }
public string MapLoading { get; set; }
public string MapReferrerPolicy { get; set; }
public string MapContainerStyleClass { get; set; }
public string MapFrameStyleClass { get; set; }
public List<SaveLocalizedTextInput> StyleTexts { get; set; } = [];
}

View file

@ -1,15 +0,0 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.Public;
public class ServiceDto : FullAuditedEntityDto<Guid>
{
public string? Icon { get; set; }
public string Title { get; set; }
public string? Description { get; set; }
public string Type { get; set; }
public string[] Features { get; set; } = [];
}

View file

@ -66,6 +66,8 @@ public class CustomComponentAppService : CrudAppService<
public override async Task<CustomComponentDto> CreateAsync(CreateUpdateCustomComponentDto input) public override async Task<CustomComponentDto> CreateAsync(CreateUpdateCustomComponentDto input)
{ {
NormalizeRoutePath(input);
var result = await base.CreateAsync(input); var result = await base.CreateAsync(input);
await SyncDataSourcesAsync(result.Id); await SyncDataSourcesAsync(result.Id);
await _seedDataSynchronizer.SyncRowAsync(ListFormCode, result.Id, SeedSyncOperation.Insert); await _seedDataSynchronizer.SyncRowAsync(ListFormCode, result.Id, SeedSyncOperation.Insert);
@ -76,6 +78,8 @@ public class CustomComponentAppService : CrudAppService<
Guid id, Guid id,
CreateUpdateCustomComponentDto input) CreateUpdateCustomComponentDto input)
{ {
NormalizeRoutePath(input);
var existingComponent = await Repository.GetAsync(id); var existingComponent = await Repository.GetAsync(id);
if (input.Props is null) if (input.Props is null)
@ -90,6 +94,16 @@ public class CustomComponentAppService : CrudAppService<
return result; return result;
} }
/// <summary>
/// Rotasi olmayan bilesen (ust bilgi, alt bilgi) icin yol bostur. Bos dize saklanirsa
/// <c>RoutePath</c> tekil indeksi ikinci rotasiz bilesende carpisir; deger NULL yazilir.
/// </summary>
private static void NormalizeRoutePath(CreateUpdateCustomComponentDto input)
{
if (string.IsNullOrWhiteSpace(input.RoutePath))
input.RoutePath = null;
}
public override async Task DeleteAsync(Guid id) public override async Task DeleteAsync(Guid id)
{ {
await base.DeleteAsync(id); await base.DeleteAsync(id);

View file

@ -277,7 +277,10 @@ public class MenuAppService : CrudAppService<
await CheckGetPolicyAsync(); await CheckGetPolicyAsync();
var query = await _menuRepository.GetQueryableAsync(); var query = await _menuRepository.GetQueryableAsync();
query = query.Where(a => !a.IsDisabled && a.ModuleId != null); // Public site menusu yonetim kenar cubugunda yer almaz; o agac ziyaretci
// duzenine aittir ve uygulama konfigurasyonu uzerinden tasinir.
query = query.Where(a => !a.IsDisabled && a.ModuleId != null &&
a.ModuleId != PlatformConsts.PublicSite.ModuleId);
var entities = await AsyncExecuter.ToListAsync(query); var entities = await AsyncExecuter.ToListAsync(query);
var entityDtos = await MapToGetListOutputDtosAsync(entities); var entityDtos = await MapToGetListOutputDtosAsync(entities);

View file

@ -21,13 +21,11 @@ using static Sozsoft.Platform.Data.Seeds.SeedConsts;
namespace Sozsoft.Platform.Public; namespace Sozsoft.Platform.Public;
/// <summary> /// <summary>
/// Public site uc noktalari. Okuma metotlari anonim erisime aciktir; sayfa tasarimini /// Public site uc noktalari: demo talebi, urun/odeme listeleri, siparis olusturma ve
/// degistiren <c>Save*PageAsync</c> metotlari <see cref="AppCodes.WebSiteDesign"/> /// ulke/il/ilce aramalari. Tamami anonim erisime aciktir.
/// yetkileriyle korunur (yetkiler PermissionsData.json icinde tanimli, seeder ile yonetilir).
/// </summary> /// </summary>
public class PublicAppService : PlatformAppService public class PublicAppService : PlatformAppService
{ {
private readonly IRepository<Service, Guid> _serviceRepository;
private readonly ISettingProvider _settingProvider; private readonly ISettingProvider _settingProvider;
private readonly ISozsoftEmailSender _emailSender; private readonly ISozsoftEmailSender _emailSender;
private readonly IRepository<Demo, Guid> _demoRepository; private readonly IRepository<Demo, Guid> _demoRepository;
@ -37,18 +35,11 @@ public class PublicAppService : PlatformAppService
private readonly IRepository<PaymentMethod, Guid> _paymentMethodRepository; private readonly IRepository<PaymentMethod, Guid> _paymentMethodRepository;
private readonly IRepository<InstallmentOption> _installmentOptionRepository; private readonly IRepository<InstallmentOption> _installmentOptionRepository;
private readonly IRepository<Order, Guid> _orderRepository; private readonly IRepository<Order, Guid> _orderRepository;
private readonly IRepository<About, Guid> _aboutRepository;
private readonly IRepository<Home, Guid> _homeRepository;
private readonly IRepository<Contact, Guid> _contactRepository;
private readonly IRepository<Country, string> _countryRepository; private readonly IRepository<Country, string> _countryRepository;
private readonly IRepository<City, Guid> _cityRepository; private readonly IRepository<City, Guid> _cityRepository;
private readonly IRepository<District, Guid> _districtRepository; private readonly IRepository<District, Guid> _districtRepository;
private readonly IRepository<LanguageKey, Guid> _languageKeyRepository;
private readonly IRepository<LanguageText, Guid> _languageTextRepository;
private readonly LanguageTextAppService _languageTextAppService;
public PublicAppService( public PublicAppService(
IRepository<Service, Guid> serviceRepository,
ISettingProvider settingProvider, ISettingProvider settingProvider,
ISozsoftEmailSender emailSender, ISozsoftEmailSender emailSender,
IRepository<Demo, Guid> demoRepository, IRepository<Demo, Guid> demoRepository,
@ -58,18 +49,11 @@ public class PublicAppService : PlatformAppService
IRepository<PaymentMethod, Guid> paymentMethodRepository, IRepository<PaymentMethod, Guid> paymentMethodRepository,
IRepository<InstallmentOption> installmentOptionRepository, IRepository<InstallmentOption> installmentOptionRepository,
IRepository<Order, Guid> orderRepository, IRepository<Order, Guid> orderRepository,
IRepository<About, Guid> aboutRepository,
IRepository<Home, Guid> homeRepository,
IRepository<Contact, Guid> contactRepository,
IRepository<Country, string> countryRepository, IRepository<Country, string> countryRepository,
IRepository<City, Guid> cityRepository, IRepository<City, Guid> cityRepository,
IRepository<District, Guid> districtRepository, IRepository<District, Guid> districtRepository
IRepository<LanguageKey, Guid> languageKeyRepository,
IRepository<LanguageText, Guid> languageTextRepository,
LanguageTextAppService languageTextAppService
) )
{ {
_serviceRepository = serviceRepository;
_settingProvider = settingProvider; _settingProvider = settingProvider;
_emailSender = emailSender; _emailSender = emailSender;
_demoRepository = demoRepository; _demoRepository = demoRepository;
@ -79,235 +63,11 @@ public class PublicAppService : PlatformAppService
_paymentMethodRepository = paymentMethodRepository; _paymentMethodRepository = paymentMethodRepository;
_installmentOptionRepository = installmentOptionRepository; _installmentOptionRepository = installmentOptionRepository;
_orderRepository = orderRepository; _orderRepository = orderRepository;
_aboutRepository = aboutRepository;
_homeRepository = homeRepository;
_contactRepository = contactRepository;
_languageKeyRepository = languageKeyRepository;
_languageTextRepository = languageTextRepository;
_languageTextAppService = languageTextAppService;
_countryRepository = countryRepository; _countryRepository = countryRepository;
_cityRepository = cityRepository; _cityRepository = cityRepository;
_districtRepository = districtRepository; _districtRepository = districtRepository;
} }
public async Task<List<ServiceDto>> GetServicesListAsync()
{
var queryable = await _serviceRepository.GetQueryableAsync();
var entity = await AsyncExecuter.ToListAsync(queryable.OrderBy(a => a.CreationTime));
return ObjectMapper.Map<List<Service>, List<ServiceDto>>(entity);
}
[Authorize(AppCodes.WebSiteDesign.About)]
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);
}
foreach (var styleText in input.StyleTexts)
{
await UpsertLanguageTextAsync(input.CultureName, styleText.Key, styleText.Value);
}
await _aboutRepository.UpdateAsync(entity, autoSave: true);
await _languageTextAppService.ClearRedisCacheAsync();
}
[Authorize(AppCodes.WebSiteDesign.Services)]
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);
foreach (var styleText in input.StyleTexts)
{
await UpsertLanguageTextAsync(input.CultureName, styleText.Key, styleText.Value);
}
await CurrentUnitOfWork!.SaveChangesAsync();
await _languageTextAppService.ClearRedisCacheAsync();
}
public async Task<HomeDto> GetHomeAsync()
{
var entity = await _homeRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(Home));
return ObjectMapper.Map<Home, HomeDto>(entity);
}
[Authorize(AppCodes.WebSiteDesign.Home)]
public async Task SaveHomePageAsync(SaveHomePageInput input)
{
var entity = await _homeRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(Home));
entity.HeroBackgroundImageKey = input.HeroBackgroundImageKey;
entity.HeroPrimaryCtaKey = input.HeroPrimaryCtaKey;
entity.HeroSecondaryCtaKey = input.HeroSecondaryCtaKey;
entity.FeaturesTitleKey = input.FeaturesTitleKey;
entity.FeaturesSubtitleKey = input.FeaturesSubtitleKey;
entity.SolutionsTitleKey = input.SolutionsTitleKey;
entity.SolutionsSubtitleKey = input.SolutionsSubtitleKey;
entity.CtaTitleKey = input.CtaTitleKey;
entity.CtaSubtitleKey = input.CtaSubtitleKey;
entity.CtaButtonLabelKey = input.CtaButtonLabelKey;
entity.SlidesJson = JsonSerializer.Serialize(input.Slides.Select(slide => new HomeSlideDto
{
TitleKey = slide.TitleKey,
SubtitleKey = slide.SubtitleKey,
StyleClass = slide.StyleClass,
Services = slide.Services.Select(service => new HomeSlideServiceDto
{
Icon = service.Icon,
TitleKey = service.TitleKey,
DescriptionKey = service.DescriptionKey,
StyleClass = service.StyleClass,
}).ToList(),
}).ToList());
entity.FeaturesJson = JsonSerializer.Serialize(input.Features.Select(feature => new HomeFeatureDto
{
Icon = feature.Icon,
TitleKey = feature.TitleKey,
DescriptionKey = feature.DescriptionKey,
StyleClass = feature.StyleClass,
}).ToList());
entity.SolutionsJson = JsonSerializer.Serialize(input.Solutions.Select(solution => new HomeSolutionDto
{
Icon = solution.Icon,
ColorClass = solution.ColorClass,
TitleKey = solution.TitleKey,
DescriptionKey = solution.DescriptionKey,
StyleClass = solution.StyleClass,
}).ToList());
await _homeRepository.UpdateAsync(entity, autoSave: false);
await UpsertLanguageTextAsync(input.CultureName, input.HeroBackgroundImageKey, input.HeroBackgroundImageValue);
await UpsertLanguageTextAsync(input.CultureName, input.HeroPrimaryCtaKey, input.HeroPrimaryCtaValue);
await UpsertLanguageTextAsync(input.CultureName, input.HeroPrimaryCtaStyleKey, input.HeroPrimaryCtaStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.HeroSecondaryCtaKey, input.HeroSecondaryCtaValue);
await UpsertLanguageTextAsync(input.CultureName, input.HeroSecondaryCtaStyleKey, input.HeroSecondaryCtaStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.FeaturesTitleKey, input.FeaturesTitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.FeaturesTitleStyleKey, input.FeaturesTitleStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.FeaturesSubtitleKey, input.FeaturesSubtitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.FeaturesSubtitleStyleKey, input.FeaturesSubtitleStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.SolutionsTitleKey, input.SolutionsTitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.SolutionsTitleStyleKey, input.SolutionsTitleStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.SolutionsSubtitleKey, input.SolutionsSubtitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.SolutionsSubtitleStyleKey, input.SolutionsSubtitleStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.CtaTitleKey, input.CtaTitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.CtaTitleStyleKey, input.CtaTitleStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.CtaSubtitleKey, input.CtaSubtitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.CtaSubtitleStyleKey, input.CtaSubtitleStyleValue);
await UpsertLanguageTextAsync(input.CultureName, input.CtaButtonLabelKey, input.CtaButtonLabelValue);
await UpsertLanguageTextAsync(input.CultureName, input.CtaButtonStyleKey, input.CtaButtonStyleValue);
foreach (var slide in input.Slides)
{
await UpsertLanguageTextAsync(input.CultureName, slide.TitleKey, slide.TitleValue);
await UpsertLanguageTextAsync(input.CultureName, slide.SubtitleKey, slide.SubtitleValue);
foreach (var service in slide.Services)
{
await UpsertLanguageTextAsync(input.CultureName, service.TitleKey, service.TitleValue);
await UpsertLanguageTextAsync(input.CultureName, service.DescriptionKey, service.DescriptionValue);
}
}
foreach (var feature in input.Features)
{
await UpsertLanguageTextAsync(input.CultureName, feature.TitleKey, feature.TitleValue);
await UpsertLanguageTextAsync(input.CultureName, feature.DescriptionKey, feature.DescriptionValue);
}
foreach (var solution in input.Solutions)
{
await UpsertLanguageTextAsync(input.CultureName, solution.TitleKey, solution.TitleValue);
await UpsertLanguageTextAsync(input.CultureName, solution.DescriptionKey, solution.DescriptionValue);
}
await CurrentUnitOfWork!.SaveChangesAsync();
await _languageTextAppService.ClearRedisCacheAsync();
}
public async Task CreateDemoAsync(DemoDto input) public async Task CreateDemoAsync(DemoDto input)
{ {
var demo = ObjectMapper.Map<DemoDto, Demo>(input); var demo = ObjectMapper.Map<DemoDto, Demo>(input);
@ -565,7 +325,6 @@ public class PublicAppService : PlatformAppService
Categories = categoryDtos Categories = categoryDtos
}; };
} }
private async Task<BlogPostDto> GetPostAsync(Guid id) private async Task<BlogPostDto> GetPostAsync(Guid id)
{ {
// Tek sorguda post ve category'yi çek (N+1 önleme) // Tek sorguda post ve category'yi çek (N+1 önleme)
@ -586,7 +345,6 @@ public class PublicAppService : PlatformAppService
return dto; return dto;
} }
public async Task<BlogPostDto> GetPostBySlugAsync(string slug) public async Task<BlogPostDto> GetPostBySlugAsync(string slug)
{ {
var post = await _postRepository.FirstOrDefaultAsync(x => x.Slug == slug); var post = await _postRepository.FirstOrDefaultAsync(x => x.Slug == slug);
@ -1043,19 +801,6 @@ public class PublicAppService : PlatformAppService
return WebUtility.HtmlEncode(value ?? string.Empty); return WebUtility.HtmlEncode(value ?? string.Empty);
} }
public async Task<AboutDto> GetAboutAsync()
{
var entity = await _aboutRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(About));
return ObjectMapper.Map<About, AboutDto>(entity);
}
public async Task<ContactDto> GetContactAsync()
{
var entity = await _contactRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(Contact));
return ObjectMapper.Map<Contact, ContactDto>(entity);
}
public async Task<List<CountryDto>> GetCountryAsync() public async Task<List<CountryDto>> GetCountryAsync()
{ {
var queryable = await _countryRepository.GetQueryableAsync(); var queryable = await _countryRepository.GetQueryableAsync();
@ -1128,121 +873,6 @@ public class PublicAppService : PlatformAppService
.ToList(); .ToList();
} }
[Authorize(AppCodes.WebSiteDesign.Contact)]
public async Task SaveContactPageAsync(SaveContactPageInput input)
{
var entity = await _contactRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(Contact));
entity.Address = input.AddressKey;
entity.PhoneNumber = input.PhoneNumber;
entity.Email = input.Email;
entity.Location = input.Location;
entity.TaxNumber = long.TryParse(input.TaxNumber, out var taxNumber) ? taxNumber : null;
entity.BankJson = JsonSerializer.Serialize(new BankDto
{
AccountHolder = input.BankAccountHolder,
Branch = input.BankBranch,
AccountNumber = input.BankAccountNumber,
Iban = input.BankIban,
StyleClass = input.BankStyleClass,
});
entity.WorkHoursJson = JsonSerializer.Serialize(new WorkHoursDto
{
Weekday = input.WorkWeekdayKey,
Weekend = input.WorkWeekendKey,
Whatsapp = input.WorkWhatsappKey,
StyleClass = input.WorkHoursStyleClass,
});
entity.MapJson = JsonSerializer.Serialize(new MapDto
{
Title = input.MapTitleKey,
Src = input.MapSrc,
Width = input.MapWidth,
Height = input.MapHeight,
AllowFullScreen = input.MapAllowFullScreen,
Loading = input.MapLoading,
ReferrerPolicy = input.MapReferrerPolicy,
ContainerStyleClass = input.MapContainerStyleClass,
FrameStyleClass = input.MapFrameStyleClass,
});
await _contactRepository.UpdateAsync(entity, autoSave: false);
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.ContactInfoTitleKey, input.ContactInfoTitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.AddressKey, input.AddressValue);
await UpsertLanguageTextAsync(input.CultureName, input.BankTitleKey, input.BankTitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.WorkHoursTitleKey, input.WorkHoursTitleValue);
await UpsertLanguageTextAsync(input.CultureName, input.WorkWeekdayKey, input.WorkWeekdayValue);
await UpsertLanguageTextAsync(input.CultureName, input.WorkWeekendKey, input.WorkWeekendValue);
await UpsertLanguageTextAsync(input.CultureName, input.WorkWhatsappKey, input.WorkWhatsappValue);
await UpsertLanguageTextAsync(input.CultureName, input.MapTitleKey, input.MapTitleValue);
foreach (var styleText in input.StyleTexts)
{
await UpsertLanguageTextAsync(input.CultureName, styleText.Key, styleText.Value);
}
await CurrentUnitOfWork!.SaveChangesAsync();
await _languageTextAppService.ClearRedisCacheAsync();
}
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();
}
} }

View file

@ -33,22 +33,6 @@ public partial class DemoToDemoDtoMapper : TwoWayMapperBase<Demo, DemoDto>
} }
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class ServiceToServiceDtoMapper : MapperBase<Service, ServiceDto>
{
public override partial ServiceDto Map(Service source);
public override partial void Map(Service source, ServiceDto destination);
public override void BeforeMap(Service source)
{
}
public override void AfterMap(Service source, ServiceDto destination)
{
}
}
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class BlogCategoryToBlogCategoryDtoMapper : MapperBase<BlogCategory, BlogCategoryDto> public partial class BlogCategoryToBlogCategoryDtoMapper : MapperBase<BlogCategory, BlogCategoryDto>
{ {
@ -173,54 +157,6 @@ public partial class OrderToOrderDtoMapper : MapperBase<Order, OrderDto>
} }
} }
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class AboutToAboutDtoMapper : MapperBase<About, AboutDto>
{
public override partial AboutDto Map(About source);
public override partial void Map(About source, AboutDto destination);
public override void BeforeMap(About source)
{
}
public override void AfterMap(About source, AboutDto destination)
{
}
}
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class HomeToHomeDtoMapper : MapperBase<Home, HomeDto>
{
public override partial HomeDto Map(Home source);
public override partial void Map(Home source, HomeDto destination);
public override void BeforeMap(Home source)
{
}
public override void AfterMap(Home source, HomeDto destination)
{
}
}
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class ContactToContactDtoMapper : MapperBase<Contact, ContactDto>
{
public override partial ContactDto Map(Contact source);
public override partial void Map(Contact source, ContactDto destination);
public override void BeforeMap(Contact source)
{
}
public override void AfterMap(Contact source, ContactDto destination)
{
}
}
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class CountryToCountryDtoMapper : MapperBase<Country, CountryDto> public partial class CountryToCountryDtoMapper : MapperBase<Country, CountryDto>
{ {

View file

@ -1194,6 +1194,12 @@
"en": "Web Site", "en": "Web Site",
"tr": "Web Site" "tr": "Web Site"
}, },
{
"resourceName": "Platform",
"key": "App.Public.Header",
"en": "Site Menu",
"tr": "Site Menüsü"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.IdentityManagement.ClaimType", "key": "App.IdentityManagement.ClaimType",
@ -7266,6 +7272,12 @@
"tr": "Vergi Dairesi", "tr": "Vergi Dairesi",
"en": "Tax Office" "en": "Tax Office"
}, },
{
"resourceName": "Platform",
"key": "App.PublicContact.WorkHours",
"en": "Working Hours",
"tr": "Çalışma Saatleri"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.PublicContact.TransferForm", "key": "App.PublicContact.TransferForm",
@ -8934,90 +8946,6 @@
"tr": "Müşteri Yorumları", "tr": "Müşteri Yorumları",
"en": "Testimonials" "en": "Testimonials"
}, },
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerTitle",
"tr": "Tasarımcı",
"en": "Designer"
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.IkonAnahtari",
"tr": "İkon Anahtarı",
"en": "Icon Key"
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.ShowPanel",
"tr": "Paneli Göster",
"en": "Show Panel"
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.NoSelection",
"tr": "Bir alan seçin",
"en": "Select a field"
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc1",
"tr": "İkon, Başlık, Açıklama ve Özellik listesini düzenleyin.",
"en": "Edit the icon, title, description, and feature list."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc2",
"tr": "Alt CTA metinlerini ve buton etiketini düzenleyin.",
"en": "Edit the sub-CTA texts and button label."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc3",
"tr": "Destek alanı başlığını ve buton metnini düzenleyin.",
"en": "Edit the support area title and button text."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc4",
"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": "App.PublicDesigner.DesignerDesc5",
"tr": "İletişim bilgilerini düzenleyin.",
"en": "Edit the contact information."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc6",
"tr": "Banka bilgilerini düzenleyin.",
"en": "Edit the bank information."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc7",
"tr": "Çalışma saatleri metinlerini düzenleyin.",
"en": "Edit the work hours information."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.DesignerDesc8",
"tr": "Harita başlığı ve iframe ayarlarını düzenleyin.",
"en": "Edit the map title and iframe settings."
},
{
"resourceName": "Platform",
"key": "App.PublicDesigner.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": "App.PublicDesigner.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", "resourceName": "Platform",
"key": "App.Listform.ListformField.DataSources", "key": "App.Listform.ListformField.DataSources",
@ -24078,12 +24006,6 @@
"en": "Blog post not found.", "en": "Blog post not found.",
"tr": "Blog yazısı bulunamadı." "tr": "Blog yazısı bulunamadı."
}, },
{
"resourceName": "Platform",
"key": "App.PublicDesigner.IconPlaceholder",
"en": "Example: {icon}",
"tr": "Örnek: {icon}"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.PublicPayment.CreateOrderError", "key": "App.PublicPayment.CreateOrderError",
@ -24240,6 +24162,60 @@
"en": "Date picker component", "en": "Date picker component",
"tr": "Tarih seçici bileşeni" "tr": "Tarih seçici bileşeni"
}, },
{
"resourceName": "Platform",
"key": "App.CatalogPublicHeaderBar.PublicHeaderBarDescription",
"en": "Public site header bar — swaps its classes when the page is scrolled",
"tr": "Public site üst bilgi çubuğu — sayfa kaydırıldığında sınıflarını değiştirir"
},
{
"resourceName": "Platform",
"key": "App.CatalogPublicNav.PublicNavDescription",
"en": "Public site menu links (Menu Manager, Pub module)",
"tr": "Public site menü bağlantıları (Menü Yöneticisi, Pub modülü)"
},
{
"resourceName": "Platform",
"key": "App.CatalogPublicMobileMenu.PublicMobileMenuDescription",
"en": "Mobile menu button and the panel it opens; the panel content is designed here",
"tr": "Mobil menü düğmesi ve açtığı panel; panelin içeriği burada tasarlanır"
},
{
"resourceName": "Platform",
"key": "App.CatalogPublicLogo.PublicLogoDescription",
"en": "Logo that follows the active theme",
"tr": "Aktif temayı izleyen logo"
},
{
"resourceName": "Platform",
"key": "App.CatalogPublicThemeToggle.PublicThemeToggleDescription",
"en": "Light/dark theme switch",
"tr": "Aydınlık/karanlık tema anahtarı"
},
{
"resourceName": "Platform",
"key": "App.CatalogPublicLanguageSelector.PublicLanguageSelectorDescription",
"en": "Language selector",
"tr": "Dil seçici"
},
{
"resourceName": "Platform",
"key": "App.CatalogPublicDemoButton.PublicDemoButtonDescription",
"en": "Button that opens the demo window",
"tr": "Demo penceresini açan düğme"
},
{
"resourceName": "Platform",
"key": "App.CatalogPlatformIcon.PlatformIconDescription",
"en": "Icon drawn by name (FaUser, FcHome …)",
"tr": "Ada göre çizilen ikon (FaUser, FcHome …)"
},
{
"resourceName": "Platform",
"key": "App.ComponentLink.LinkDescription",
"en": "Link (a) — navigates to the given address",
"tr": "Bağlantı (a) — verilen adrese gider"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.ComponentDiv.DivDescription", "key": "App.ComponentDiv.DivDescription",

View file

@ -7726,10 +7726,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
IsActive = true, IsActive = true,
AllowSearch = true, AllowSearch = true,
// Rota "/" ile baslamalidir; sunucu tarafinda da ayni kural dogrulanir. // Rota bos birakilabilir (rotasi olmayan bilesen); dolduruldugunda "/" ile
// baslamalidir. Ayni kural sunucu tarafinda da dogrulanir.
ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] { ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] {
new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) }, new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.pattern), Pattern = "^(/.*)?$", Message = "Route path must start with /" },
new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.pattern), Pattern = "^/.*", Message = "Route path must start with /" },
}), }),
ColumnCustomizationJson = DefaultColumnCustomizationJson, ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name), PermissionJson = DefaultFieldPermissionJson(listForm.Name),
@ -7781,8 +7781,11 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
SourceDbType = DbType.String, SourceDbType = DbType.String,
FieldName = "Dependencies", FieldName = "Dependencies",
EditorType2 = EditorTypes.dxTextBox,
CaptionName = "App.Platform.Dependencies", CaptionName = "App.Platform.Dependencies",
EditGroupOrderNo = 1,
EditOrderNo = 5,
EditorType2 = EditorTypes.dxTextBox,
ColSpan = 1,
Width = 0, Width = 0,
ListOrderNo = 6, ListOrderNo = 6,
Visible = true, Visible = true,

View file

@ -34,6 +34,7 @@ public class MenuSeedDto
public int Order { get; set; } public int Order { get; set; }
public string Url { get; set; } public string Url { get; set; }
public string Icon { get; set; } public string Icon { get; set; }
public string CssClass { get; set; }
public string RequiredPermissionName { get; set; } public string RequiredPermissionName { get; set; }
public bool IsDisabled { get; set; } public bool IsDisabled { get; set; }
public string ModuleId { get; set; } public string ModuleId { get; set; }
@ -120,6 +121,7 @@ public class MenuDataSeeder : IDataSeedContributor, ITransientDependency
Order = item.Order, Order = item.Order,
Url = item.Url, Url = item.Url,
Icon = item.Icon, Icon = item.Icon,
CssClass = item.CssClass,
RequiredPermissionName = item.RequiredPermissionName, RequiredPermissionName = item.RequiredPermissionName,
IsDisabled = item.IsDisabled, IsDisabled = item.IsDisabled,
ModuleId = item.ModuleId ModuleId = item.ModuleId

View file

@ -1,21 +1,5 @@
{ {
"Routes": [ "Routes": [
{
"key": "home",
"path": "/home",
"componentType": "normal",
"componentPath": "@/views/public/Home",
"routeType": "public",
"authority": []
},
{
"key": "about",
"path": "/about",
"componentType": "normal",
"componentPath": "@/views/public/About",
"routeType": "public",
"authority": []
},
{ {
"key": "products", "key": "products",
"path": "/products", "path": "/products",
@ -48,14 +32,6 @@
"routeType": "public", "routeType": "public",
"authority": [] "authority": []
}, },
{
"key": "services",
"path": "/services",
"componentType": "normal",
"componentPath": "@/views/public/Services",
"routeType": "public",
"authority": []
},
{ {
"key": "blog", "key": "blog",
"path": "/blog", "path": "/blog",
@ -80,14 +56,6 @@
"routeType": "public", "routeType": "public",
"authority": [] "authority": []
}, },
{
"key": "contact",
"path": "/contact",
"componentType": "normal",
"componentPath": "@/views/public/Contact",
"routeType": "public",
"authority": []
},
{ {
"key": "access-denied", "key": "access-denied",
"path": "/access-denied", "path": "/access-denied",
@ -370,38 +338,6 @@
"routeType": "protected", "routeType": "protected",
"authority": [] "authority": []
}, },
{
"key": "homeDesigner",
"path": "/admin/public/home/designer",
"componentType": "normal",
"componentPath": "@/views/public/Home",
"routeType": "protected",
"authority": ["App.Home"]
},
{
"key": "aboutDesigner",
"path": "/admin/public/about/designer",
"componentType": "normal",
"componentPath": "@/views/public/About",
"routeType": "protected",
"authority": ["App.About"]
},
{
"key": "servicesDesigner",
"path": "/admin/public/services/designer",
"componentType": "normal",
"componentPath": "@/views/public/Services",
"routeType": "protected",
"authority": ["App.Services"]
},
{
"key": "contactDesigner",
"path": "/admin/public/contact/designer",
"componentType": "normal",
"componentPath": "@/views/public/Contact",
"routeType": "protected",
"authority": ["App.Contact"]
},
{ {
"key": "admin.videoroom", "key": "admin.videoroom",
"path": "/admin/videoroom/dashboard", "path": "/admin/videoroom/dashboard",
@ -433,6 +369,9 @@
}, },
{ {
"Name": "Adm" "Name": "Adm"
},
{
"Name": "Pub"
} }
], ],
"Menus": [ "Menus": [
@ -590,39 +529,6 @@
"IsDisabled": false, "IsDisabled": false,
"ModuleId": "Sas" "ModuleId": "Sas"
}, },
{
"ParentCode": "App.Public",
"Code": "App.Home",
"DisplayName": "App.Home",
"Order": 0,
"Url": "/admin/public/home/designer",
"Icon": "FcHome",
"RequiredPermissionName": "App.Home",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Public",
"Code": "App.About",
"DisplayName": "App.About",
"Order": 1,
"Url": "/admin/public/about/designer",
"Icon": "FcAbout",
"RequiredPermissionName": "App.About",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.Public",
"Code": "App.Services",
"DisplayName": "App.Services",
"Order": 2,
"Url": "/admin/public/services/designer",
"Icon": "FcServices",
"RequiredPermissionName": "App.Services",
"IsDisabled": false,
"ModuleId": "Sas"
},
{ {
"ParentCode": "App.Public", "ParentCode": "App.Public",
"Code": "App.Orders.Products", "Code": "App.Orders.Products",
@ -700,17 +606,6 @@
"IsDisabled": false, "IsDisabled": false,
"ModuleId": "Sas" "ModuleId": "Sas"
}, },
{
"ParentCode": "App.Public",
"Code": "App.Contact",
"DisplayName": "App.Contact",
"Order": 10,
"Url": "/admin/public/contact/designer",
"Icon": "FcContacts",
"RequiredPermissionName": "App.Contact",
"IsDisabled": false,
"ModuleId": "Sas"
},
{ {
"ParentCode": "App.Saas", "ParentCode": "App.Saas",
"Code": "App.DevRequests", "Code": "App.DevRequests",
@ -1470,6 +1365,85 @@
"RequiredPermissionName": "App.ForumManagement.Publish", "RequiredPermissionName": "App.ForumManagement.Publish",
"IsDisabled": false, "IsDisabled": false,
"ModuleId": "Adm" "ModuleId": "Adm"
},
{
"ParentCode": null,
"Code": "App.Public.Header",
"DisplayName": "App.Public.Header",
"Order": 900,
"Url": null,
"Icon": "FcGlobe",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
},
{
"ParentCode": "App.Public.Header",
"Code": "App.Public.Header.Home",
"DisplayName": "App.Home",
"Order": 10,
"Url": "/home",
"Icon": "FaHome",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
},
{
"ParentCode": "App.Public.Header",
"Code": "App.Public.Header.About",
"DisplayName": "App.About",
"Order": 20,
"Url": "/about",
"Icon": "FaInfoCircle",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
},
{
"ParentCode": "App.Public.Header",
"Code": "App.Public.Header.Products",
"DisplayName": "App.Platform.Orders",
"Order": 40,
"Url": "/products",
"Icon": "FaBoxOpen",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
},
{
"ParentCode": "App.Public.Header",
"Code": "App.Public.Header.Blog",
"DisplayName": "App.Platform.BlogManagement",
"Order": 50,
"Url": "/blog",
"Icon": "FaBookOpen",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
},
{
"ParentCode": "App.Public.Header",
"Code": "App.Public.Header.Contact",
"DisplayName": "App.Contact",
"Order": 60,
"Url": "/contact",
"Icon": "FaPhoneAlt",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
},
{
"ParentCode": "App.Public.Header",
"Code": "App.Public.Header.Login",
"DisplayName": "App.PublicNav.Giris",
"Order": 900,
"Url": "/login",
"Icon": null,
"CssClass": "cta",
"RequiredPermissionName": null,
"IsDisabled": false,
"ModuleId": "Pub"
} }
] ]
} }

View file

@ -811,60 +811,6 @@
"MultiTenancySide": 2, "MultiTenancySide": 2,
"Modules": "Sas|Adm" "Modules": "Sas|Adm"
}, },
{
"GroupName": "App.Saas",
"Name": "App.Home",
"ParentName": "App.Public",
"DisplayName": "App.Home",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.Home.Design",
"ParentName": "App.Home",
"DisplayName": "Design",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.About",
"ParentName": "App.Public",
"DisplayName": "App.About",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.About.Design",
"ParentName": "App.About",
"DisplayName": "Design",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.Services",
"ParentName": "App.Public",
"DisplayName": "App.Services",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.Services.Design",
"ParentName": "App.Services",
"DisplayName": "Design",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{ {
"GroupName": "App.Saas", "GroupName": "App.Saas",
"Name": "App.Orders.Products", "Name": "App.Orders.Products",
@ -1315,24 +1261,6 @@
"MultiTenancySide": 2, "MultiTenancySide": 2,
"Modules": "Sas|Adm" "Modules": "Sas|Adm"
}, },
{
"GroupName": "App.Saas",
"Name": "App.Contact",
"ParentName": "App.Public",
"DisplayName": "App.Contact",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.Contact.Design",
"ParentName": "App.Contact",
"DisplayName": "Design",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{ {
"GroupName": "App.Saas", "GroupName": "App.Saas",
"Name": "App.DevRequests", "Name": "App.DevRequests",

View file

@ -55,9 +55,6 @@ public enum TableNameEnum
UomCategory, UomCategory,
Uom, Uom,
WorkHour, WorkHour,
About,
Home,
Service,
Product, Product,
PaymentMethod, PaymentMethod,
InstallmentOption, InstallmentOption,
@ -67,7 +64,6 @@ public enum TableNameEnum
BlogCategory, BlogCategory,
BlogPost, BlogPost,
Demo, Demo,
Contact,
Announcement, Announcement,
AnnouncementComment, AnnouncementComment,
AnnouncementLike, AnnouncementLike,

View file

@ -522,10 +522,6 @@ public static class PlatformConsts
public const string CrudEndpoints = Default + ".CrudEndpoints"; public const string CrudEndpoints = Default + ".CrudEndpoints";
} }
//Web Site
public const string Home = Prefix.App + ".Home";
public const string About = Prefix.App + ".About";
public const string Services = Prefix.App + ".Services";
public static class Orders public static class Orders
{ {
public const string Default = Prefix.App + ".Orders"; public const string Default = Prefix.App + ".Orders";
@ -544,19 +540,6 @@ public static class PlatformConsts
public const string BlogCategory = Default + ".Category"; public const string BlogCategory = Default + ".Category";
} }
public const string Demos = Prefix.App + ".Demos"; public const string Demos = Prefix.App + ".Demos";
public const string Contact = Prefix.App + ".Contact";
/// <summary>
/// Public sitedeki sayfa tasarim modu (`?design=1`) yetkileri.
/// Yetkiler <c>Migrations/PermissionsData.json</c> icinde tanimlanir ve seeder ile yonetilir.
/// </summary>
public static class WebSiteDesign
{
public const string Home = AppCodes.Home + ".Design";
public const string About = AppCodes.About + ".Design";
public const string Services = AppCodes.Services + ".Design";
public const string Contact = AppCodes.Contact + ".Design";
}
//Administration //Administration
public const string Administration = Prefix.App + ".Administration"; public const string Administration = Prefix.App + ".Administration";
@ -774,6 +757,23 @@ public static class PlatformConsts
public const string ColumnLookupTagBoxEditorApplyValueModeUseButtons = "useButtons"; public const string ColumnLookupTagBoxEditorApplyValueModeUseButtons = "useButtons";
} }
/// <summary>
/// Public site menusunun sozlesmesi. Menuler <see cref="ModuleId"/> modulu altinda
/// tutulur; <see cref="HeaderCode"/> altindaki kayitlar ust menuyu besler. Alt bilgi
/// menuden beslenmez, kendi Custom Component'inde tasarlanir.
/// </summary>
public static class PublicSite
{
public const string ModuleId = "Pub";
public const string HeaderCode = "App.Public.Header";
/// <summary>
/// Menunun <c>CssClass</c> alaninda bu belirtec varsa baglanti duz yazi yerine
/// vurgulu buton olarak cizilir (orn. "Giris").
/// </summary>
public const string CallToActionClass = "cta";
}
public static class Urls public static class Urls
{ {
public const string EmailConfirmation = "Account.EmailConfirmation"; public const string EmailConfirmation = "Account.EmailConfirmation";

View file

@ -52,9 +52,6 @@ public static class TableNameResolver
{ nameof(TableNameEnum.ForumCategory), (TablePrefix.TenantByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.ForumCategory), (TablePrefix.TenantByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.ForumTopic), (TablePrefix.TenantByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.ForumTopic), (TablePrefix.TenantByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.ForumPost), (TablePrefix.TenantByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.ForumPost), (TablePrefix.TenantByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Home), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.About), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Service), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Product), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.Product), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.PaymentMethod), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.PaymentMethod), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.InstallmentOption), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.InstallmentOption), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
@ -64,7 +61,6 @@ public static class TableNameResolver
{ nameof(TableNameEnum.BlogCategory), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.BlogCategory), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.BlogPost), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.BlogPost), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Demo), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.Demo), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Contact), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Route), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.Route), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Module), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.Module), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
{ nameof(TableNameEnum.Menu), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.Menu), (TablePrefix.PlatformByName, MenuPrefix.Sas) },

View file

@ -10,7 +10,7 @@ namespace Sozsoft.Platform.Data;
public interface IAfterAllMigrationsSqlExecutor public interface IAfterAllMigrationsSqlExecutor
{ {
/// <param name="tenantId"> /// <param name="tenantId">
/// null ise HostData/ExecuteData, aksi halde TenantData/ExecuteData klasörü işlenir. /// null ise host/, aksi halde tenants/{tenantId}/ kapsamındaki {sql|postgres}/execute klasörü işlenir.
/// </param> /// </param>
Task ExecuteAsync(Guid? tenantId = null); Task ExecuteAsync(Guid? tenantId = null);
} }

View file

@ -422,10 +422,6 @@ public static class SeedConsts
public const string Licence = Prefix.App + ".Licence"; public const string Licence = Prefix.App + ".Licence";
public const string LicenceItem = Prefix.App + ".LicenceItem"; public const string LicenceItem = Prefix.App + ".LicenceItem";
//Web Site
public const string Home = Prefix.App + ".Home";
public const string About = Prefix.App + ".About";
public const string Services = Prefix.App + ".Services";
public static class Orders public static class Orders
{ {
public const string Default = Prefix.App + ".Orders"; public const string Default = Prefix.App + ".Orders";
@ -444,19 +440,6 @@ public static class SeedConsts
public const string BlogCategory = Default + ".Category"; public const string BlogCategory = Default + ".Category";
} }
public const string Demos = Prefix.App + ".Demos"; public const string Demos = Prefix.App + ".Demos";
public const string Contact = Prefix.App + ".Contact";
/// <summary>
/// Public sitedeki sayfa tasarim modu (`?design=1`) yetkileri.
/// Yetkiler <c>Migrations/PermissionsData.json</c> icinde tanimlanir ve seeder ile yonetilir.
/// </summary>
public static class WebSiteDesign
{
public const string Home = AppCodes.Home + ".Design";
public const string About = AppCodes.About + ".Design";
public const string Services = AppCodes.Services + ".Design";
public const string Contact = AppCodes.Contact + ".Design";
}
public static class Menus public static class Menus
{ {

View file

@ -8,8 +8,8 @@ namespace Sozsoft.Platform.Data.Seeds;
/// <summary> /// <summary>
/// Runtime'da duzenlenebilen seed dosyalarinin klasorunu cozer. /// Runtime'da duzenlenebilen seed dosyalarinin klasorunu cozer.
/// <para> /// <para>
/// Kod ile birlikte versiyonlanan sabit seed'ler (ornegin TenantDataSeeder'in okudugu /// Kod ile birlikte versiyonlanan sabit seed'ler (DbMigrator projesindeki *Data.json dosyalari)
/// TenantData.json) buraya dahil degildir; onlar kendi projelerinin ciktisinda kalir. /// buraya dahil degildir; onlar kendi projelerinin ciktisinda kalir.
/// </para> /// </para>
/// <para> /// <para>
/// Klasor bilincli olarak hicbir projenin icinde degildir: depoda <c>configs/seeds</c> altinda durur ve /// Klasor bilincli olarak hicbir projenin icinde degildir: depoda <c>configs/seeds</c> altinda durur ve

View file

@ -6,7 +6,8 @@ namespace Sozsoft.Platform.Entities;
public class CustomComponent : FullAuditedEntity<Guid> public class CustomComponent : FullAuditedEntity<Guid>
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string RoutePath { get; set; } = string.Empty; /// <summary>Rotasi olmayan bilesende (ust bilgi, alt bilgi) NULL kalir; tekil indeks dolu degerleri kapsar.</summary>
public string? RoutePath { get; set; }
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;
public string? Props { get; set; } public string? Props { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
@ -21,7 +22,7 @@ public class CustomComponent : FullAuditedEntity<Guid>
/// </summary> /// </summary>
public string? DataSources { get; set; } public string? DataSources { get; set; }
public CustomComponent(string name, string routePath, string code, string? props, string? description, bool isActive, string? dependencies, string? dataSources = null) public CustomComponent(string name, string? routePath, string code, string? props, string? description, bool isActive, string? dependencies, string? dataSources = null)
{ {
Name = name; Name = name;
RoutePath = routePath; RoutePath = routePath;

View file

@ -1,11 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities;
public class About : FullAuditedEntity<Guid>
{
public string StatsJson { get; set; }
public string DescriptionsJson { get; set; }
public string SectionsJson { get; set; }
}

View file

@ -1,18 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities;
public class Contact : FullAuditedEntity<Guid>
{
public string Address { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Location { get; set; }
public long? TaxNumber { get; set; }
// JSON string kolonlar
public string BankJson { get; set; }
public string WorkHoursJson { get; set; }
public string MapJson { get; set; }
}

View file

@ -1,21 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities;
public class Home : FullAuditedEntity<Guid>
{
public string HeroBackgroundImageKey { get; set; }
public string HeroPrimaryCtaKey { get; set; }
public string HeroSecondaryCtaKey { get; set; }
public string FeaturesTitleKey { get; set; }
public string FeaturesSubtitleKey { get; set; }
public string SolutionsTitleKey { get; set; }
public string SolutionsSubtitleKey { get; set; }
public string CtaTitleKey { get; set; }
public string CtaSubtitleKey { get; set; }
public string CtaButtonLabelKey { get; set; }
public string SlidesJson { get; set; }
public string FeaturesJson { get; set; }
public string SolutionsJson { get; set; }
}

View file

@ -1,17 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities;
public class Service : FullAuditedEntity<Guid>
{
public string? Icon { get; set; }
public string Title { get; set; }
public string? Description { get; set; }
public string Type { get; set; }
// JSON olarak saklanacak
public string[] Features { get; set; } = [];
}

View file

@ -89,9 +89,6 @@ public class PlatformDbContext :
public DbSet<IdentityLinkUser> LinkUsers { get; set; } public DbSet<IdentityLinkUser> LinkUsers { get; set; }
public DbSet<IdentityUserDelegation> UserDelegations { get; set; } public DbSet<IdentityUserDelegation> UserDelegations { get; set; }
public DbSet<IdentitySession> Sessions { get; set; } public DbSet<IdentitySession> Sessions { get; set; }
public DbSet<About> Abouts { get; set; }
public DbSet<Home> Homes { get; set; }
public DbSet<Service> Services { get; set; }
public DbSet<Product> Products { get; set; } public DbSet<Product> Products { get; set; }
public DbSet<PaymentMethod> PaymentMethods { get; set; } public DbSet<PaymentMethod> PaymentMethods { get; set; }
public DbSet<InstallmentOption> InstallmentOptions { get; set; } public DbSet<InstallmentOption> InstallmentOptions { get; set; }
@ -100,7 +97,6 @@ public class PlatformDbContext :
public DbSet<BlogPost> BlogPosts { get; set; } public DbSet<BlogPost> BlogPosts { get; set; }
public DbSet<BlogCategory> BlogCategories { get; set; } public DbSet<BlogCategory> BlogCategories { get; set; }
public DbSet<Demo> Demos { get; set; } public DbSet<Demo> Demos { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<Sector> Sectors { get; set; } public DbSet<Sector> Sectors { get; set; }
public DbSet<SkillType> SkillTypes { get; set; } public DbSet<SkillType> SkillTypes { get; set; }
public DbSet<DevRequest> DevRequests { get; set; } public DbSet<DevRequest> DevRequests { get; set; }
@ -678,10 +674,10 @@ public class PlatformDbContext :
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(128); b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.RoutePath).IsRequired().HasMaxLength(512);
b.Property(x => x.Code).IsRequired(); b.Property(x => x.Code).IsRequired();
// The visual designer document contains user-facing text. Keep it Unicode so // The visual designer document contains user-facing text. Keep it Unicode so
// Turkish and other non-ASCII property values survive a database round-trip. // Turkish and other non-ASCII property values survive a database round-trip.
b.Property(x => x.RoutePath).HasMaxLength(512);
b.Property(x => x.Props).HasColumnType("nvarchar(max)"); b.Property(x => x.Props).HasColumnType("nvarchar(max)");
b.Property(x => x.Description).HasMaxLength(512); b.Property(x => x.Description).HasMaxLength(512);
b.Property(x => x.Dependencies).HasMaxLength(2048); b.Property(x => x.Dependencies).HasMaxLength(2048);
@ -689,7 +685,9 @@ public class PlatformDbContext :
b.Property(x => x.DataSources).HasColumnType("nvarchar(max)"); b.Property(x => x.DataSources).HasColumnType("nvarchar(max)");
b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0"); b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
b.HasIndex(x => new { x.RoutePath }).IsUnique().HasFilter("[IsDeleted] = 0"); // Rotasi olmayan bilesen (alt bilgi, ust bilgi) `RoutePath` tasimaz; tekil indeks
// yalnizca dolu degerleri kapsar, aksi halde her rotasiz bilesen digeriyle carpisir.
b.HasIndex(x => new { x.RoutePath }).IsUnique().HasFilter("[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
}); });
builder.Entity<ReportCategory>(b => builder.Entity<ReportCategory>(b =>
@ -957,64 +955,6 @@ public class PlatformDbContext :
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
//Web Site
builder.Entity<About>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.About)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.StatsJson).HasColumnType("nvarchar(max)");
b.Property(x => x.DescriptionsJson).HasColumnType("nvarchar(max)");
b.Property(x => x.SectionsJson).HasColumnType("nvarchar(max)");
});
builder.Entity<Home>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Home)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.HeroBackgroundImageKey).HasMaxLength(256);
b.Property(x => x.HeroPrimaryCtaKey).HasMaxLength(256);
b.Property(x => x.HeroSecondaryCtaKey).HasMaxLength(256);
b.Property(x => x.FeaturesTitleKey).HasMaxLength(256);
b.Property(x => x.FeaturesSubtitleKey).HasMaxLength(256);
b.Property(x => x.SolutionsTitleKey).HasMaxLength(256);
b.Property(x => x.SolutionsSubtitleKey).HasMaxLength(256);
b.Property(x => x.CtaTitleKey).HasMaxLength(256);
b.Property(x => x.CtaSubtitleKey).HasMaxLength(256);
b.Property(x => x.CtaButtonLabelKey).HasMaxLength(256);
b.Property(x => x.SlidesJson).HasColumnType("nvarchar(max)");
b.Property(x => x.FeaturesJson).HasColumnType("nvarchar(max)");
b.Property(x => x.SolutionsJson).HasColumnType("nvarchar(max)");
});
builder.Entity<Service>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Service)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Title).IsRequired().HasMaxLength(128);
b.Property(x => x.Description).HasMaxLength(512);
b.Property(x => x.Icon).HasMaxLength(64);
b.Property(x => x.Type).HasMaxLength(64);
b.Property(x => x.Features)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<string[]>(v, (JsonSerializerOptions)null)
)
.Metadata
.SetValueComparer(
new ValueComparer<string[]>(
(a, b) => a.SequenceEqual(b),
a => a.Aggregate(0, (c, v) => HashCode.Combine(c, v.GetHashCode())),
a => a.ToArray()
)
);
b.HasIndex(x => new { x.Title }).IsUnique().HasFilter("[IsDeleted] = 0");
});
builder.Entity<Product>(b => builder.Entity<Product>(b =>
{ {
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Product)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Product)), Prefix.DbSchema);
@ -1165,21 +1105,6 @@ public class PlatformDbContext :
b.Property(x => x.Message).IsRequired().HasMaxLength(2048); b.Property(x => x.Message).IsRequired().HasMaxLength(2048);
}); });
builder.Entity<Contact>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Contact)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Address).HasMaxLength(512);
b.Property(x => x.PhoneNumber).HasMaxLength(20);
b.Property(x => x.Email).HasMaxLength(128);
b.Property(x => x.Location).HasMaxLength(256);
b.Property(x => x.BankJson).HasColumnType("nvarchar(max)");
b.Property(x => x.WorkHoursJson).HasColumnType("nvarchar(max)");
b.Property(x => x.MapJson).HasColumnType("nvarchar(max)");
});
builder.Entity<LogEntry>(b => builder.Entity<LogEntry>(b =>
{ {
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.LogEntry)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.LogEntry)), Prefix.DbSchema);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260904084415_Initial")] [Migration("20260908125150_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -630,55 +630,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_H_NotificationType", (string)null); b.ToTable("Sas_H_NotificationType", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.About", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("DescriptionsJson")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("SectionsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("StatsJson")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Sas_H_About", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Agenda", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Agenda", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -1505,74 +1456,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_H_City", (string)null); b.ToTable("Sas_H_City", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.Contact", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Address")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("BankJson")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Email")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Location")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("MapJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<long?>("TaxNumber")
.HasColumnType("bigint");
b.Property<string>("WorkHoursJson")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Sas_H_Contact", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ContactTitle", b => modelBuilder.Entity("Sozsoft.Platform.Entities.ContactTitle", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -1962,7 +1845,6 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("RoutePath") b.Property<string>("RoutePath")
.IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("nvarchar(512)"); .HasColumnType("nvarchar(512)");
@ -1974,7 +1856,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("RoutePath") b.HasIndex("RoutePath")
.IsUnique() .IsUnique()
.HasFilter("[IsDeleted] = 0"); .HasFilter("[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
b.ToTable("Sas_H_CustomComponent", (string)null); b.ToTable("Sas_H_CustomComponent", (string)null);
}); });
@ -2957,95 +2839,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_T_GlobalSearch", (string)null); b.ToTable("Sas_T_GlobalSearch", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.Home", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("CtaButtonLabelKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("CtaSubtitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("CtaTitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("FeaturesJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("FeaturesSubtitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("FeaturesTitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("HeroBackgroundImageKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("HeroPrimaryCtaKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("HeroSecondaryCtaKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("SlidesJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SolutionsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SolutionsSubtitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("SolutionsTitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.ToTable("Sas_H_Home", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.InstallmentOption", b => modelBuilder.Entity("Sozsoft.Platform.Entities.InstallmentOption", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -5087,70 +4880,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_Sector", (string)null); b.ToTable("Adm_T_Sector", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.Service", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("Features")
.HasColumnType("nvarchar(max)");
b.Property<string>("Icon")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("Title")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Sas_H_Service", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Skill", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Skill", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")

View file

@ -922,27 +922,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Sas_H_About",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
StatsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
DescriptionsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
SectionsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_H_About", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_H_AiBot", name: "Sas_H_AiBot",
columns: table => new columns: table => new
@ -1059,32 +1038,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Sas_H_ChangeLog", x => x.Id); table.PrimaryKey("PK_Sas_H_ChangeLog", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Sas_H_Contact",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Address = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Email = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Location = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
TaxNumber = table.Column<long>(type: "bigint", nullable: true),
BankJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
WorkHoursJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
MapJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_H_Contact", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_H_ContactTitle", name: "Sas_H_ContactTitle",
columns: table => new columns: table => new
@ -1178,7 +1131,7 @@ namespace Sozsoft.Platform.Migrations
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false), Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
RoutePath = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false), RoutePath = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
Code = table.Column<string>(type: "nvarchar(max)", nullable: false), Code = table.Column<string>(type: "nvarchar(max)", nullable: false),
Props = table.Column<string>(type: "nvarchar(max)", nullable: true), Props = table.Column<string>(type: "nvarchar(max)", nullable: true),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true), Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
@ -1361,37 +1314,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Sas_H_DynamicService", x => x.Id); table.PrimaryKey("PK_Sas_H_DynamicService", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Sas_H_Home",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
HeroBackgroundImageKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
HeroPrimaryCtaKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
HeroSecondaryCtaKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
FeaturesTitleKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
FeaturesSubtitleKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SolutionsTitleKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SolutionsSubtitleKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CtaTitleKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CtaSubtitleKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CtaButtonLabelKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
SlidesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
FeaturesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
SolutionsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_H_Home", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_H_InstallmentOption", name: "Sas_H_InstallmentOption",
columns: table => new columns: table => new
@ -1725,29 +1647,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Sas_H_Route", x => x.Id); table.PrimaryKey("PK_Sas_H_Route", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Sas_H_Service",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Icon = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
Title = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
Features = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_H_Service", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_H_SettingDefinition", name: "Sas_H_SettingDefinition",
columns: table => new columns: table => new
@ -4167,7 +4066,7 @@ namespace Sozsoft.Platform.Migrations
table: "Sas_H_CustomComponent", table: "Sas_H_CustomComponent",
column: "RoutePath", column: "RoutePath",
unique: true, unique: true,
filter: "[IsDeleted] = 0"); filter: "[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sas_H_CustomEndpoint_Name", name: "IX_Sas_H_CustomEndpoint_Name",
@ -4322,13 +4221,6 @@ namespace Sozsoft.Platform.Migrations
unique: true, unique: true,
filter: "[IsDeleted] = 0"); filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_Service_Title",
table: "Sas_H_Service",
column: "Title",
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sas_H_Skill_SkillTypeId_Name", name: "IX_Sas_H_Skill_SkillTypeId_Name",
table: "Sas_H_Skill", table: "Sas_H_Skill",
@ -4562,9 +4454,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "OpenIddictTokens"); name: "OpenIddictTokens");
migrationBuilder.DropTable(
name: "Sas_H_About");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_AiBot"); name: "Sas_H_AiBot");
@ -4580,9 +4469,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_ChangeLog"); name: "Sas_H_ChangeLog");
migrationBuilder.DropTable(
name: "Sas_H_Contact");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_ContactTitle"); name: "Sas_H_ContactTitle");
@ -4616,9 +4502,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_DynamicService"); name: "Sas_H_DynamicService");
migrationBuilder.DropTable(
name: "Sas_H_Home");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_InstallmentOption"); name: "Sas_H_InstallmentOption");
@ -4658,9 +4541,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_Route"); name: "Sas_H_Route");
migrationBuilder.DropTable(
name: "Sas_H_Service");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_H_SettingDefinition"); name: "Sas_H_SettingDefinition");

View file

@ -627,55 +627,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_H_NotificationType", (string)null); b.ToTable("Sas_H_NotificationType", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.About", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("DescriptionsJson")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("SectionsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("StatsJson")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Sas_H_About", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Agenda", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Agenda", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -1502,74 +1453,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_H_City", (string)null); b.ToTable("Sas_H_City", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.Contact", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Address")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("BankJson")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Email")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Location")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("MapJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<long?>("TaxNumber")
.HasColumnType("bigint");
b.Property<string>("WorkHoursJson")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Sas_H_Contact", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ContactTitle", b => modelBuilder.Entity("Sozsoft.Platform.Entities.ContactTitle", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -1959,7 +1842,6 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("RoutePath") b.Property<string>("RoutePath")
.IsRequired()
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("nvarchar(512)"); .HasColumnType("nvarchar(512)");
@ -1971,7 +1853,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("RoutePath") b.HasIndex("RoutePath")
.IsUnique() .IsUnique()
.HasFilter("[IsDeleted] = 0"); .HasFilter("[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
b.ToTable("Sas_H_CustomComponent", (string)null); b.ToTable("Sas_H_CustomComponent", (string)null);
}); });
@ -2954,95 +2836,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_T_GlobalSearch", (string)null); b.ToTable("Sas_T_GlobalSearch", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.Home", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("CtaButtonLabelKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("CtaSubtitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("CtaTitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("FeaturesJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("FeaturesSubtitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("FeaturesTitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("HeroBackgroundImageKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("HeroPrimaryCtaKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("HeroSecondaryCtaKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("SlidesJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SolutionsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("SolutionsSubtitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("SolutionsTitleKey")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.ToTable("Sas_H_Home", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.InstallmentOption", b => modelBuilder.Entity("Sozsoft.Platform.Entities.InstallmentOption", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -5084,70 +4877,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_Sector", (string)null); b.ToTable("Adm_T_Sector", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.Service", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("Features")
.HasColumnType("nvarchar(max)");
b.Property<string>("Icon")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("Title")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Sas_H_Service", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Skill", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Skill", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")

View file

@ -1,360 +0,0 @@
{
"Homes": [
{
"heroBackgroundImageKey": "Public.home.hero.backgroundImage",
"heroPrimaryCtaKey": "App.HeroCta.Consultation",
"heroSecondaryCtaKey": "App.HeroCta.Discover",
"featuresTitleKey": "App.PublicFeatures.FeaturesTitle",
"featuresSubtitleKey": "App.PublicFeatures.Subtitle",
"solutionsTitleKey": "App.PublicSolutions.SolutionsTitle",
"solutionsSubtitleKey": "App.PublicSolutions.Subtitle",
"ctaTitleKey": "App.PublicCommon.GetStarted",
"ctaSubtitleKey": "App.PublicCommon.Contact",
"ctaButtonLabelKey": "App.PublicCommon.LearnMore",
"slides": [
{
"titleKey": "App.HeroSlide1.Slide1Title",
"subtitleKey": "App.HeroSlide1.Subtitle",
"services": [
{
"icon": "FaCalendarAlt",
"titleKey": "App.Slide1Service1.Service1Title",
"descriptionKey": "App.Slide1Service1.Service1Desc"
},
{
"icon": "FaUsers",
"titleKey": "App.Slide1Service2.Service2Title",
"descriptionKey": "App.Slide1Service2.Service2Desc"
},
{
"icon": "FaShieldAlt",
"titleKey": "App.Slide1Service3.Service3Title",
"descriptionKey": "App.Slide1Service3.Service3Desc"
}
]
},
{
"titleKey": "App.HeroSlide2.Slide2Title",
"subtitleKey": "App.HeroSlide2.Subtitle",
"services": [
{
"icon": "FaChartBar",
"titleKey": "App.Slide2Service1.Service1Title",
"descriptionKey": "App.Slide2Service1.Service1Desc"
},
{
"icon": "FaCreditCard",
"titleKey": "App.Slide2Service2.Service2Title",
"descriptionKey": "App.Slide2Service2.Service2Desc"
},
{
"icon": "FaDatabase",
"titleKey": "App.Slide2Service3.Service3Title",
"descriptionKey": "App.Slide2Service3.Service3Desc"
}
]
},
{
"titleKey": "App.HeroSlide3.Slide3Title",
"subtitleKey": "App.HeroSlide3.Subtitle",
"services": [
{
"icon": "FaDesktop",
"titleKey": "App.Slide3Service1.Service1Title",
"descriptionKey": "App.Slide3Service1.Service1Desc"
},
{
"icon": "FaServer",
"titleKey": "App.Slide3Service2.Service2Title",
"descriptionKey": "App.Slide3Service2.Service2Desc"
},
{
"icon": "FaMobileAlt",
"titleKey": "App.Slide3Service3.Service3Title",
"descriptionKey": "App.Slide3Service3.Service3Desc"
}
]
}
],
"features": [
{
"icon": "FaUsers",
"titleKey": "App.PublicFeatures.Reliable",
"descriptionKey": "App.FeaturesReliable.ReliableDesc"
},
{
"icon": "FaCalendarAlt",
"titleKey": "App.VideoRoom.Planning",
"descriptionKey": "App.FeaturesRapid.RapidDesc"
},
{
"icon": "FaBookOpen",
"titleKey": "App.PublicFeatures.Expert",
"descriptionKey": "App.FeaturesExpert.ExpertDesc"
},
{
"icon": "FaCreditCard",
"titleKey": "App.PublicFeatures.Muhasebe",
"descriptionKey": "App.FeaturesMuhasebe.MuhasebeDesc"
},
{
"icon": "FaRegComment",
"titleKey": "App.PublicFeatures.Iletisim",
"descriptionKey": "App.FeaturesIletisim.IletisimDesc"
},
{
"icon": "FaPhone",
"titleKey": "App.PublicFeatures.Mobil",
"descriptionKey": "App.FeaturesMobil.MobilDesc"
},
{
"icon": "FaChartBar",
"titleKey": "App.PublicFeatures.Scalable",
"descriptionKey": "App.FeaturesScalable.ScalableDesc"
},
{
"icon": "FaShieldAlt",
"titleKey": "App.Listform.ListformField.Security",
"descriptionKey": "App.FeaturesGuvenlik.GuvenlikDesc"
}
],
"solutions": [
{
"icon": "FaDesktop",
"colorClass": "bg-blue-600",
"titleKey": "App.BlogCategories.Webdev",
"descriptionKey": "App.SolutionsWeb.WebDesc"
},
{
"icon": "FaMobileAlt",
"colorClass": "bg-purple-600",
"titleKey": "App.ServicesMobile.MobileTitle",
"descriptionKey": "App.SolutionsMobile.MobileDesc"
},
{
"icon": "FaServer",
"colorClass": "bg-green-600",
"titleKey": "App.SolutionsCustom.CustomTitle",
"descriptionKey": "App.SolutionsCustom.CustomDesc"
},
{
"icon": "FaDatabase",
"colorClass": "bg-red-600",
"titleKey": "App.SolutionsDatabase.DatabaseTitle",
"descriptionKey": "App.SolutionsDatabase.DatabaseDesc"
}
]
}
],
"Abouts": [
{
"stats": [
{
"Icon": "FaUser",
"Value": "740\u002B",
"LabelKey": "App.AboutStats.Users",
"UseCounter": true,
"CounterEnd": "740\u002B",
"CounterSuffix": "\u002B",
"CounterDuration": 3000
},
{
"icon": "FaUsers",
"value": "310+",
"labelKey": "App.AboutStats.Clients",
"useCounter": true,
"counterEnd": "310+",
"counterSuffix": "+",
"counterDuration": 2500
},
{
"icon": "FaAward",
"value": "25",
"labelKey": "App.AboutStats.Experience",
"useCounter": true,
"counterEnd": "20",
"counterSuffix": "+",
"counterDuration": 2000
},
{
"icon": "FaClock",
"value": "7/24",
"labelKey": "App.AboutStats.Support",
"useCounter": false
},
{
"icon": "FaGlobe",
"value": "3",
"labelKey": "App.Definitions.Country",
"useCounter": true,
"counterEnd": "3",
"counterDuration": 1500
}
],
"descriptions": [
"App.AboutDescription.Part1",
"App.AboutDescription.Motto",
"App.AboutDescription.Part2",
"App.AboutDescription.Closing"
],
"sections": [
{
"key": "App.PublicAbout.Mission",
"descKey": "App.AboutMission.MissionDesc"
},
{
"key": "App.PublicAbout.Vision",
"descKey": "App.AboutVision.VisionDesc"
}
]
}
],
"Services": [
{
"icon": "FaCode",
"title": "App.ServicesSoftware.SoftwareTitle",
"description": "App.ServicesSoftware.SoftwareDesc",
"type": "service",
"features": [
"App.SoftwareFeatures.Analysis",
"App.SoftwareFeatures.Design",
"App.SoftwareFeatures.Development",
"App.SoftwareFeatures.Testing",
"App.SoftwareFeatures.Maintenance"
]
},
{
"icon": "FaUsers",
"title": "App.BlogCategories.Webdev",
"description": "App.ServicesWeb.WebDesc",
"type": "service",
"features": [
"App.WebFeatures.Frontend",
"App.WebFeatures.Backend",
"App.WebFeatures.Api",
"App.WebFeatures.Seo",
"App.WebFeatures.Performance"
]
},
{
"icon": "FaShieldAlt",
"title": "App.ServicesMobile.MobileTitle",
"description": "App.ServicesMobile.MobileDesc",
"type": "service",
"features": [
"App.MobileFeatures.Design",
"App.MobileFeatures.Native",
"App.MobileFeatures.Cross",
"App.MobileFeatures.Push",
"App.MobileFeatures.Store"
]
},
{
"icon": "FaServer",
"title": "App.ServicesDatabase.DatabaseTitle",
"description": "App.ServicesDatabase.DatabaseDesc",
"type": "service",
"features": [
"App.DatabaseFeatures.Design",
"App.DatabaseFeatures.Optimization",
"App.DatabaseFeatures.Migration",
"App.DatabaseFeatures.Backup",
"App.DatabaseFeatures.Recovery"
]
},
{
"icon": "FaGlobe",
"title": "App.ServicesIntegration.IntegrationTitle",
"description": "App.ServicesIntegration.IntegrationDesc",
"type": "service",
"features": [
"App.IntegrationFeatures.Api",
"App.IntegrationFeatures.Middleware",
"App.IntegrationFeatures.Legacy",
"App.IntegrationFeatures.Realtime",
"App.IntegrationFeatures.Monitoring"
]
},
{
"icon": "FaCog",
"title": "App.ServicesConsulting.ConsultingTitle",
"description": "App.ServicesConsulting.ConsultingDesc",
"type": "service",
"features": [
"App.ConsultingFeatures.Tech",
"App.ConsultingFeatures.Project",
"App.ConsultingFeatures.Digital",
"App.ConsultingFeatures.Risk",
"App.ConsultingFeatures.Training"
]
},
{
"icon": "FaUsers",
"title": "App.SupportBranchRemote.BranchRemoteTitle",
"description": null,
"type": "support",
"features": [
"App.BranchRemoteFeatures.Priority",
"App.BranchRemoteFeatures.Remote",
"App.BranchRemoteFeatures.Optimization",
"App.BranchRemoteFeatures.Maintenance",
"App.BranchRemoteFeatures.Consulting"
]
},
{
"icon": "FaServer",
"title": "App.SupportBackup.BackupTitle",
"description": null,
"type": "support",
"features": [
"App.BackupFeatures.Daily",
"App.BackupFeatures.Encrypted",
"App.BackupFeatures.Recovery",
"App.BackupFeatures.Verification",
"App.BackupFeatures.Access"
]
},
{
"icon": "FaGlobe",
"title": "App.SupportSms.SmsTitle",
"description": null,
"type": "support",
"features": [
"App.SmsFeatures.Packages",
"App.SmsFeatures.Bulk",
"App.SmsFeatures.Template",
"App.SmsFeatures.Reporting",
"App.SmsFeatures.Api"
]
}
],
"Contacts": [
{
"address": "App.ContactAddress.Full",
"phoneNumber": "5447697638",
"email": "destek@sozsoft.com",
"location": "Sarıgazi",
"taxNumber": "32374982750",
"bank": {
"accountHolder": "Özlem Öztürk",
"branch": "03663 / Enpara",
"accountNumber": "73941177",
"iban": "TR11 0015 7000 0000 0073 9411 77"
},
"workHour": {
"weekday": "App.ContactWorkHours.Weekday",
"weekend": "App.ContactWorkHours.Weekend",
"whatsapp": "App.ContactWorkHours.Whatsapp"
},
"map": {
"title": "App.PublicContact.Location",
"src": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3006.209566407676!2d28.757000999999992!3d41.10811400000001!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x8a0bbbdfcfd3fd24!2zU8O2enNvZnQ!5e0!3m2!1str!2str!4v1450816303558",
"width": "100%",
"height": "700",
"allowFullScreen": true,
"loading": "lazy",
"referrerPolicy": "no-referrer-when-downgrade"
}
}
]
}

View file

@ -1,238 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Forum;
using Sozsoft.Platform.Public;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
namespace Sozsoft.Platform.Data.Seeds;
public class TenantSeederDto
{
//Saas
//Definitions
//Public
public List<HomeSeedDto> Homes { get; set; }
public List<AboutSeedDto> Abouts { get; set; }
public List<ServiceSeedDto> Services { get; set; }
public List<ContactSeedDto> Contacts { get; set; }
//Forum
//Intranet
}
public class ServiceSeedDto
{
public string Icon { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Type { get; set; }
public string[] Features { get; set; }
}
public class AboutSeedDto
{
public List<StatDto> Stats { get; set; }
public List<string> Descriptions { get; set; }
public List<SectionDto> Sections { get; set; }
}
public class HomeSeedDto
{
public string HeroBackgroundImageKey { get; set; }
public string HeroPrimaryCtaKey { get; set; }
public string HeroSecondaryCtaKey { get; set; }
public string FeaturesTitleKey { get; set; }
public string FeaturesSubtitleKey { get; set; }
public string SolutionsTitleKey { get; set; }
public string SolutionsSubtitleKey { get; set; }
public string CtaTitleKey { get; set; }
public string CtaSubtitleKey { get; set; }
public string CtaButtonLabelKey { get; set; }
public List<HomeSlideSeedDto> Slides { get; set; }
public List<HomeFeatureSeedDto> Features { get; set; }
public List<HomeSolutionSeedDto> Solutions { get; set; }
}
public class HomeSlideSeedDto
{
public string TitleKey { get; set; }
public string SubtitleKey { get; set; }
public string StyleClass { get; set; }
public List<HomeSlideServiceSeedDto> Services { get; set; }
}
public class HomeSlideServiceSeedDto
{
public string Icon { get; set; }
public string TitleKey { get; set; }
public string DescriptionKey { get; set; }
public string StyleClass { get; set; }
}
public class HomeFeatureSeedDto
{
public string Icon { get; set; }
public string TitleKey { get; set; }
public string DescriptionKey { get; set; }
public string StyleClass { get; set; }
}
public class HomeSolutionSeedDto
{
public string Icon { get; set; }
public string ColorClass { get; set; }
public string TitleKey { get; set; }
public string DescriptionKey { get; set; }
public string StyleClass { get; set; }
}
public class ContactSeedDto
{
public string Address { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Location { get; set; }
public long TaxNumber { get; set; }
public BankDto Bank { get; set; }
public WorkHoursDto WorkHour { get; set; }
public MapDto Map { get; set; }
}
public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
{
private readonly ILookupNormalizer _lookupNormalizer;
private readonly IRepository<Branch, Guid> _branchRepository;
private readonly IRepository<Home, Guid> _homeRepository;
private readonly IRepository<About, Guid> _aboutRepository;
private readonly IRepository<Service, Guid> _servicesRepository;
private readonly IRepository<Contact, Guid> _contactRepository;
private readonly IIdentityUserRepository _repositoryUser;
public TenantDataSeeder(
ILookupNormalizer lookupNormalizer,
IIdentityUserRepository repositoryUser,
IRepository<Branch, Guid> branchRepository,
IRepository<Home, Guid> homeRepository,
IRepository<About, Guid> aboutRepository,
IRepository<Service, Guid> servicesRepository,
IRepository<Contact, Guid> contactRepository
)
{
_lookupNormalizer = lookupNormalizer;
_repositoryUser = repositoryUser;
_branchRepository = branchRepository;
_homeRepository = homeRepository;
_servicesRepository = servicesRepository;
_aboutRepository = aboutRepository;
_contactRepository = contactRepository;
}
public async Task SeedAsync(DataSeedContext context)
{
var assemblyLocation = Path.GetDirectoryName(typeof(TenantDataSeeder).Assembly.Location)!;
var configuration = new ConfigurationBuilder()
.SetBasePath(assemblyLocation)
.AddJsonFile(Path.Combine("Seeds", "TenantData.json"), optional: false, reloadOnChange: false)
.AddJsonFile(Path.Combine("Seeds", $"TenantData.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? ""}.json"), optional: true)
.Build();
var items = configuration.Get<TenantSeederDto>();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
foreach (var item in items.Services)
{
var exists = await _servicesRepository.AnyAsync(x => x.Title == item.Title);
if (!exists)
{
await _servicesRepository.InsertAsync(new Service
{
Icon = item.Icon,
Title = item.Title,
Description = item.Description,
Type = item.Type,
Features = item.Features
});
}
}
foreach (var item in items.Abouts)
{
var exists = await _aboutRepository.FirstOrDefaultAsync();
if (exists == null)
{
await _aboutRepository.InsertAsync(new About
{
StatsJson = JsonSerializer.Serialize(item.Stats),
DescriptionsJson = JsonSerializer.Serialize(item.Descriptions),
SectionsJson = JsonSerializer.Serialize(item.Sections)
});
}
}
foreach (var item in items.Homes)
{
var exists = await _homeRepository.FirstOrDefaultAsync();
if (exists == null)
{
await _homeRepository.InsertAsync(new Home
{
HeroBackgroundImageKey = item.HeroBackgroundImageKey,
HeroPrimaryCtaKey = item.HeroPrimaryCtaKey,
HeroSecondaryCtaKey = item.HeroSecondaryCtaKey,
FeaturesTitleKey = item.FeaturesTitleKey,
FeaturesSubtitleKey = item.FeaturesSubtitleKey,
SolutionsTitleKey = item.SolutionsTitleKey,
SolutionsSubtitleKey = item.SolutionsSubtitleKey,
CtaTitleKey = item.CtaTitleKey,
CtaSubtitleKey = item.CtaSubtitleKey,
CtaButtonLabelKey = item.CtaButtonLabelKey,
SlidesJson = JsonSerializer.Serialize(item.Slides),
FeaturesJson = JsonSerializer.Serialize(item.Features),
SolutionsJson = JsonSerializer.Serialize(item.Solutions)
});
}
}
foreach (var item in items.Contacts)
{
var exists = await _contactRepository.FirstOrDefaultAsync();
if (exists == null)
{
await _contactRepository.InsertAsync(new Contact
{
Address = item.Address,
PhoneNumber = item.PhoneNumber,
Email = item.Email,
Location = item.Location,
TaxNumber = item.TaxNumber,
BankJson = JsonSerializer.Serialize(item.Bank),
WorkHoursJson = JsonSerializer.Serialize(item.WorkHour),
MapJson = JsonSerializer.Serialize(item.Map)
});
}
}
}
}

View file

@ -7,22 +7,6 @@
<RootNamespace>Sozsoft.Platform</RootNamespace> <RootNamespace>Sozsoft.Platform</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Seeds\TenantData.json" />
<Content Include="Seeds\TenantData.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\TenantData.Dev.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\TenantData.Production.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EFCore.BulkExtensions" Version="10.0.1" /> <PackageReference Include="EFCore.BulkExtensions" Version="10.0.1" />
<ProjectReference Include="..\..\modules\Sozsoft.Languages\Sozsoft.Languages.EntityFrameworkCore\Sozsoft.Languages.EntityFrameworkCore.csproj" /> <ProjectReference Include="..\..\modules\Sozsoft.Languages\Sozsoft.Languages.EntityFrameworkCore\Sozsoft.Languages.EntityFrameworkCore.csproj" />

View file

@ -53,6 +53,28 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
context.ApplicationConfiguration.SetProperty( context.ApplicationConfiguration.SetProperty(
"customComponents", await customComponentRepository.GetListAsync()); "customComponents", await customComponentRepository.GetListAsync());
// Public site menusu: ziyaretci menu ucunu cagiramaz ([Authorize]), bu yuzden
// rotalar ve custom component'ler gibi anonim erisilen konfigurasyonla tasinir.
var menuRepository = context.ServiceProvider.GetRequiredService<IRepository<Menu, Guid>>();
var publicMenus = await menuRepository.GetListAsync(
menu => menu.ModuleId == PlatformConsts.PublicSite.ModuleId && !menu.IsDisabled);
context.ApplicationConfiguration.SetProperty("publicMenus", publicMenus
.OrderBy(menu => menu.Order)
.Select(menu => new
{
code = menu.Code,
displayName = menu.DisplayName,
url = menu.Url,
icon = menu.Icon,
order = menu.Order,
parentCode = menu.ParentCode,
cssClass = menu.CssClass,
target = menu.Target,
requiredPermissionName = menu.RequiredPermissionName
})
.ToList());
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>(); var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
if (currentUser.Id.HasValue) if (currentUser.Id.HasValue)
{ {

View file

@ -184,6 +184,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
`permission.constant.ts` + `LanguagesData.json`. `permission.constant.ts` + `LanguagesData.json`.
- **Yeni bir dil anahtarı**`LanguagesData.json`; eklemeden önce anahtarın zaten var olup - **Yeni bir dil anahtarı**`LanguagesData.json`; eklemeden önce anahtarın zaten var olup
olmadığını kontrol et. olmadığını kontrol et.
- **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca
seed dosyasını görür. Seed dosyası `ui/` dışında olduğu için aynı sınıflar `ui/safelist.txt`
sonuna da yazılır (`twSafelistGenerator`, derlemede otomatik); yoksa uygulama sunucuda stilsiz
kalır. İkinci bir safelist dosyası açma.
- **Custom component kaydı/silinmesi**`data/App.DeveloperKit.CustomComponents.json` - **Custom component kaydı/silinmesi**`data/App.DeveloperKit.CustomComponents.json`
(Custom Components ekranının seed verisi); bileşenin kendi dosyası yoktur, her bileşen bu (Custom Components ekranının seed verisi); bileşenin kendi dosyası yoktur, her bileşen bu
dosyada bir satırdır. Hem listeden hem Visual Designer'dan (`CustomComponentAppService`) yapılan dosyada bir satırdır. Hem listeden hem Visual Designer'dan (`CustomComponentAppService`) yapılan
@ -234,6 +238,23 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
kullanıcının runtime revizyonunu ezmez. Yeni bir seeder yazarken bu kuralı taklit et: **migrate kullanıcının runtime revizyonunu ezmez. Yeni bir seeder yazarken bu kuralı taklit et: **migrate
varsayılanı eklemektir, ezmek değil.** varsayılanı eklemektir, ezmek değil.**
- **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`. - **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`.
- **Public site** → menü `Menu` tablosunda `ModuleId = "Pub"` altında (`PlatformConsts.PublicSite`);
`App.Public.Header` hem üst menüyü hem alt bilgideki hızlı bağlantıları besler (ikisi de
`PublicNav` düğümü) ve ziyaretçiye uygulama konfigürasyonunun `publicMenus` alanıyla taşınır
(menü ucu `[Authorize]`). Sayfa ise Custom Component'tir: `/admin/` ile
başlamayan `RoutePath` otomatik public route olur ve aynı yolu paylaşan fiziksel route'u gölgeler;
`RoutePath` boş bırakılabilir (rotasız bileşen). Yeni public sayfa =
`data/App.DeveloperKit.CustomComponents.json` satırı + `Pub` menü kaydı; ikisi birlikte yazılır.
Üst ve alt bilgi rotasız bileşenlerdir (`PublicHeader`, `PublicFooter`), layout ikisini de
`renderComponent` ile çizer; rotasız bileşende `RoutePath` **NULL** yazılır (tekil indeks yalnızca
dolu değerleri kapsar). Üst bilginin davranışı `components/publicSite` katalog düğümlerinde,
yerleşimi tasarımcıdadır.
**Public bileşenlerin hepsi tasarımcı belgesidir** (`Props.visualDesigner`, `sourceMode: visual`);
kod belgeden üretilir (`generateDesignerCode`), elle yazılmış koda dönülmez. Runtime kapsamında
`react-icons`/`react-router` yoktur — ikon `PlatformIcon` düğümü, gezinme `a` düğümü. Kanvasın
doğrudan çizdiği katalog düğümleri tek sözlüktedir (`componentEditor/runtimeNodes.ts`); yeni bir
düğüm eklerken bileşen + bu sözlük + `catalog.ts` tanımı + dil anahtarı birlikte yazılır. Elle
yazılmış bir bileşen tasarımcıda kod modunda açılır, kanvasa çevrilmez. Ayrıntı: `README.md` §10.4.
- **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır. - **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır.
Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar; Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar;
tipli DTO alan bir uca (`list-form-dynamic-api/...`, Custom Endpoint, Dynamic Service) tipli DTO alan bir uca (`list-form-dynamic-api/...`, Custom Endpoint, Dynamic Service)

File diff suppressed because one or more lines are too long

View file

@ -47,28 +47,6 @@
"IsQuantityBased": true, "IsQuantityBased": true,
"ImageUrl": "https://images.pexels.com/photos/3184360/pexels-photo-3184360.jpeg?auto=compress&cs=tinysrgb&w=300" "ImageUrl": "https://images.pexels.com/photos/3184360/pexels-photo-3184360.jpeg?auto=compress&cs=tinysrgb&w=300"
}, },
{
"Id": "c2cf95b8-f63b-5ad4-9e5e-edbb8901da82",
"Name": "App.PublicProducts.TeacherLicense",
"Description": "App.ProductsTeacherLicense.TeacherLicenseDesc",
"Category": "App.ProductsCategories.Lisans",
"Order": 5,
"MonthlyPrice": 500,
"YearlyPrice": 1700,
"IsQuantityBased": true,
"ImageUrl": "https://images.pexels.com/photos/5212345/pexels-photo-5212345.jpeg?auto=compress&cs=tinysrgb&w=300"
},
{
"Id": "8c231f32-2709-54f7-8607-35e63e9b4a4e",
"Name": "App.PublicProducts.MobileReporting",
"Description": "App.ProductsMobileReporting.MobileReportingDesc",
"Category": "App.ProductsCategories.Lisans",
"Order": 6,
"MonthlyPrice": 400,
"YearlyPrice": 1400,
"IsQuantityBased": false,
"ImageUrl": "https://images.pexels.com/photos/1092644/pexels-photo-1092644.jpeg?auto=compress&cs=tinysrgb&w=300"
},
{ {
"Id": "0c927e71-0c51-5a2d-899c-c22302b563a0", "Id": "0c927e71-0c51-5a2d-899c-c22302b563a0",
"Name": "App.PublicProducts.RemoteBranchTraining", "Name": "App.PublicProducts.RemoteBranchTraining",

View file

@ -4496,3 +4496,292 @@ mr-1.2
mr-1.3 mr-1.3
mr-1.4 mr-1.4
sozsoft-* sozsoft-*
# --- custom component classes ---
!bg-transparent
absolute
backdrop-blur-sm
bg-amber-50
bg-blue-600
bg-blue-900
bg-blue-950/25
bg-gradient-to-br
bg-gradient-to-r
bg-gray-100
bg-gray-50
bg-gray-950
bg-gray-950/80
bg-green-600
bg-purple-600
bg-red-50
bg-red-600
bg-sky-600
bg-slate-100
bg-slate-300
bg-slate-50
bg-white
bg-white/10
bg-white/20
bg-white/5
block
border
border-0
border-amber-300
border-b
border-blue-600
border-collapse
border-dashed
border-gray-200
border-l-4
border-slate-100
border-slate-200
border-slate-300
border-t
border-white/10
border-white/5
break-all
col-start-3
container
dark:bg-amber-950
dark:bg-gray-800
dark:bg-gray-900
dark:bg-gray-950
dark:bg-gray-950/45
dark:bg-red-950
dark:bg-slate-700
dark:bg-slate-800
dark:bg-slate-900
dark:block
dark:border-amber-800
dark:border-gray-800
dark:border-slate-700
dark:border-slate-800
dark:from-gray-950
dark:hidden
dark:hover:!bg-transparent
dark:hover:bg-slate-800
dark:hover:bg-slate-800/70
dark:hover:text-gray-300
dark:hover:text-white
dark:opacity-35
dark:shadow-gray-950/40
dark:text-amber-200
dark:text-blue-400
dark:text-gray-100
dark:text-gray-200
dark:text-gray-300
dark:text-gray-400
dark:text-green-400
dark:text-red-200
dark:text-red-400
dark:text-slate-100
dark:text-slate-200
dark:text-slate-300
dark:text-slate-400
dark:text-white
dark:to-gray-950
dark:via-gray-950
disabled:cursor-not-allowed
disabled:opacity-40
duration-500
fixed
flex
flex-col
flex-shrink-0
flex-wrap
font-bold
font-medium
font-semibold
from-blue-500
from-blue-900
gap-1
gap-12
gap-2
gap-3
gap-6
gap-8
grid
grid-cols-1
grid-cols-[auto_1fr_auto]
h-10
h-5
h-9
h-[600px]
h-full
hidden
hover:!bg-transparent
hover:bg-blue-50
hover:bg-red-700
hover:bg-sky-700
hover:bg-slate-50
hover:bg-white/10
hover:bg-white/20
hover:border-sky-400
hover:from-blue-600
hover:shadow-xl
hover:text-blue-600
hover:text-sky-700
hover:to-purple-600
inline-block
inline-flex
inset-0
inset-x-0
items-center
items-start
justify-between
justify-center
justify-self-end
justify-self-start
last:border-b-0
last:mb-0
leading-relaxed
lg:col-start-1
lg:col-start-2
lg:flex
lg:grid-cols-2
lg:grid-cols-4
lg:grid-cols-5
lg:grid-cols-[1fr_auto]
lg:hidden
lg:justify-self-end
max-w-2xl
max-w-3xl
max-w-4xl
max-w-64
max-w-[190px]
mb-1
mb-12
mb-16
mb-2
mb-3
mb-4
mb-6
mb-8
md:col-span-4
md:flex-row
md:grid-cols-12
md:grid-cols-2
md:grid-cols-3
md:mt-0
md:text-2xl
md:text-6xl
min-h-screen
min-w-0
min-w-full
ml-1
ml-auto
mt-1
mt-12
mt-3
mt-4
mt-6
mx-1
mx-auto
object-contain
opacity-20
overflow-auto
overflow-hidden
p-3
p-4
p-5
p-8
pb-1
pb-16
pb-6
pt-12
pt-2
pt-20
pt-3
pt-40
pt-6
px-3
px-4
px-6
px-8
py-1.5
py-10
py-12
py-16
py-2
py-20
py-3
py-4
py-6
relative
rounded-2xl
rounded-lg
rounded-md
rounded-xl
shadow-black/30
shadow-black/40
shadow-lg
shadow-md
shadow-sm
shadow-xl
sm:grid-cols-2
space-x-2
space-x-3
space-x-4
space-x-6
space-y-2
space-y-3
space-y-4
text-2xl
text-3xl
text-4xl
text-5xl
text-[10px]
text-amber-800
text-blue-300
text-blue-500
text-blue-600
text-center
text-gray-300
text-gray-500
text-gray-600
text-gray-700
text-gray-800
text-gray-900
text-green-500
text-green-600
text-left
text-lg
text-purple-600
text-red-600
text-red-700
text-slate-400
text-slate-500
text-slate-600
text-slate-700
text-slate-800
text-sm
text-white
text-white/90
text-xl
text-xs
text-yellow-600
to-purple-500
to-purple-900
top-full
tracking-wider
transition
transition-all
transition-colors
transition-shadow
truncate
uppercase
via-indigo-900
w-24
w-9
w-auto
w-full
w-px
whitespace-nowrap
xl:block
xl:col-start-2
xl:col-start-3
xl:grid-cols-[auto_1fr_auto]
xl:h-auto
xl:justify-self-center
xl:max-w-none
z-10
z-50

View file

@ -1703,6 +1703,52 @@ export const CUSTOM_COMPONENTS: ComponentDefinition[] = [
}, },
] ]
export const HTML_ELEMENTS: ComponentDefinition[] = [ export const HTML_ELEMENTS: ComponentDefinition[] = [
{
name: 'a',
icon: 'Link',
category: 'basic',
description: 'App.ComponentLink.LinkDescription',
properties: [
{
name: 'children',
type: 'string',
value: '',
category: 'properties',
},
{
name: 'href',
type: 'string',
value: '',
category: 'properties',
},
{
name: 'target',
type: 'select',
value: '',
options: ['', '_blank', '_self'],
category: 'properties',
},
{
name: 'className',
type: 'string',
value: '',
category: 'properties',
},
{
name: 'style',
type: 'object',
value: {},
category: 'properties',
},
{
name: 'onClick',
type: 'string',
value: '',
category: 'events',
},
],
hooks: [],
},
{ {
name: 'div', name: 'div',
icon: 'Square', icon: 'Square',

View file

@ -4,6 +4,8 @@ interface PlatformIconProps {
/** Icon name as stored in the designer, e.g. `FaUser`. */ /** Icon name as stored in the designer, e.g. `FaUser`. */
name?: string name?: string
className?: string className?: string
/** Piksel cinsinden boyut; verilmezse ikon `className` ile boyutlandirilir. */
size?: number
} }
/** /**
@ -12,13 +14,13 @@ interface PlatformIconProps {
* and the generated component need one place that turns a name into an element. * and the generated component need one place that turns a name into an element.
* An unknown or empty name renders nothing, so an unset icon leaves no gap. * An unknown or empty name renders nothing, so an unset icon leaves no gap.
*/ */
const PlatformIcon = ({ name, className }: PlatformIconProps) => { const PlatformIcon = ({ name, className, size }: PlatformIconProps) => {
const navigationIcon = useNavigationIcons() const navigationIcon = useNavigationIcons()
if (!name) return null if (!name) return null
const Icon = navigationIcon[name] const Icon = navigationIcon[name]
// The dictionary loads asynchronously; until it arrives there is nothing to // The dictionary loads asynchronously; until it arrives there is nothing to
// draw, and the component re-renders on its own once it does. // draw, and the component re-renders on its own once it does.
return Icon ? <Icon className={className} /> : null return Icon ? <Icon className={className} size={size} /> : null
} }
export default PlatformIcon export default PlatformIcon

View file

@ -0,0 +1,27 @@
import type React from 'react'
import PlatformIcon from './PlatformIcon'
import {
PublicDemoButton,
PublicHeaderBar,
PublicLanguageSelector,
PublicLogo,
PublicMobileMenu,
PublicNav,
PublicThemeToggle,
} from '@/components/publicSite'
/**
* Katalogda yer alan ama derlenmis custom component listesinde bulunmayan
* dugumler. Kanvas bunlari dogrudan cizer, uretilen kod da ayni adla cagirir;
* iki taraf da bu sozlukten beslendigi icin tasarim ve calisma zamani ayrisamaz.
*/
export const RUNTIME_NODE_COMPONENTS = {
PlatformIcon,
PublicHeaderBar,
PublicLogo,
PublicLanguageSelector,
PublicThemeToggle,
PublicDemoButton,
PublicNav,
PublicMobileMenu,
} as Record<string, React.ComponentType<never>>

View file

@ -1,626 +1,39 @@
import View from '@/views/Views' import View from '@/views/Views'
import React, { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useLocation, Link, useNavigate } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { FaFacebook, FaTwitter, FaLinkedin, FaInstagram } from 'react-icons/fa'
import {
LuMenu,
LuX,
LuHouse,
LuInfo,
LuBriefcase,
LuPackage,
LuBookOpen,
LuPhone,
LuMapPin,
LuMail,
LuMoon,
LuSun,
} from 'react-icons/lu'
import Logo from '@/views/public/Logo'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import LanguageSelector from '../template/LanguageSelector'
import Demo from '@/views/public/Demo' import Demo from '@/views/public/Demo'
import { ScrollContext } from '@/contexts/ScrollContext'
import { DemoProvider } from '@/contexts/DemoContext' import { DemoProvider } from '@/contexts/DemoContext'
import useDarkMode from '@/utils/hooks/useDarkmode' import { PUBLIC_FOOTER_COMPONENT, PUBLIC_HEADER_COMPONENT } from '@/utils/hooks/usePublicMenu'
import { THEME_ENUM } from '@/constants/theme.constant' import { useComponents } from '@/contexts/ComponentContext'
import { Button } from '../ui'
interface NavLink {
resourceKey?: string
name: string
path?: string
action?: () => void
icon?: React.ComponentType<any>
}
/**
* Public site kabugu. Ust ve alt bilgi birer Custom Component'tir: tasarimlari
* Visual Designer'dan duzenlenir, icerikleri kendi dugum agaclarinda yasar.
* Burada yalnizca rota degisiminde basa sarma ve demo penceresi kalir.
*/
const PublicLayout = () => { const PublicLayout = () => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate()
const { translate } = useLocalization()
const [isDarkMode, setThemeMode] = useDarkMode()
const [isOpen, setIsOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [isDemoOpen, setIsDemoOpen] = useState(false) const [isDemoOpen, setIsDemoOpen] = useState(false)
const currentYear = new Date().getFullYear() const { isComponentRegistered, renderComponent } = useComponents()
useEffect(() => { useEffect(() => {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, [location.pathname]) }, [location.pathname])
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 10
if (isScrolled !== scrolled) {
setScrolled(isScrolled)
}
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [scrolled])
const toggleMenu = () => setIsOpen(!isOpen)
const navLinks: NavLink[] = [
{
resourceKey: 'App.Home',
name: translate('::App.Home'),
path: ROUTES_ENUM.public.home,
icon: LuHouse,
},
{
resourceKey: 'App.About',
name: translate('::App.About'),
path: ROUTES_ENUM.public.about,
icon: LuInfo,
},
{
resourceKey: 'App.Services',
name: translate('::App.Services'),
path: ROUTES_ENUM.public.services,
icon: LuBriefcase,
},
{
resourceKey: 'Public.nav.products',
name: translate('::App.Platform.Orders'),
path: ROUTES_ENUM.public.products,
icon: LuPackage,
},
{
resourceKey: 'Public.nav.blog',
name: translate('::App.Platform.BlogManagement'),
path: ROUTES_ENUM.public.blog,
icon: LuBookOpen,
},
{
resourceKey: 'Public.nav.contact',
name: translate('::App.Contact'),
path: ROUTES_ENUM.public.contact,
icon: LuPhone,
},
{
resourceKey: 'App.PublicNav.Giris',
name: translate('::App.PublicNav.Giris'),
path: ROUTES_ENUM.authenticated.login,
},
]
const isLoginLink = (resourceKey?: string) => resourceKey === 'App.PublicNav.Giris'
const demoButton = (className = '', onClick?: () => void) => (
<Button
type="button"
onClick={() => {
setIsDemoOpen(true)
onClick?.()
}}
variant="plain"
size="sm"
className={`group !h-9 !w-9 !bg-transparent !px-0 !text-gray-200 hover:!bg-transparent hover:!text-white !shadow-none !overflow-visible transition-colors ${className}`}
>
<span className="relative inline-flex h-7 w-7 items-center justify-center overflow-visible rounded-full border-2 border-stone-700 bg-gradient-to-b from-red-400 via-red-600 to-red-800 shadow-[inset_0_-3px_6px_rgba(0,0,0,0.35),0_1px_3px_rgba(0,0,0,0.35)] ring-1 ring-black/60 transition-all duration-200 group-hover:scale-105 group-hover:from-red-300 group-hover:via-red-500 group-hover:to-red-700 group-hover:shadow-[inset_0_-3px_6px_rgba(0,0,0,0.3),0_2px_8px_rgba(239,68,68,0.35)]">
<span className="absolute inset-x-1 top-1 h-2 rounded-full bg-white/35 blur-[1px] transition-colors group-hover:bg-white/50" />
<span className="absolute left-1/2 top-1/2 z-10 w-12 -translate-x-1/2 -translate-y-1/2 text-center uppercase leading-none tracking-tight text-white drop-shadow">
{translate('::App.Demos')}
</span>
</span>
</Button>
)
const themeToggle = (
<div className="inline-flex h-9 items-center rounded-lg p-0.5">
<Button
type="button"
onClick={() => setThemeMode(THEME_ENUM.MODE_LIGHT)}
aria-pressed={!isDarkMode}
title={translate('::App.Theme.LightMode')}
icon={<LuSun size={15} strokeWidth={1.8} />}
variant="plain"
size="xs"
className={`!h-8 !w-8 xl:!w-auto !px-0 xl:!px-2.5 !rounded-l-md !rounded-r-none !border-transparent !shadow-none ${
!isDarkMode
? '!bg-white !text-gray-950'
: '!bg-transparent !text-gray-300 hover:!bg-transparent hover:!text-white'
}`}
/>
<Button
type="button"
onClick={() => setThemeMode(THEME_ENUM.MODE_DARK)}
aria-pressed={isDarkMode}
title={translate('::App.Theme.DarkMode')}
icon={<LuMoon size={15} strokeWidth={1.8} />}
variant="plain"
size="xs"
className={`!h-8 !w-8 xl:!w-auto !px-0 xl:!px-2.5 !rounded-r-md !rounded-l-none !border-transparent !shadow-none ${
isDarkMode
? '!bg-gray-300 !text-gray-800'
: '!bg-transparent !text-gray-300 hover:!bg-transparent hover:!text-white'
}`}
/>
</div>
)
const isActiveLink = (path?: string) => {
if (!path) return false
if (path === ROUTES_ENUM.public.home) return location.pathname === path
return location.pathname.startsWith(path)
}
return ( return (
<DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}> <DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}>
<ScrollContext.Provider value={scrolled}> <div className="flex min-h-screen flex-col overflow-x-hidden">
<div className="flex flex-col min-h-screen"> {isComponentRegistered(PUBLIC_HEADER_COMPONENT) &&
{/* HEADER */} renderComponent(PUBLIC_HEADER_COMPONENT)}
<header
className={`fixed w-full z-50 transition-all duration-500 ${
scrolled
? 'bg-gray-950 shadow-lg shadow-black/30 py-2 border-b border-white/10'
: 'bg-gray-950/80 backdrop-blur-sm py-4 border-b border-white/5'
}`}
>
<div className="container mx-auto px-6 relative grid grid-cols-[auto_1fr_auto] items-center lg:grid-cols-[1fr_auto] xl:grid-cols-[auto_1fr_auto]">
<Logo
mode={isDarkMode ? 'dark' : 'light'}
className="relative z-10 max-w-[190px] overflow-hidden lg:hidden xl:block xl:max-w-none"
imgClass="h-10 w-auto object-contain xl:h-auto"
/>
{/* Desktop / tablet navigation */}
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1">
{navLinks
.filter((l) => !isLoginLink(l.resourceKey))
.map((link) => {
const active = isActiveLink(link.path)
const baseClass =
'relative flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 group'
const activeClass = active ? 'text-white' : 'text-gray-300 hover:text-white'
return link.path ? (
<Link
key={link.path}
to={link.path}
className={`${baseClass} ${activeClass}`}
>
{link.icon && (
<link.icon
size={22}
strokeWidth={1.75}
className={
active
? 'text-blue-400'
: 'text-gray-400 group-hover:text-blue-400 transition-colors'
}
/>
)}
{link.name}
<span
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
}`}
/>
</Link>
) : (
<Button
key={link.name}
onClick={link.action}
variant="plain"
size="sm"
className={`${baseClass} ${activeClass}`}
>
{link.icon && (
<link.icon
size={22}
strokeWidth={1.75}
className="text-gray-400 group-hover:text-blue-400 transition-colors"
/>
)}
{link.name}
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
</Button>
)
})}
</nav>
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
{demoButton('rounded-lg')}
<div className="h-5 w-px bg-white/20" />
<div className="flex h-9 w-9 items-center justify-center">
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
</div>
<div className="h-5 w-px bg-white/20" />
{themeToggle}
<div className="h-5 w-px bg-white/20" />
{navLinks
.filter((l) => isLoginLink(l.resourceKey))
.map((link) =>
link.path ? (
<Button
key={link.path}
type="button"
variant="solid"
shape="round"
onClick={() => navigate(link.path!)}
>
{link.name}
</Button>
) : null,
)}
</div>
{/* Mobile Menu Button */}
<Button
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
onClick={toggleMenu}
aria-label={translate('::App.Platform.ToggleMenu')}
aria-expanded={isOpen}
icon={
isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />
}
variant="plain"
size="xs"
/>
</div>
{/* Mobile Navigation */}
<div
className={`lg:hidden transition-all duration-300 ease-in-out ${
isOpen
? 'max-h-screen overflow-visible opacity-100'
: 'max-h-0 overflow-hidden opacity-0'
}`}
>
<div className="border-t border-white/10 bg-gray-950/98 backdrop-blur-md">
<div className="container mx-auto px-6 py-3">
<nav className="flex flex-col gap-1">
<div className="lg:hidden flex flex-col gap-1">
{navLinks.map((link) => {
const active = isActiveLink(link.path)
const isLogin = isLoginLink(link.resourceKey)
if (isLogin) {
return link.path ? (
<Link
key={link.path}
to={link.path}
onClick={toggleMenu}
className="mt-2 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
>
{link.name}
</Link>
) : null
}
return link.path ? (
<Link
key={link.path}
to={link.path}
onClick={toggleMenu}
className={`flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium rounded-md transition-colors ${
active
? 'bg-white/10 text-white'
: 'text-gray-300 hover:bg-white/5 hover:text-white'
}`}
>
{link.icon && (
<link.icon
size={16}
strokeWidth={1.75}
className={active ? 'text-blue-400' : 'text-gray-400'}
/>
)}
{link.name}
</Link>
) : (
<Button
key={link.name}
onClick={() => {
link.action?.()
toggleMenu()
}}
variant="plain"
size="sm"
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
>
{link.icon && (
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
)}
{link.name}
</Button>
)
})}
</div>
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-1">
{demoButton('rounded-lg', toggleMenu)}
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
</div>
</div>
<div className="flex h-10 items-center rounded-lg px-1">{themeToggle}</div>
{navLinks
.filter((l) => isLoginLink(l.resourceKey))
.map((link) =>
link.path ? (
<Link
key={link.path}
to={link.path}
onClick={toggleMenu}
className="hidden h-10 lg:inline-flex items-center justify-center px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-colors"
>
{link.name}
</Link>
) : null,
)}
</div>
</div>
</nav>
</div>
</div>
</div>
</header>
<main className="flex-grow"> <main className="flex-grow">
<View /> <View />
</main> </main>
<footer className="bg-gray-900 text-white pt-16 pb-8"> {isComponentRegistered(PUBLIC_FOOTER_COMPONENT) && renderComponent(PUBLIC_FOOTER_COMPONENT)}
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div>
<Logo mode={isDarkMode ? 'dark' : 'light'} />
<p className="mt-4 text-gray-400 text-sm">
{translate('::App.PublicFooter.CompanyInfo')}
</p>
<div className="flex space-x-4 mt-6">
<a
href="https://facebook.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaFacebook size={20} />
</a>
<a
href="https://twitter.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaTwitter size={20} />
</a>
<a
href="https://linkedin.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaLinkedin size={20} />
</a>
<a
href="https://instagram.com/sozsoft"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<FaInstagram size={20} />
</a>
</div>
</div>
{/* Quick Links */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::App.PublicFooter.QuickLinksTitle')}
</h3>
<ul className="space-y-2">
<li>
<Link
to={ROUTES_ENUM.public.home}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Home')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.products}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Platform.Orders')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Services')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.About')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.blog}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Platform.BlogManagement')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.contact}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.Contact')}
</Link>
</li>
</ul>
</div>
{/* Services */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::App.PublicServices.ServicesTitle')}
</h3>
<ul className="space-y-2">
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.ServicesSoftware.SoftwareTitle')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.BlogCategories.Webdev')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.ServicesMobile.MobileTitle')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.ServicesDatabase.DatabaseTitle')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.ServicesIntegration.IntegrationTitle')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.services}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.ServicesConsulting.ConsultingTitle')}
</Link>
</li>
</ul>
</div>
{/* Contact Info */}
<div>
<h3 className="text-lg font-bold mb-4 text-white">
{translate('::App.Contact')}
</h3>
<ul className="space-y-3">
<li className="flex items-start space-x-3">
<LuMapPin
size={18}
strokeWidth={1.75}
className="text-gray-400 mt-1 flex-shrink-0"
/>
<span className="text-gray-400">{translate('::App.PublicFooter.Address')}</span>
</li>
<li className="flex items-center space-x-3">
<LuPhone
size={18}
strokeWidth={1.75}
className="text-gray-400 flex-shrink-0"
/>
<a
href="tel:+905447697638"
className="text-gray-400 hover:text-white transition-colors"
>
+90 (544) 769 7 638
</a>
</li>
<li className="flex items-center space-x-3">
<LuMail
size={18}
strokeWidth={1.75}
className="text-gray-400 flex-shrink-0"
/>
<a
href="mailto:destek@sozsoft.com"
className="text-gray-400 hover:text-white transition-colors"
>
destek@sozsoft.com
</a>
</li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 mt-12 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center">
<p className="text-gray-400 text-sm">
&copy; {currentYear} Sözsoft Platform. {translate('::App.PublicFooter.Copyright')}
</p>
<div className="mt-4 md:mt-0">
<ul className="flex space-x-6 text-sm">
<li>
<Link
to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.PublicFooter.PrivacyPolicy')}
</Link>
</li>
<li>
<Link
to={ROUTES_ENUM.public.about}
className="text-gray-400 hover:text-white transition-colors"
>
{translate('::App.PublicFooter.TermsOfUse')}
</Link>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
{/* Demo Modal */}
<Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} /> <Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} />
</div> </div>
</ScrollContext.Provider>
</DemoProvider> </DemoProvider>
) )
} }

View file

@ -2,7 +2,7 @@ import React from 'react'
import { FaCalendar, FaClock, FaMinus, FaPlus, FaShoppingCart } from 'react-icons/fa' import { FaCalendar, FaClock, FaMinus, FaPlus, FaShoppingCart } from 'react-icons/fa'
import { BillingCycle } from '@/proxy/order/models' import { BillingCycle } from '@/proxy/order/models'
import { BasketData } from '@/utils/basketUtils' import { BasketData } from '@/utils/basketUtils'
import { useScroll } from '@/contexts/ScrollContext' import { useScrolled } from '@/utils/hooks/useScrolled'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button } from '@/components/ui' import { Button } from '@/components/ui'
@ -27,7 +27,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
basketData, basketData,
}) => { }) => {
const hasBasketItems = basketData.items.length > 0 const hasBasketItems = basketData.items.length > 0
const isScrolled = useScroll() const isScrolled = useScrolled()
const { translate } = useLocalization() const { translate } = useLocalization()
return ( return (

View file

@ -0,0 +1,343 @@
/**
* Public site basliginin tasarlanabilir parcalari. Her biri Visual Designer
* katalogunda bir dugumdur: `PublicHeader` custom component'i bunlari kanvasta
* birlestirir, uretilen kod ayni bilesenleri cagirir. Menu, tema, dil ve demo
* gibi davranislar burada yasar; yerlesim ve siniflar tasarimciya birakilir.
*/
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { LuMenu, LuMoon, LuSun, LuX } from 'react-icons/lu'
import Logo from '@/views/public/Logo'
import LanguageSelector from '@/components/template/LanguageSelector'
import { PageIcon } from '@/views/public/shared'
import { Button } from '@/components/ui'
import { THEME_ENUM } from '@/constants/theme.constant'
import useDarkMode from '@/utils/hooks/useDarkmode'
import { useScrolled } from '@/utils/hooks/useScrolled'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useOptionalDemo } from '@/contexts/DemoContext'
import {
PUBLIC_MENU_CTA_CLASS,
usePublicMenu,
type PublicMenuNode,
} from '@/utils/hooks/usePublicMenu'
import { ROUTES_ENUM } from '@/routes/route.constant'
/** Mobil panel acikken bir baglantiya basilinca panel kendini kapatir. */
const MobileMenuContext = createContext<(() => void) | null>(null)
const isCallToAction = (item: PublicMenuNode) =>
item.cssClass?.split(/\s+/).includes(PUBLIC_MENU_CTA_CLASS) ?? false
// ------------------------------------------------------------------ HeaderBar
interface PublicHeaderBarProps {
className?: string
/** Sayfa kaydirildiginda `className` yerine bu uygulanir; bos birakilirsa baslik degismez. */
scrolledClassName?: string
/** Kaydirmanin "basladi" sayildigi piksel esigi. */
scrollThreshold?: number
children?: React.ReactNode
}
export const PublicHeaderBar = ({
className = '',
scrolledClassName = '',
scrollThreshold = 10,
children,
}: PublicHeaderBarProps) => {
const scrolled = useScrolled(scrollThreshold)
return (
<header className={scrolled && scrolledClassName ? scrolledClassName : className}>
{children}
</header>
)
}
// ----------------------------------------------------------------------- Logo
interface PublicLogoProps {
className?: string
imgClassName?: string
}
/** Logonun aydinlik/karanlik varyantini aktif temadan secer. */
export const PublicLogo = ({ className = '', imgClassName = '' }: PublicLogoProps) => {
const [isDarkMode] = useDarkMode()
return <Logo className={className} imgClass={imgClassName} mode={isDarkMode ? 'dark' : 'light'} />
}
// ------------------------------------------------------------------- Language
export const PublicLanguageSelector = ({ className = '' }: { className?: string }) => (
<LanguageSelector className={className} />
)
// ---------------------------------------------------------------- ThemeToggle
interface PublicThemeToggleProps {
className?: string
buttonClassName?: string
activeClassName?: string
iconSize?: number
}
export const PublicThemeToggle = ({
className = 'inline-flex h-9 items-center rounded-lg p-0.5',
buttonClassName = '!h-8 !w-8 xl:!w-auto !px-0 xl:!px-2.5 !border-transparent !shadow-none !bg-transparent !text-gray-300 hover:!bg-transparent hover:!text-white',
activeClassName = '!bg-white !text-gray-950',
iconSize = 15,
}: PublicThemeToggleProps) => {
const [isDarkMode, setThemeMode] = useDarkMode()
const { translate } = useLocalization()
const modes = [
{
mode: THEME_ENUM.MODE_LIGHT,
active: !isDarkMode,
title: '::App.Theme.LightMode',
icon: <LuSun size={iconSize} strokeWidth={1.8} />,
shape: '!rounded-l-md !rounded-r-none',
},
{
mode: THEME_ENUM.MODE_DARK,
active: isDarkMode,
title: '::App.Theme.DarkMode',
icon: <LuMoon size={iconSize} strokeWidth={1.8} />,
shape: '!rounded-r-md !rounded-l-none',
},
]
return (
<div className={className}>
{modes.map((item) => (
<Button
key={item.mode}
aria-pressed={item.active}
className={`${buttonClassName} ${item.shape} ${item.active ? activeClassName : ''}`}
icon={item.icon}
size="xs"
title={translate(item.title)}
type="button"
variant="plain"
onClick={() => setThemeMode(item.mode)}
/>
))}
</div>
)
}
// ----------------------------------------------------------------- DemoButton
export const PublicDemoButton = ({ className = '' }: { className?: string }) => {
const { translate } = useLocalization()
const { openDemo } = useOptionalDemo()
const closeMobileMenu = useContext(MobileMenuContext)
return (
<Button
className={`group !h-9 !w-9 !overflow-visible !bg-transparent !px-0 !text-gray-200 !shadow-none transition-colors hover:!bg-transparent hover:!text-white ${className}`}
size="sm"
type="button"
variant="plain"
onClick={() => {
openDemo()
closeMobileMenu?.()
}}
>
<span className="relative inline-flex h-7 w-7 items-center justify-center overflow-visible rounded-full border-2 border-stone-700 bg-gradient-to-b from-red-400 via-red-600 to-red-800 shadow-[inset_0_-3px_6px_rgba(0,0,0,0.35),0_1px_3px_rgba(0,0,0,0.35)] ring-1 ring-black/60 transition-all duration-200 group-hover:scale-105 group-hover:from-red-300 group-hover:via-red-500 group-hover:to-red-700 group-hover:shadow-[inset_0_-3px_6px_rgba(0,0,0,0.3),0_2px_8px_rgba(239,68,68,0.35)]">
<span className="absolute inset-x-1 top-1 h-2 rounded-full bg-white/35 blur-[1px] transition-colors group-hover:bg-white/50" />
<span className="absolute left-1/2 top-1/2 z-10 w-12 -translate-x-1/2 -translate-y-1/2 text-center uppercase leading-none tracking-tight text-white drop-shadow">
{translate('::App.Demos')}
</span>
</span>
</Button>
)
}
// ------------------------------------------------------------------------ Nav
interface PublicNavProps {
/** `navigation` gezinme baglantilari · `actions` vurgulu (cta) kayitlar · `all` hepsi. */
source?: 'navigation' | 'actions' | 'all'
/** `horizontal` masaustu seridi · `vertical` mobil liste. */
variant?: 'horizontal' | 'vertical'
className?: string
itemClassName?: string
activeItemClassName?: string
iconSize?: number
showIcons?: boolean
}
const HORIZONTAL_ITEM =
'group relative flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium text-gray-300 transition-all duration-200 hover:text-white'
const VERTICAL_ITEM =
'flex items-center gap-2.5 rounded-md px-3 py-2.5 text-sm font-medium text-gray-300 transition-colors hover:bg-white/5 hover:text-white'
const ACTION_ITEM =
'inline-flex items-center justify-center rounded-full bg-blue-600 px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-500'
const ACTION_ITEM_VERTICAL =
'mt-2 flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-500'
export const PublicNav = ({
source = 'navigation',
variant = 'horizontal',
className = '',
itemClassName = '',
activeItemClassName = '',
iconSize,
showIcons = true,
}: PublicNavProps) => {
const { headerLinks } = usePublicMenu()
const { translate } = useLocalization()
const location = useLocation()
const closeMobileMenu = useContext(MobileMenuContext)
const isAction = source === 'actions'
const horizontal = variant === 'horizontal'
const items = useMemo(
() =>
source === 'all'
? headerLinks
: headerLinks.filter((item) => isCallToAction(item) === isAction),
[headerLinks, isAction, source],
)
const label = (item: PublicMenuNode) => translate(`::${item.displayName ?? item.code}`)
const isActive = (url?: string) => {
if (!url) return false
return url === ROUTES_ENUM.public.home
? location.pathname === url
: location.pathname.startsWith(url)
}
const defaultItemClass = isAction
? horizontal
? ACTION_ITEM
: ACTION_ITEM_VERTICAL
: horizontal
? HORIZONTAL_ITEM
: VERTICAL_ITEM
const baseItem = itemClassName || defaultItemClass
const activeItem =
activeItemClassName || (isAction ? '' : horizontal ? 'text-white' : 'bg-white/10 text-white')
// Katalog varsayilani 0'dir: "otomatik" demek, varyanta gore boyut secilir.
const size = iconSize || (horizontal ? 22 : 16)
const itemContent = (item: PublicMenuNode, active: boolean) => (
<>
{showIcons && (
<PageIcon
className={
active ? 'text-blue-400' : 'text-gray-400 transition-colors group-hover:text-blue-400'
}
name={item.icon}
size={size}
/>
)}
{label(item)}
{horizontal && !isAction && (
<span
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
}`}
/>
)}
</>
)
const renderItem = (item: PublicMenuNode, extraClass = '') => {
const active = isActive(item.url)
const itemClass = `${baseItem} ${active ? activeItem : ''} ${extraClass}`
if (!item.url) {
return (
<span key={item.code} className={`${itemClass} cursor-default`}>
{itemContent(item, active)}
</span>
)
}
return (
<Link key={item.code} className={itemClass} to={item.url} onClick={() => closeMobileMenu?.()}>
{itemContent(item, active)}
</Link>
)
}
return (
<nav className={className}>
{items.map((item) => {
if (!item.children.length) return renderItem(item)
// Alt menusu olan kayit: masaustunde acilir liste, mobilde girintili satirlar.
if (horizontal) {
return (
<div key={item.code} className="group/menu relative">
{renderItem(item)}
<div className="invisible absolute left-0 top-full z-20 min-w-[13rem] rounded-lg border border-white/10 bg-gray-950/98 py-2 opacity-0 shadow-xl backdrop-blur-md transition-all duration-150 group-hover/menu:visible group-hover/menu:opacity-100">
{item.children.map((child) => renderItem(child, 'px-4 py-2'))}
</div>
</div>
)
}
return (
<div key={item.code} className="flex flex-col">
{renderItem(item)}
{item.children.map((child) => renderItem(child, 'ml-4'))}
</div>
)
})}
</nav>
)
}
// ----------------------------------------------------------------- MobileMenu
interface PublicMobileMenuProps {
className?: string
buttonClassName?: string
panelClassName?: string
children?: React.ReactNode
}
/**
* Hamburger dugmesi ve acilir panel. Panelin icerigi tasarimcidan gelir; panel
* en yakin konumlandirilmis ust ogenin altina acilir.
*/
export const PublicMobileMenu = ({
className = '',
buttonClassName = 'flex h-9 w-9 items-center justify-center rounded-md text-white transition-colors hover:bg-white/10',
panelClassName = 'absolute inset-x-0 top-full z-50 border-t border-white/10 bg-gray-950 px-6 py-3 shadow-xl shadow-black/40',
children,
}: PublicMobileMenuProps) => {
const [open, setOpen] = useState(false)
const { translate } = useLocalization()
const { pathname } = useLocation()
// Panelin icindeki her baglanti context'i kullanmaz (tasarimciya konan ham `a`
// dugumleri, tarayici geri/ileri): adres degistiginde panel her hâlükârda kapanir.
useEffect(() => setOpen(false), [pathname])
return (
<MobileMenuContext.Provider value={() => setOpen(false)}>
{/* Panel de sarmalayicinin icinde: `lg:hidden` gibi kirilim siniflari boylece
panele de isler, masaustu genisligine donuldugunde acik kalmaz. */}
<div className={className}>
<Button
aria-expanded={open}
aria-label={translate('::App.Platform.ToggleMenu')}
className={buttonClassName}
icon={open ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />}
size="xs"
type="button"
variant="plain"
onClick={() => setOpen((previous) => !previous)}
/>
{open && <div className={panelClassName}>{children}</div>}
</div>
</MobileMenuContext.Provider>
)
}

View file

@ -0,0 +1,83 @@
import React from 'react'
import { createPortal } from 'react-dom'
/**
* Tasarim yuzeyini kendi belgesinde cizer. Tailwind'in `md:`/`lg:` kurallari
* tarayici penceresine gore cozulur; kanvas ayni sayfada durdugu surece cihaz
* secimi yalnizca paneli daraltir, yerlesimi degistirmez. Ayri bir belge bu
* kurallari secilen genislige gore cozer: tasarimcida gorulen, canlida gorulendir.
*
* Stil etiketleri ust belgeden kopyalanir (uretimde `<link>`, gelistirmede HMR ile
* degisen `<style>` etiketleri) ve tema sinifi izlenir.
*/
const CanvasFrame = ({ width, children }: { width: string; children: React.ReactNode }) => {
const frameRef = React.useRef<HTMLIFrameElement>(null)
const [frameDocument, setFrameDocument] = React.useState<Document | null>(null)
const [height, setHeight] = React.useState(600)
const attach = React.useCallback(() => {
const doc = frameRef.current?.contentDocument
if (doc?.body) setFrameDocument(doc)
}, [])
React.useEffect(attach, [attach])
// Ust belgedeki stiller cerceveye kopyalanir ve degisimleri izlenir.
React.useEffect(() => {
if (!frameDocument) return undefined
const sync = () => {
frameDocument.head.querySelectorAll('[data-canvas-style]').forEach((node) => node.remove())
document.head.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => {
const copy = node.cloneNode(true) as HTMLElement
copy.setAttribute('data-canvas-style', '')
frameDocument.head.appendChild(copy)
})
}
sync()
const observer = new MutationObserver(sync)
observer.observe(document.head, { childList: true, subtree: true, characterData: true })
return () => observer.disconnect()
}, [frameDocument])
// Tema sinifi: kanvas, uygulamanin aydinlik/karanlik modunu izler.
React.useEffect(() => {
if (!frameDocument) return undefined
const sync = () => {
frameDocument.documentElement.className = document.documentElement.className
frameDocument.body.style.margin = '0'
}
sync()
const observer = new MutationObserver(sync)
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
return () => observer.disconnect()
}, [frameDocument])
// Cerceve icerigi kadar uzar; kaydirma disaridaki panelde kalir.
React.useEffect(() => {
if (!frameDocument?.body) return undefined
const observer = new ResizeObserver(() =>
setHeight(Math.max(frameDocument.body.scrollHeight, 200)),
)
observer.observe(frameDocument.body)
return () => observer.disconnect()
}, [frameDocument])
return (
<iframe
ref={frameRef}
className="block border-0 bg-transparent"
style={{ width, height }}
title="canvas"
onLoad={attach}
>
{frameDocument ? createPortal(children, frameDocument.body) : null}
</iframe>
)
}
export default CanvasFrame

View file

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import * as UiKit from '@/components/ui' import * as UiKit from '@/components/ui'
import PlatformIcon from '@/components/componentEditor/PlatformIcon' import PlatformIcon from '@/components/componentEditor/PlatformIcon'
import { RUNTIME_NODE_COMPONENTS } from '@/components/componentEditor/runtimeNodes'
import PlatformSelectComponent from '@/components/componentEditor/selectComponents' import PlatformSelectComponent from '@/components/componentEditor/selectComponents'
import PlatformViewHost, { import PlatformViewHost, {
type PlatformViewName, type PlatformViewName,
@ -8,7 +9,7 @@ import PlatformViewHost, {
import apiService from '@/services/api.service' import apiService from '@/services/api.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { formatLocaleValue } from '@/utils/localeFormat' import { formatLocaleValue } from '@/utils/localeFormat'
import { FaArrowDown, FaArrowUp, FaClone, FaGripVertical, FaTrash } from 'react-icons/fa' import { FaGripVertical } from 'react-icons/fa'
import { beginDesignerDragSoon, endDesignerDrag, useDesignerDrag } from './designerDrag' import { beginDesignerDragSoon, endDesignerDrag, useDesignerDrag } from './designerDrag'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import { import {
@ -167,10 +168,7 @@ interface VisualCanvasProps {
) => void ) => void
/** Moves an existing node into a container/table cell, or to the root. */ /** Moves an existing node into a container/table cell, or to the root. */
onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void
onMove?: (id: string, direction: -1 | 1) => void
onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void
onDuplicate?: (id: string) => void
onDelete?: (id: string) => void
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
dataValues?: Record<string, unknown> dataValues?: Record<string, unknown>
@ -1289,6 +1287,18 @@ const renderElement = (
refs, refs,
refOverrides, refOverrides,
) )
// Platform düğümleri (ikon, public site parçaları) kataloğun bir parçasıdır ama
// derlenmiş custom component listesinde değildir; kanvas onları doğrudan çizer,
// üretilen kod da aynı bileşenleri çağırır.
const RuntimeNode = RUNTIME_NODE_COMPONENTS[node.type]
if (RuntimeNode) {
const { children: runtimeChildren, ...runtimeProps } = props
return React.createElement(
RuntimeNode as React.ComponentType<Record<string, unknown>>,
runtimeProps,
content ?? (runtimeChildren as React.ReactNode),
)
}
if (node.kind === 'custom') { if (node.kind === 'custom') {
return renderCustomComponent?.(node.type, { ...props, children: content }) || null return renderCustomComponent?.(node.type, { ...props, children: content }) || null
} }
@ -1429,21 +1439,97 @@ const HTML_PLACEHOLDER_ELEMENTS = new Set([
'h6', 'h6',
]) ])
/**
* Kanvasta her dugum kendi sarmalayici div'i icinde cizilir ve ust ogenin grid/flex
* duzeninde asil oge o sarmalayici olur. Dugumun kendi yerlesim siniflari sarmalayiciya
* tasinmazsa (`md:col-span-4`, `flex-1`, `absolute inset-0`, kenar bosluklari) tasarim
* zamani gorunumu canlidan farkli cikar; asagidaki ayrim bunu engeller.
*/
const HOISTED_LAYOUT_PATTERN =
/^(-?m[trblxyse]?-|w-(?!full$|auto$)|basis-|order-|self-|justify-self-|(col|row)-(span|start|end|auto)|\[grid-(column|row):)/
const HOISTED_LAYOUT_EXACT = new Set([
'flex-1',
'flex-auto',
'flex-initial',
'flex-none',
'grow',
'grow-0',
'shrink',
'shrink-0',
])
/** Sarmalayici da tasir ama dugumden silinmez: ikisi de ayni kutuyu kaplar. */
const SHARED_LAYOUT_PATTERN = /^(absolute|fixed|sticky|w-full|h-full)$|^-?(inset|top|right|bottom|left)-|^z-|^(min|max)-w-/
const NODE_POSITION_PATTERN = /^(absolute|fixed|sticky)$/
/** `md:`, `dark:hover:` gibi varyant onlerini atar; `[grid-column:1/-1]` bozulmadan kalir. */
const stripVariants = (token: string) => token.replace(/^(?:(?:[\w-]+|\[[^\]]+\]):)+/, '')
/**
* Kanvas kendi paneline cizer; `fixed` bir dugum tarayici penceresine yapisip
* tasarim yuzeyinden tasar. Tasarim zamaninda akista kalmasi icin akisa donusturulur.
*/
const canvasPositionClass = (token: string) =>
stripVariants(token) === 'fixed' ? token.replace(/fixed$/, 'relative') : token
/**
* Gorunurluk sinifi dugumde kalirsa sarmalayici yine de bir grid/flex hucresi isgal eder:
* mobilde gizlenmesi gereken masaustu seridi yer kaplar ve komsulari yanlis hucreye iter.
* Sarmalayiciya `hidden` ile birlikte gorunur hallerinin blok karsiligi verilir; dugumun
* kendi `display` degeri (flex/grid) el degmeden kalir, ic duzeni bozulmaz.
*/
const DISPLAY_CLASSES = new Set([
'hidden',
'block',
'inline-block',
'inline',
'flex',
'inline-flex',
'grid',
'inline-grid',
'table',
'flow-root',
'contents',
'list-item',
])
const wrapperDisplayClass = (token: string, base: string) =>
`${token.slice(0, token.length - base.length)}${base === 'hidden' ? 'hidden' : 'block'}`
const splitLayoutClasses = (className: unknown) => {
const hoisted: string[] = []
const remaining: string[] = []
let positioned = false
for (const token of String(className ?? '')
.split(/\s+/)
.filter(Boolean)) {
const base = stripVariants(token)
if (SHARED_LAYOUT_PATTERN.test(base)) {
hoisted.push(canvasPositionClass(token))
remaining.push(canvasPositionClass(token))
positioned ||= NODE_POSITION_PATTERN.test(base)
} else if (DISPLAY_CLASSES.has(base)) {
hoisted.push(wrapperDisplayClass(token, base))
remaining.push(token)
} else if (HOISTED_LAYOUT_PATTERN.test(base) || HOISTED_LAYOUT_EXACT.has(base)) {
hoisted.push(token)
} else {
remaining.push(token)
}
}
return { hoisted: hoisted.join(' '), remaining: remaining.join(' '), positioned }
}
const NodeView = ({ const NodeView = ({
node, node,
index,
siblingCount = 1,
isRoot = false,
selectedId, selectedId,
interactive, interactive,
onSelect, onSelect,
onDropComponent, onDropComponent,
onDropComponentBeside, onDropComponentBeside,
onMoveIntoContainer, onMoveIntoContainer,
onMove,
onReorder, onReorder,
onDuplicate,
onDelete,
onNodePropChange, onNodePropChange,
renderCustomComponent, renderCustomComponent,
dataValues, dataValues,
@ -1451,9 +1537,6 @@ const NodeView = ({
formScope, formScope,
}: { }: {
node: DesignerNode node: DesignerNode
index: number
siblingCount?: number
isRoot?: boolean
selectedId: string | null selectedId: string | null
interactive: boolean interactive: boolean
onSelect?: (id: string) => void onSelect?: (id: string) => void
@ -1464,10 +1547,7 @@ const NodeView = ({
placement: 'before' | 'after', placement: 'before' | 'after',
) => void ) => void
onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void
onMove?: (id: string, direction: -1 | 1) => void
onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void
onDuplicate?: (id: string) => void
onDelete?: (id: string) => void
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
dataValues: Record<string, unknown> dataValues: Record<string, unknown>
@ -1483,10 +1563,13 @@ const NodeView = ({
const refOverrides = refOverride?.props || {} const refOverrides = refOverride?.props || {}
const refHidden = Boolean(refOverride?.hidden) const refHidden = Boolean(refOverride?.hidden)
const selected = interactive && selectedId === node.id const selected = interactive && selectedId === node.id
const layout = splitLayoutClasses(node.props.className)
// Tasinan siniflar dugumden dusurulur, yoksa genislik/bosluk iki kez uygulanir.
const renderedNode = layout.hoisted
? { ...node, props: { ...node.props, className: layout.remaining } }
: node
// A nested node can always move: at the edge of its container it is lifted out. // A nested node can always move: at the edge of its container it is lifted out.
// Only the first/last node at the root has nowhere left to go. // Only the first/last node at the root has nowhere left to go.
const canMoveUp = !isRoot || index > 0
const canMoveDown = !isRoot || index < siblingCount - 1
const acceptsDroppedChildren = [ const acceptsDroppedChildren = [
'PageContainer', 'PageContainer',
'FlexRow', 'FlexRow',
@ -1633,8 +1716,6 @@ const NodeView = ({
<NodeView <NodeView
key={`${child.id}_${itemIndex}`} key={`${child.id}_${itemIndex}`}
node={child} node={child}
index={childIndex}
siblingCount={node.children.length}
selectedId={selectedId} selectedId={selectedId}
interactive={interactive} interactive={interactive}
renderCustomComponent={renderCustomComponent} renderCustomComponent={renderCustomComponent}
@ -1645,10 +1726,7 @@ const NodeView = ({
onDropComponent={onDropComponent} onDropComponent={onDropComponent}
onDropComponentBeside={onDropComponentBeside} onDropComponentBeside={onDropComponentBeside}
onMoveIntoContainer={onMoveIntoContainer} onMoveIntoContainer={onMoveIntoContainer}
onMove={onMove}
onReorder={onReorder} onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange} onNodePropChange={onNodePropChange}
/> />
)), )),
@ -1729,8 +1807,6 @@ const NodeView = ({
<NodeView <NodeView
key={child.id} key={child.id}
node={child} node={child}
index={childIndex}
siblingCount={tabNodes.length}
selectedId={selectedId} selectedId={selectedId}
interactive={interactive} interactive={interactive}
renderCustomComponent={renderCustomComponent} renderCustomComponent={renderCustomComponent}
@ -1741,10 +1817,7 @@ const NodeView = ({
onDropComponent={onDropComponent} onDropComponent={onDropComponent}
onDropComponentBeside={onDropComponentBeside} onDropComponentBeside={onDropComponentBeside}
onMoveIntoContainer={onMoveIntoContainer} onMoveIntoContainer={onMoveIntoContainer}
onMove={onMove}
onReorder={onReorder} onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange} onNodePropChange={onNodePropChange}
/> />
))} ))}
@ -1824,8 +1897,6 @@ const NodeView = ({
<NodeView <NodeView
key={child.id} key={child.id}
node={child} node={child}
index={childIndex}
siblingCount={node.children.length}
selectedId={selectedId} selectedId={selectedId}
interactive={interactive} interactive={interactive}
renderCustomComponent={renderCustomComponent} renderCustomComponent={renderCustomComponent}
@ -1836,10 +1907,7 @@ const NodeView = ({
onDropComponent={onDropComponent} onDropComponent={onDropComponent}
onDropComponentBeside={onDropComponentBeside} onDropComponentBeside={onDropComponentBeside}
onMoveIntoContainer={onMoveIntoContainer} onMoveIntoContainer={onMoveIntoContainer}
onMove={onMove}
onReorder={onReorder} onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange} onNodePropChange={onNodePropChange}
/> />
))} ))}
@ -1983,7 +2051,9 @@ const NodeView = ({
return ( return (
<DesignerDraggedSubtreeContext.Provider value={insideDraggedSubtree}> <DesignerDraggedSubtreeContext.Provider value={insideDraggedSubtree}>
<div <div
className={`group/node relative min-h-[28px] rounded-md ${refHidden ? 'opacity-40' : ''} ${ className={`group/node rounded-md ${layout.positioned ? '' : 'relative'} ${
interactive && !layout.positioned ? 'min-h-[28px]' : ''
} ${layout.hoisted} ${refHidden ? 'opacity-40' : ''} ${
// Sürüklenen düğümün kendisi soluklaşır; nereye gittiği hedeften okunur. // Sürüklenen düğümün kendisi soluklaşır; nereye gittiği hedeften okunur.
isDragSource ? 'opacity-40' : '' isDragSource ? 'opacity-40' : ''
} ${ } ${
@ -2001,7 +2071,11 @@ const NodeView = ({
}`} }`}
data-designer-node={node.id} data-designer-node={node.id}
draggable={interactive} draggable={interactive}
onClickCapture={() => { onClickCapture={(event) => {
// Kanvas gercek sayfa degildir: bir baglantiya tiklamak tasarimciyi terk etmemeli.
// Yakalama evresinde durdurulur; react-router'in Link'i de `defaultPrevented`
// gorunce gezinmez, tiklama yalnizca secim anlamina gelir.
if ((event.target as HTMLElement).closest?.('a[href]')) event.preventDefault()
if (interactive) onSelect?.(node.id) if (interactive) onSelect?.(node.id)
}} }}
onClick={(event) => { onClick={(event) => {
@ -2045,12 +2119,6 @@ const NodeView = ({
<div className="absolute inset-x-0 -bottom-2 z-20 h-2" {...edgeStripProps('after')} /> <div className="absolute inset-x-0 -bottom-2 z-20 h-2" {...edgeStripProps('after')} />
</> </>
)} )}
{/*
Taşıma tutamağı. Bir Input ya da Select'in gövdesi tamamen kontrolle
kaplıdır ve tarayıcı oradan üst öğenin sürüklenmesini başlatmaz; tutamak
her bileşen için garanti bir tutma noktası verir. Sürükleme sürerken
gizlenir, aksi halde komşu düğümün bırakma alanını kapatırdı.
*/}
{/* Yanına bırakma çizgisi: bileşen tam olarak buraya girer. */} {/* Yanına bırakma çizgisi: bileşen tam olarak buraya girer. */}
{dropHint === 'before' && ( {dropHint === 'before' && (
<div className="pointer-events-none absolute inset-x-0 -top-1 z-30 flex items-center gap-1"> <div className="pointer-events-none absolute inset-x-0 -top-1 z-30 flex items-center gap-1">
@ -2079,79 +2147,26 @@ const NodeView = ({
{translate('::App.VisualDesignerCanvas.CannotMoveHere')} {translate('::App.VisualDesignerCanvas.CannotMoveHere')}
</span> </span>
)} )}
{interactive && (
<div
className={`absolute right-1 top-1 z-20 items-center overflow-hidden rounded-md bg-sky-600 text-white shadow-lg ${selected ? 'flex' : 'hidden'}`}
>
{/* {/*
Taşıma tutamağı. Bir Input ya da Select'in gövdesi tamamen canlı Tasima tutamagi. Bir Input ya da Select'in govdesi tamamen canli kontrolle
kontrolle kaplıdır ve tarayıcı oradan üst öğenin sürüklenmesini kaplidir ve tarayici oradan ust ogenin suruklenmesini baslatmaz; bu tutamak
başlatmaz; bu rozet her bileşen için garanti bir tutma noktası verir. her bilesen icin garanti bir tutma noktasi verir. Eskiden burada tasima ve
sil/kopyala dugmelerini de tasiyan genis bir serit vardi; kucuk bir ikonun ya
da ayirici cizginin ustunu tamamen ortuyordu. Dugmeler sag paneldeki secili
bilesen basligina tasindi, kanvasta yalnizca kose tutamagi kaldi.
*/} */}
{interactive && (
<span <span
draggable draggable
className="flex cursor-grab items-center gap-1 px-2 text-[10px] font-semibold active:cursor-grabbing" className={`absolute -right-2 -top-2 z-30 h-4 w-4 cursor-grab items-center justify-center rounded-full bg-sky-600 text-[8px] text-white shadow ring-1 ring-white active:cursor-grabbing dark:ring-gray-900 ${selected ? 'flex' : 'hidden'}`}
title={translate('::App.VisualDesignerCanvas.DragToMove', { title={translate('::App.VisualDesignerCanvas.DragToMove', {
name: node.ref || node.type, name: node.ref || node.type,
})} })}
onDragStart={startNodeDrag}
onDragEnd={() => endDesignerDrag()} onDragEnd={() => endDesignerDrag()}
onDragStart={startNodeDrag}
> >
<FaGripVertical /> {node.type} <FaGripVertical />
</span> </span>
<button
className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40"
disabled={!canMoveUp}
title={translate(
canMoveUp ? '::App.Platform.MoveUp' : '::App.VisualDesignerCanvas.AlreadyAtTop',
)}
type="button"
onClick={(event) => {
event.stopPropagation()
onMove?.(node.id, -1)
}}
>
<FaArrowUp />
</button>
<button
className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40"
disabled={!canMoveDown}
title={translate(
canMoveDown
? '::App.Platform.MoveDown'
: '::App.VisualDesignerCanvas.AlreadyAtBottom',
)}
type="button"
onClick={(event) => {
event.stopPropagation()
onMove?.(node.id, 1)
}}
>
<FaArrowDown />
</button>
<button
className="p-1.5 hover:bg-sky-700"
title={translate('::App.Platform.Duplicate')}
type="button"
onClick={(event) => {
event.stopPropagation()
onDuplicate?.(node.id)
}}
>
<FaClone />
</button>
<button
className="p-1.5 hover:bg-red-600"
title={translate('::App.Platform.Delete')}
type="button"
onClick={(event) => {
event.stopPropagation()
onDelete?.(node.id)
}}
>
<FaTrash />
</button>
</div>
)} )}
{interactive && refHidden && ( {interactive && refHidden && (
<span className="absolute left-1 top-1 z-10 rounded bg-slate-700 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-white"> <span className="absolute left-1 top-1 z-10 rounded bg-slate-700 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-white">
@ -2163,7 +2178,7 @@ const NodeView = ({
resetKey={JSON.stringify([node.props, node.bindings, node.events])} resetKey={JSON.stringify([node.props, node.bindings, node.events])}
> >
{renderElement( {renderElement(
node, renderedNode,
contentChildren, contentChildren,
dataValues, dataValues,
currentItem, currentItem,
@ -2178,6 +2193,10 @@ const NodeView = ({
</PreviewBoundary> </PreviewBoundary>
{/* Tabs has a drop zone inside every tab, so it needs no outer placeholder. */} {/* Tabs has a drop zone inside every tab, so it needs no outer placeholder. */}
{interactive && {interactive &&
// Bos bir konteynerin "buraya birak" kutusu her zaman gorunurse ayirici cizgi
// gibi kucuk ogeler tasarim yuzeyinde sisip duruyor; yalnizca surukleme
// sirasinda ya da dugum seciliyken gosterilir.
(dragging || selected) &&
!hasVisibleChildren && !hasVisibleChildren &&
acceptsDroppedChildren && acceptsDroppedChildren &&
node.type !== 'Card' && node.type !== 'Card' &&
@ -2191,7 +2210,6 @@ const NodeView = ({
{translate('::App.VisualDesignerCanvas.DropComponentHere')} {translate('::App.VisualDesignerCanvas.DropComponentHere')}
</div> </div>
)} )}
<span className="hidden">{index}</span>
</div> </div>
</DesignerDraggedSubtreeContext.Provider> </DesignerDraggedSubtreeContext.Provider>
) )
@ -2205,10 +2223,7 @@ const VisualCanvas = ({
onDropComponent, onDropComponent,
onDropComponentBeside, onDropComponentBeside,
onMoveIntoContainer, onMoveIntoContainer,
onMove,
onReorder, onReorder,
onDuplicate,
onDelete,
onNodePropChange, onNodePropChange,
renderCustomComponent, renderCustomComponent,
dataValues = {}, dataValues = {},
@ -2277,9 +2292,11 @@ const VisualCanvas = ({
<CanvasWidthContext.Provider value={canvasWidth}> <CanvasWidthContext.Provider value={canvasWidth}>
<DesignerRefContext.Provider value={refStore}> <DesignerRefContext.Provider value={refStore}>
<div <div
className={`min-h-full p-8 text-slate-900 transition dark:text-slate-100 ${ className={`min-h-full text-slate-900 transition dark:text-slate-100 ${
drag && interactive ? 'bg-sky-50/40 dark:bg-sky-950/20' : '' // Cihaz secildiginde kanvas o cihazin ekranidir: sayfa kenardan kenara cizilir,
}`} // canlida olmayan bir tasarim yuzeyi bosluguyla daralmaz.
canvasWidth === 'responsive' ? 'p-8' : ''
} ${drag && interactive ? 'bg-sky-50/40 dark:bg-sky-950/20' : ''}`}
onClick={() => interactive && onSelect?.('')} onClick={() => interactive && onSelect?.('')}
onDragOver={(event) => interactive && event.preventDefault()} onDragOver={(event) => interactive && event.preventDefault()}
onDrop={(event) => { onDrop={(event) => {
@ -2307,15 +2324,22 @@ const VisualCanvas = ({
</span> </span>
</div> </div>
)} )}
{/*
Transform sayfa kutusunu `position: fixed` icin kapsayan blok yapar, boylece
sabitlenen bir dugum tarayici penceresine degil sayfa genisligine yayilir.
Dugumun kendi `fixed` sinifi akisa cevriliyor (`canvasPositionClass`) ama bir
bilesen bunu baska bir proptan getirebilir PublicHeaderBar kaydirilinca
`scrolledClassName` ile `fixed` olur ve serit ozellik panelinin ustune binerdi.
*/}
{nodes.length ? ( {nodes.length ? (
<div className="space-y-4"> <div
{nodes.map((node, index) => ( className={canvasWidth === 'responsive' ? 'space-y-4' : ''}
style={{ transform: 'translateZ(0)' }}
>
{nodes.map((node) => (
<NodeView <NodeView
key={node.id} key={node.id}
isRoot
node={node} node={node}
index={index}
siblingCount={nodes.length}
selectedId={selectedId} selectedId={selectedId}
interactive={interactive} interactive={interactive}
renderCustomComponent={renderCustomComponent} renderCustomComponent={renderCustomComponent}
@ -2324,10 +2348,7 @@ const VisualCanvas = ({
onDropComponent={onDropComponent} onDropComponent={onDropComponent}
onDropComponentBeside={onDropComponentBeside} onDropComponentBeside={onDropComponentBeside}
onMoveIntoContainer={onMoveIntoContainer} onMoveIntoContainer={onMoveIntoContainer}
onMove={onMove}
onReorder={onReorder} onReorder={onReorder}
onDuplicate={onDuplicate}
onDelete={onDelete}
onNodePropChange={onNodePropChange} onNodePropChange={onNodePropChange}
/> />
))} ))}

View file

@ -30,7 +30,7 @@ interface GeneratedComponentMetadata {
} }
const generatedMetadata = generatedComponentProps as Record<string, GeneratedComponentMetadata> const generatedMetadata = generatedComponentProps as Record<string, GeneratedComponentMetadata>
const CONTAINER_NAMES = new Set(['div', 'Card', 'FormContainer', 'Table', 'Tabs', 'Timeline']) const CONTAINER_NAMES = new Set(['a', 'div', 'Card', 'FormContainer', 'Table', 'Tabs', 'Timeline'])
const getToolboxGroup = (name: string): DesignerComponentDefinition['toolboxGroup'] => const getToolboxGroup = (name: string): DesignerComponentDefinition['toolboxGroup'] =>
name === 'Table' ? 'layout' : DESIGNER_DATA_COMPONENT_NAMES.has(name) ? 'data' : 'ui' name === 'Table' ? 'layout' : DESIGNER_DATA_COMPONENT_NAMES.has(name) ? 'data' : 'ui'
/** Components intentionally kept out of the toolbox. */ /** Components intentionally kept out of the toolbox. */
@ -875,6 +875,122 @@ const platformDefinition = (
hooks: [], hooks: [],
}) })
/**
* Public site basliginin parcalari. Menu, tema, dil ve demo davranisi bilesenin
* icinde yasar; tasarimci yerlesimi ve siniflari belirler. Kanvas ve uretilen kod
* ayni bilesenleri kullanir (`components/publicSite`).
*/
export const PUBLIC_SITE_COMPONENTS: DesignerComponentDefinition[] = [
{
name: 'PublicHeaderBar',
icon: 'PanelTop',
category: 'layout',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicHeaderBar.PublicHeaderBarDescription',
acceptsChildren: true,
properties: [
{ name: 'className', type: 'string', value: '', category: 'styling' },
{ name: 'scrolledClassName', type: 'string', value: '', category: 'styling' },
{ name: 'scrollThreshold', type: 'number', value: 10, category: 'properties' },
],
hooks: [],
},
{
name: 'PublicNav',
icon: 'Menu',
category: 'basic',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicNav.PublicNavDescription',
properties: [
{
name: 'source',
type: 'select',
value: 'navigation',
options: ['navigation', 'actions', 'all'],
category: 'properties',
},
{
name: 'variant',
type: 'select',
value: 'horizontal',
options: ['horizontal', 'vertical'],
category: 'properties',
},
{ name: 'showIcons', type: 'boolean', value: true, category: 'properties' },
{ name: 'iconSize', type: 'number', value: 0, category: 'styling' },
{ name: 'className', type: 'string', value: '', category: 'styling' },
{ name: 'itemClassName', type: 'string', value: '', category: 'styling' },
{ name: 'activeItemClassName', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'PublicMobileMenu',
icon: 'PanelLeft',
category: 'layout',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicMobileMenu.PublicMobileMenuDescription',
acceptsChildren: true,
properties: [
{ name: 'className', type: 'string', value: '', category: 'styling' },
{ name: 'buttonClassName', type: 'string', value: '', category: 'styling' },
{ name: 'panelClassName', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'PublicLogo',
icon: 'Image',
category: 'basic',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicLogo.PublicLogoDescription',
properties: [
{ name: 'className', type: 'string', value: '', category: 'styling' },
{ name: 'imgClassName', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'PublicThemeToggle',
icon: 'Sun',
category: 'basic',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicThemeToggle.PublicThemeToggleDescription',
properties: [
{ name: 'className', type: 'string', value: '', category: 'styling' },
{ name: 'buttonClassName', type: 'string', value: '', category: 'styling' },
{ name: 'activeClassName', type: 'string', value: '', category: 'styling' },
{ name: 'iconSize', type: 'number', value: 15, category: 'styling' },
],
hooks: [],
},
{
name: 'PublicLanguageSelector',
icon: 'Globe',
category: 'basic',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicLanguageSelector.PublicLanguageSelectorDescription',
properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }],
hooks: [],
},
{
name: 'PublicDemoButton',
icon: 'Play',
category: 'basic',
kind: 'custom',
toolboxGroup: 'platform',
description: 'App.CatalogPublicDemoButton.PublicDemoButtonDescription',
properties: [{ name: 'className', type: 'string', value: '', category: 'styling' }],
hooks: [],
},
]
export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [ export const PLATFORM_COMPONENTS: DesignerComponentDefinition[] = [
platformDefinition('ListView', 'List', 'App.CatalogListView.ListViewDescription'), platformDefinition('ListView', 'List', 'App.CatalogListView.ListViewDescription'),
platformDefinition('DataGridView', 'Grid', 'App.CatalogDataGridView.DataGridViewDescription'), platformDefinition('DataGridView', 'Grid', 'App.CatalogDataGridView.DataGridViewDescription'),
@ -1085,6 +1201,22 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
], ],
hooks: [], hooks: [],
}, },
{
// Ikonlar ada gore cizilir (menu ikonlariyla ayni sozluk); kanvas ve uretilen
// kod ayni `PlatformIcon` bilesenini kullanir.
name: 'PlatformIcon',
icon: 'Image',
category: 'basic',
kind: 'custom',
toolboxGroup: 'ui',
description: 'App.CatalogPlatformIcon.PlatformIconDescription',
properties: [
{ name: 'name', type: 'string', value: 'FaUser', category: 'properties' },
{ name: 'size', type: 'number', value: 24, category: 'styling' },
{ name: 'className', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{ {
name: 'Spacer', name: 'Spacer',
icon: 'Space', icon: 'Space',
@ -1146,6 +1278,7 @@ export const getDesignerCatalog = (customNames: string[] = []): DesignerComponen
...DESIGNER_EXTRAS, ...DESIGNER_EXTRAS,
FORM_DEFINITION, FORM_DEFINITION,
...PLATFORM_COMPONENTS, ...PLATFORM_COMPONENTS,
...PUBLIC_SITE_COMPONENTS,
...html, ...html,
...ui, ...ui,
...generatedOnlyUi, ...generatedOnlyUi,

View file

@ -4,18 +4,6 @@ export const AI_ASSISTANT = 'App.Definitions.AiBot.Asistant'
/** Veritabanı migration + seed tetikleme yetkisi (host tarafı). */ /** Veritabanı migration + seed tetikleme yetkisi (host tarafı). */
export const DB_MIGRATE = 'App.Setup.Migrate' export const DB_MIGRATE = 'App.Setup.Migrate'
/**
* Public site sayfalarının tasarım modu yetkileri.
* Sunucuda `PermissionsData.json` içinde tanımlıdır ve seeder ile yönetilir;
* asıl kontrol `PublicAppService.Save*PageAsync` üzerindedir.
*/
export const PUBLIC_PAGE_DESIGN = {
HOME: 'App.Home.Design',
ABOUT: 'App.About.Design',
SERVICES: 'App.Services.Design',
CONTACT: 'App.Contact.Design',
} as const
/** /**
* Wizard dosya yöneticisi yetkileri. `PermissionsData.json` içinde tanımlıdır ve seeder ile * Wizard dosya yöneticisi yetkileri. `PermissionsData.json` içinde tanımlıdır ve seeder ile
* yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol * yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol

View file

@ -222,11 +222,20 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
try { try {
// Babel and the UI kit are only needed when an active runtime component // Babel and the UI kit are only needed when an active runtime component
// exists. Keep them out of the application startup bundle. // exists. Keep them out of the application startup bundle.
const [Babel, ui, { default: axios }, { default: PlatformViewHost }] = await Promise.all([ const [
Babel,
ui,
{ default: axios },
{ default: PlatformViewHost },
{ RUNTIME_NODE_COMPONENTS },
] = await Promise.all([
import('@babel/standalone'), import('@babel/standalone'),
import('../components/ui'), import('../components/ui'),
import('axios'), import('axios'),
import('../components/componentEditor/PlatformViewHost'), import('../components/componentEditor/PlatformViewHost'),
// Katalog dugumleri: react-icons ve public site parcalari runtime
// kapsaminda degildir, kanvasin cizdigi bilesenler buradan gecirilir.
import('../components/componentEditor/runtimeNodes'),
]) ])
if (cancelled) return if (cancelled) return
@ -235,6 +244,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
...ui, ...ui,
UiKit: ui, UiKit: ui,
PlatformViewHost, PlatformViewHost,
...RUNTIME_NODE_COMPONENTS,
apiService, apiService,
DOMPurify, DOMPurify,
axios, axios,

View file

@ -8,16 +8,10 @@ const DemoContext = createContext<DemoContextValue | undefined>(undefined)
export const DemoProvider = DemoContext.Provider export const DemoProvider = DemoContext.Provider
export const useDemo = () => { /**
const context = useContext(DemoContext) * Demo penceresini acan cagri. Saglayici disinda cizilen bileşen (tasarimci kanvasi)
* de ayni dugumu kullanabilsin diye eksik context bir hataya degil, bos cagriya doner.
if (!context) { */
throw new Error('useDemo must be used within a DemoProvider')
}
return context
}
export const useOptionalDemo = () => { export const useOptionalDemo = () => {
const context = useContext(DemoContext) const context = useContext(DemoContext)

View file

@ -1,6 +0,0 @@
// ScrollContext.tsx
import { createContext, useContext } from 'react'
export const ScrollContext = createContext<boolean>(false)
export const useScroll = () => useContext(ScrollContext)

View file

@ -1,22 +0,0 @@
export interface AboutDto {
id: string
tenantId?: string
statsDto: StatDto[]
descriptionsDto: string[]
sectionsDto: SectionDto[]
}
export interface StatDto {
icon: string
value: string
labelKey: string
useCounter?: boolean
counterEnd?: string
counterSuffix?: string
counterDuration?: number
}
export interface SectionDto {
key: string
descKey: string
}

View file

@ -1,39 +0,0 @@
export interface ContactDto {
id: string
address: string
phoneNumber: string
email: string
location: string
taxNumber: string
bankDto: BankDto
workHoursDto: WorkHoursDto
mapDto: MapDto
}
export interface BankDto {
accountHolder: string
branch: string
accountNumber: string
iban: string
styleClass?: string
}
export interface WorkHoursDto {
weekday: string
weekend: string
whatsapp: string
styleClass?: string
}
export interface MapDto {
title: string
src: string
width: string
height: string
style?: string
containerStyleClass?: string
frameStyleClass?: string
allowFullScreen?: boolean
loading: string
referrerPolicy: string
}

View file

@ -25,6 +25,15 @@ const runtimeComponentCache = new Map<string, React.ComponentType<any>>()
// time instead. // time instead.
const runtimeRenderers = new Map<string, (name: string, props?: any) => React.ReactNode>() const runtimeRenderers = new Map<string, (name: string, props?: any) => React.ReactNode>()
/**
* Rota kaydı, karşılığı olan dosya kaldırıldıktan sonra da veritabanında kalabilir
* (seeder yalnızca ekler). Böyle bir kayıt router'a alınmaz; yolu bir custom component
* devraldıysa o çizilir, devralmadıysa NotFound gösterilir.
*/
export function hasPhysicalComponent(componentPath: string) {
return `../${componentPath.replace(/^@\//, '')}.tsx` in modules
}
// Fiziksel komponent yükleme (mevcut mantık) // Fiziksel komponent yükleme (mevcut mantık)
function loadPhysicalComponent(componentPath: string) { function loadPhysicalComponent(componentPath: string) {
const cleanedPath = componentPath.replace(/^@\//, '') const cleanedPath = componentPath.replace(/^@\//, '')
@ -124,7 +133,9 @@ export interface DynamicReactRoute {
// API'den gelen route objesini, React Router için uygun hale getirir // API'den gelen route objesini, React Router için uygun hale getirir
export function mapDynamicRoutes(routes: RouteDto[]): DynamicReactRoute[] { export function mapDynamicRoutes(routes: RouteDto[]): DynamicReactRoute[] {
return routes.map((route) => ({ return routes
.filter((route) => route.componentType === 'dynamic' || hasPhysicalComponent(route.componentPath))
.map((route) => ({
key: route.path, key: route.path,
path: route.path, path: route.path,
getComponent: (registeredComponents, renderComponent, isComponentRegistered) => getComponent: (registeredComponents, renderComponent, isComponentRegistered) =>

View file

@ -62,10 +62,18 @@ export const DynamicRouter: React.FC = () => {
useComponents() useComponents()
const location = useLocation() const location = useLocation()
const dynamicRoutes = React.useMemo( const dynamicRoutes = React.useMemo(() => {
() => [...mapDynamicRoutes(routes), ...mapCustomComponentRoutes(components)], const customComponentRoutes = mapCustomComponentRoutes(components)
[routes, components], const claimedPaths = new Set(customComponentRoutes.map((route) => route.path))
)
// Ayni yolu iki route paylasirsa custom component kazanir: bir ekranin fiziksel
// sayfadan runtime bilesene devri (ve bileseni pasife alarak geri donusu) yalnizca
// Custom Components ekranindan yonetilir, rota kaydini silmeyi gerektirmez.
return [
...mapDynamicRoutes(routes).filter((route) => !claimedPaths.has(route.path)),
...customComponentRoutes,
]
}, [routes, components])
// /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı. // /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı.
// bootstrapFailed durumu Layouts'ta, layout seçilmeden önce ele alınır. // bootstrapFailed durumu Layouts'ta, layout seçilmeden önce ele alınır.

View file

@ -2,18 +2,11 @@
setup: '/setup', setup: '/setup',
public: { public: {
home: '/home', home: '/home',
about: '/about',
aboutDesigner: '/about/designer',
products: '/products', products: '/products',
checkout: '/checkout', checkout: '/checkout',
payment: '/payment', payment: '/payment',
success: '/success', success: '/success',
services: '/services',
servicesDesigner: '/services/designer',
blog: '/blog', blog: '/blog',
blogDetail: '/blog/:id',
demo: '/demo',
contact: '/contact',
accessDenied: '/access-denied', accessDenied: '/access-denied',
}, },
authenticated: { authenticated: {

View file

@ -1,60 +0,0 @@
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[]
styleTexts: SaveLocalizedTextInput[]
}
export function getAbout() {
return apiService.fetchData<AboutDto>(
{
method: 'GET',
url: '/api/app/public/about',
},
{ 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' },
)
}

View file

@ -1,73 +0,0 @@
import apiService from './api.service'
import { ContactDto } from '@/proxy/contact/models'
export interface SaveLocalizedTextInput {
key: string
value: string
}
export function getContact() {
return apiService.fetchData<ContactDto>(
{
method: 'GET',
url: '/api/app/public/contact',
},
{ apiName: 'Default' },
)
}
export interface SaveContactPageInput extends Record<string, unknown> {
cultureName: string
heroTitleKey: string
heroTitleValue: string
heroSubtitleKey: string
heroSubtitleValue: string
heroImageKey: string
heroImageValue: string
contactInfoTitleKey: string
contactInfoTitleValue: string
addressKey: string
addressValue: string
phoneNumber: string
email: string
location: string
taxNumber: string
bankTitleKey: string
bankTitleValue: string
bankAccountHolder: string
bankBranch: string
bankAccountNumber: string
bankIban: string
bankStyleClass: string
workHoursTitleKey: string
workHoursTitleValue: string
workWeekdayKey: string
workWeekdayValue: string
workWeekendKey: string
workWeekendValue: string
workWhatsappKey: string
workWhatsappValue: string
workHoursStyleClass: string
mapTitleKey: string
mapTitleValue: string
mapSrc: string
mapWidth: string
mapHeight: string
mapAllowFullScreen: boolean
mapLoading: string
mapReferrerPolicy: string
mapContainerStyleClass: string
mapFrameStyleClass: string
styleTexts: SaveLocalizedTextInput[]
}
export function saveContactPage(input: SaveContactPageInput) {
return apiService.fetchData<void>(
{
method: 'POST',
url: '/api/app/public/save-contact-page',
data: input,
},
{ apiName: 'Default' },
)
}

View file

@ -0,0 +1,30 @@
import apiService from './api.service'
export interface CountryDto {
id: string;
name: string;
groupName: string;
currency: string;
phoneCode: number;
taxLabel: string;
zipRequired: boolean;
stateRequired: boolean;
cities: CityDto[];
}
export interface CityDto {
id: string;
name: string;
}
export function getCountry() {
return apiService.fetchData<CountryDto[]>(
{
method: 'GET',
url: '/api/app/public/country',
},
{ apiName: 'Default' },
)
}

View file

@ -1,179 +0,0 @@
import apiService from './api.service'
export interface HomeSlideServiceDto {
icon: string
titleKey: string
descriptionKey: string
styleClass?: string
}
export interface HomeSlideDto {
titleKey: string
subtitleKey: string
styleClass?: string
services: HomeSlideServiceDto[]
}
export interface HomeFeatureDto {
icon: string
titleKey: string
descriptionKey: string
styleClass?: string
}
export interface HomeSolutionDto {
icon: string
colorClass: string
titleKey: string
descriptionKey: string
styleClass?: string
}
export interface HomeDto {
id: string
heroBackgroundImageKey: string
heroPrimaryCtaKey: string
heroSecondaryCtaKey: string
featuresTitleKey: string
featuresSubtitleKey: string
solutionsTitleKey: string
solutionsSubtitleKey: string
ctaTitleKey: string
ctaSubtitleKey: string
ctaButtonLabelKey: string
slidesDto: HomeSlideDto[]
featuresDto: HomeFeatureDto[]
solutionsDto: HomeSolutionDto[]
}
export interface SaveHomeSlideServiceInput {
icon: string
titleKey: string
titleValue: string
descriptionKey: string
descriptionValue: string
styleClass: string
}
export interface SaveHomeSlideInput {
titleKey: string
titleValue: string
subtitleKey: string
subtitleValue: string
styleClass: string
services: SaveHomeSlideServiceInput[]
}
export interface SaveHomeFeatureInput {
icon: string
titleKey: string
titleValue: string
descriptionKey: string
descriptionValue: string
styleClass: string
}
export interface SaveHomeSolutionInput {
icon: string
colorClass: string
titleKey: string
titleValue: string
descriptionKey: string
descriptionValue: string
styleClass: string
}
export interface SaveHomePageInput {
cultureName: string
heroBackgroundImageKey: string
heroBackgroundImageValue: string
heroPrimaryCtaKey: string
heroPrimaryCtaValue: string
heroPrimaryCtaStyleKey: string
heroPrimaryCtaStyleValue: string
heroSecondaryCtaKey: string
heroSecondaryCtaValue: string
heroSecondaryCtaStyleKey: string
heroSecondaryCtaStyleValue: string
featuresTitleKey: string
featuresTitleValue: string
featuresTitleStyleKey: string
featuresTitleStyleValue: string
featuresSubtitleKey: string
featuresSubtitleValue: string
featuresSubtitleStyleKey: string
featuresSubtitleStyleValue: string
solutionsTitleKey: string
solutionsTitleValue: string
solutionsTitleStyleKey: string
solutionsTitleStyleValue: string
solutionsSubtitleKey: string
solutionsSubtitleValue: string
solutionsSubtitleStyleKey: string
solutionsSubtitleStyleValue: string
ctaTitleKey: string
ctaTitleValue: string
ctaTitleStyleKey: string
ctaTitleStyleValue: string
ctaSubtitleKey: string
ctaSubtitleValue: string
ctaSubtitleStyleKey: string
ctaSubtitleStyleValue: string
ctaButtonLabelKey: string
ctaButtonLabelValue: string
ctaButtonStyleKey: string
ctaButtonStyleValue: string
slides: SaveHomeSlideInput[]
features: SaveHomeFeatureInput[]
solutions: SaveHomeSolutionInput[]
}
export interface CountryDto {
id: string;
name: string;
groupName: string;
currency: string;
phoneCode: number;
taxLabel: string;
zipRequired: boolean;
stateRequired: boolean;
cities: CityDto[];
}
export interface CityDto {
id: string;
name: string;
}
export function getHome() {
return apiService.fetchData<HomeDto>(
{
method: 'GET',
url: '/api/app/public/home',
},
{ apiName: 'Default' },
)
}
export function getCountry() {
return apiService.fetchData<CountryDto[]>(
{
method: 'GET',
url: '/api/app/public/country',
},
{ apiName: 'Default' },
)
}
export function saveHomePage(input: SaveHomePageInput) {
return apiService.fetchData<void, SaveHomePageInput>(
{
method: 'POST',
url: '/api/app/public/save-home-page',
data: input,
},
{ apiName: 'Default' },
)
}

View file

@ -1,61 +0,0 @@
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[]
styleTexts: SaveLocalizedTextInput[]
}
export function getServices() {
return apiService.fetchData<ServiceDto[]>(
{
method: 'GET',
url: '/api/app/public/services-list',
},
{ 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' },
)
}

View file

@ -6,6 +6,7 @@ import appConfig from '../proxy/configs/app.config'
import { NavigationTree } from '../proxy/menus/navigation' import { NavigationTree } from '../proxy/menus/navigation'
import { MenuDto } from '../proxy/menus/models' import { MenuDto } from '../proxy/menus/models'
import getChildren from '../utils/navigation' import getChildren from '../utils/navigation'
import { PUBLIC_MENU_MODULE_ID } from '../utils/hooks/usePublicMenu'
import { import {
ApplicationConfigurationDto, ApplicationConfigurationDto,
ApplicationLocalizationRequestDto, ApplicationLocalizationRequestDto,
@ -108,12 +109,17 @@ export const abpConfigModel: AbpConfigModel = {
const texts = getState().texts const texts = getState().texts
const defaultResourceName = getState().config?.localization.defaultResourceName const defaultResourceName = getState().config?.localization.defaultResourceName
result.data.items?.forEach((menu: MenuDto) => { // Public site menusu ziyaretci duzenine aittir; yonetim navigasyonunda yer almaz.
const items = (result.data.items ?? []).filter(
(menu: MenuDto) => menu.moduleId !== PUBLIC_MENU_MODULE_ID,
)
items.forEach((menu: MenuDto) => {
const displayName = '::' + menu.displayName const displayName = '::' + menu.displayName
menu.displayName = getLocalization(texts || {}, defaultResourceName, displayName) menu.displayName = getLocalization(texts || {}, defaultResourceName, displayName)
}) })
const menu = getChildren(result.data.items ?? [], null) const menu = getChildren(items, null)
actions.setMenu(menu) actions.setMenu(menu)
}), }),
} }

View file

@ -0,0 +1,65 @@
import { useMemo } from 'react'
import { useStoreState } from '@/store'
/**
* Public site menu kaydi. Menuler `Pub` modulu altinda tutulur ve uygulama
* konfigurasyonunun `publicMenus` alaniyla tasinir; menu ucu `[Authorize]`
* oldugu icin ziyaretci ayri bir istek atamaz.
*/
export interface PublicMenuItem {
code: string
displayName?: string
url?: string
icon?: string
order: number
parentCode?: string
cssClass?: string
target?: string
requiredPermissionName?: string
}
export interface PublicMenuNode extends PublicMenuItem {
children: PublicMenuNode[]
}
/** Ust menude bu sinifi tasiyan kayit duz baglanti yerine vurgulu buton olarak cizilir. */
export const PUBLIC_MENU_CTA_CLASS = 'cta'
/** Public site menulerinin modulu; yonetim navigasyonu bu modulu disarida birakir. */
export const PUBLIC_MENU_MODULE_ID = 'Pub'
/** Ust ve alt bilgiyi cizen Custom Component'lerin adlari. */
export const PUBLIC_HEADER_COMPONENT = 'PublicHeader'
export const PUBLIC_FOOTER_COMPONENT = 'PublicFooter'
export const PUBLIC_MENU_HEADER_CODE = 'App.Public.Header'
function buildChildren(
items: PublicMenuItem[],
parentCode: string,
isVisible: (item: PublicMenuItem) => boolean,
): PublicMenuNode[] {
return items
.filter((item) => item.parentCode === parentCode && isVisible(item))
.sort((left, right) => left.order - right.order)
.map((item) => ({ ...item, children: buildChildren(items, item.code, isVisible) }))
}
/**
* Ust menu baglantilarini menu agacindan turetir. Alt bilgi kendi Custom Component'inde
* tasarlanir, menuden beslenmez.
*/
export function usePublicMenu() {
const publicMenus = useStoreState(
(state) => state.abpConfig.config?.extraProperties?.['publicMenus'],
) as PublicMenuItem[] | undefined
const grantedPolicies = useStoreState((state) => state.abpConfig.config?.auth?.grantedPolicies)
return useMemo(() => {
const items = Array.isArray(publicMenus) ? publicMenus : []
const isVisible = (item: PublicMenuItem) =>
!item.requiredPermissionName || grantedPolicies?.[item.requiredPermissionName] === true
return { headerLinks: buildChildren(items, PUBLIC_MENU_HEADER_CODE, isVisible) }
}, [publicMenus, grantedPolicies])
}

View file

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react'
/**
* Sayfa esigin otesine kaydirildi mi. Kabuk artik tasarlanabilir bir Custom
* Component oldugu icin durum bir context'ten degil, ihtiyaci olan her bilesenin
* kendi dinleyicisinden gelir.
*/
export const useScrolled = (threshold = 10) => {
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > threshold)
handleScroll()
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [threshold])
return scrolled
}

View file

@ -1,5 +1,4 @@
import { import {
AuthorizationTypeEnum,
ColumnCascadeFilterOperatorEnum, ColumnCascadeFilterOperatorEnum,
ColumnRowTypeListOptions, ColumnRowTypeListOptions,
ColumnSortDirectionEnum, ColumnSortDirectionEnum,
@ -29,19 +28,6 @@ import { enumToList } from '../../../../utils/enumUtils'
import { PREFIX } from '@/constants/theme.constant' import { PREFIX } from '@/constants/theme.constant'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
export enum WidgetColorEnum {
'Blue' = 'blue',
'Green' = 'green',
'Purple' = 'purple',
'Gray' = 'gray',
'Red' = 'red',
'Yellow' = 'yellow',
'Pink' = 'pink',
'Indigo' = 'indigo',
'Teal' = 'teal',
'Orange' = 'orange',
}
export const overflowWrapOptions = [ export const overflowWrapOptions = [
{ value: 'normal', label: 'Normal' }, { value: 'normal', label: 'Normal' },
{ value: 'break-word', label: 'Break Word' }, { value: 'break-word', label: 'Break Word' },
@ -150,21 +136,6 @@ export const customValueTypeOptions = [
{ value: 5, label: 'Sequence' }, { value: 5, label: 'Sequence' },
] ]
export const gridColumnCopyFieldTypeOptions = [
{ value: 0, label: 'All' },
{ value: 1, label: 'General' },
{ value: 2, label: 'Filter' },
{ value: 3, label: 'Header' },
{ value: 4, label: 'Group' },
{ value: 5, label: 'Customization' },
{ value: 6, label: 'GroupSummary' },
{ value: 7, label: 'TotalSummary' },
{ value: 8, label: 'Editing' },
{ value: 9, label: 'Lookup' },
{ value: 10, label: 'Validation' },
{ value: 11, label: 'Styling' },
]
export const itemTypeOptions = [ export const itemTypeOptions = [
{ value: 'button', label: 'Button' }, { value: 'button', label: 'Button' },
{ value: 'empty', label: 'Empty' }, { value: 'empty', label: 'Empty' },
@ -180,18 +151,6 @@ export const urlOptions = [
{ value: '_top', label: 'Top' }, { value: '_top', label: 'Top' },
] ]
export const joinConditionOptions = [
{ value: 'AND', label: 'AND' },
{ value: 'OR', label: 'OR' },
]
export type listFormCustomizationType = 'customization' | 'detail' | ''
export const columnLookupTagBoxApplyValueModeListOptions = [
{ value: 'instantly', label: 'Instantly' },
{ value: 'useButtons', label: 'Use Buttons' },
]
export const columnEditorTypeListOptions = [ export const columnEditorTypeListOptions = [
{ value: 'dxAutocomplete', label: 'dxAutocomplete' }, { value: 'dxAutocomplete', label: 'dxAutocomplete' },
{ value: 'dxCalendar', label: 'dxCalendar' }, { value: 'dxCalendar', label: 'dxCalendar' },
@ -527,7 +486,6 @@ export const dbSourceTypeOptions = enumToList(DbTypeEnum)
export const dataSourceTypeOptions = enumToList(DataSourceTypeEnum) export const dataSourceTypeOptions = enumToList(DataSourceTypeEnum)
export const selectCommandTypeOptions = enumToList(SelectCommandTypeEnum) export const selectCommandTypeOptions = enumToList(SelectCommandTypeEnum)
export const listFormCustomizationOptions = enumToList(ListFormCustomizationTypeEnum) export const listFormCustomizationOptions = enumToList(ListFormCustomizationTypeEnum)
export const authorizationTypeOptions = enumToList(AuthorizationTypeEnum)
export const sortDirectionOptions = enumToList<string>(SortDirectionEnum) export const sortDirectionOptions = enumToList<string>(SortDirectionEnum)
export const fixedPositionOptions = enumToList<string>(FixedPositionEnum) export const fixedPositionOptions = enumToList<string>(FixedPositionEnum)
export const columnSummaryTypeListOptions = enumToList<string>(SummaryTypeEnum) export const columnSummaryTypeListOptions = enumToList<string>(SummaryTypeEnum)
@ -546,7 +504,6 @@ export const cascadeFilterOperator = enumToList<string>(ColumnCascadeFilterOpera
export const pivotSettingsAreaOptions = enumToList<string>(PivotSettingsAreaEnum) export const pivotSettingsAreaOptions = enumToList<string>(PivotSettingsAreaEnum)
export const pivotSettingsGroupIntervalOptions = enumToList<string>(PivotSettingsGroupIntervalEnum) export const pivotSettingsGroupIntervalOptions = enumToList<string>(PivotSettingsGroupIntervalEnum)
export const pivotSortDirectionOptions = enumToList<string>(ColumnSortDirectionEnum) export const pivotSortDirectionOptions = enumToList<string>(ColumnSortDirectionEnum)
export const widgetColorOptions = enumToList<string>(WidgetColorEnum)
// Tema renk haritası (border ve fill) // Tema renk haritası (border ve fill)
const styleColorMap: Record< const styleColorMap: Record<
string, string,

View file

@ -1,5 +1,3 @@
export type ChartOperation = '' | 'select' | 'insert' | 'update' | 'delete'
export type ChartDialogType = '' | 'pane' | 'serie' | 'annotation' | 'axis'
export type ListViewLayoutType = export type ListViewLayoutType =
| 'grid' | 'grid'
| 'card' | 'card'

View file

@ -32,7 +32,7 @@ import {
import * as Yup from 'yup' import * as Yup from 'yup'
import { ProfileDto, UpdateProfileDto } from '@/proxy/account/models' import { ProfileDto, UpdateProfileDto } from '@/proxy/account/models'
import { getProfile, updateProfile } from '@/services/account.service' import { getProfile, updateProfile } from '@/services/account.service'
import { CountryDto, getCountry } from '@/services/home.service' import { CountryDto, getCountry } from '@/services/country.service'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useSetting } from '@/utils/hooks/useSetting' import { useSetting } from '@/utils/hooks/useSetting'

View file

@ -31,7 +31,7 @@ import {
putUserLookout, putUserLookout,
putUserPermission, putUserPermission,
} from '@/services/identity.service' } from '@/services/identity.service'
import { CountryDto, getCountry } from '@/services/home.service' import { CountryDto, getCountry } from '@/services/country.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Field, FieldArray, FieldProps, Form, Formik, FormikHelpers } from 'formik' import { Field, FieldArray, FieldProps, Form, Formik, FormikHelpers } from 'formik'

View file

@ -7,6 +7,7 @@ import {
FaArrowLeft, FaArrowLeft,
FaBolt, FaBolt,
FaCheckCircle, FaCheckCircle,
FaClone,
FaArrowDown, FaArrowDown,
FaArrowUp, FaArrowUp,
FaChevronDown, FaChevronDown,
@ -41,6 +42,8 @@ import { getListForms } from '@/services/admin/list-form.service'
import { developerKitService } from '@/services/developerKit.service' import { developerKitService } from '@/services/developerKit.service'
import { Button, Notification, Select, toast } from '@/components/ui' import { Button, Notification, Select, toast } from '@/components/ui'
import StyleModal from '@/components/codeLayout/StyleModal' import StyleModal from '@/components/codeLayout/StyleModal'
import { RUNTIME_NODE_COMPONENTS } from '@/components/componentEditor/runtimeNodes'
import CanvasFrame from '@/components/visualDesigner/CanvasFrame'
import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas' import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
import { import {
beginDesignerDragSoon, beginDesignerDragSoon,
@ -843,6 +846,20 @@ const parseGeneratedJsxProps = (
return props return props
} }
/**
* Tasarimci ile uretilmis kodu tanir. Elle yazilmis bir bilesenin kodu dugum agacina
* cevrilemez: donusum yalnizca tanidigi etiketleri toplar, gerisini (duz HTML, metin,
* yardimci bilesenler) sessizce duserdi ve kaydedildiginde kodun kendisi kaybolurdu.
* Bu yuzden imzasi olmayan kod "kod modu" olarak acilir.
*/
const isDesignerGeneratedCode = (code: string | undefined) =>
Boolean(
code &&
(code.includes('__SOZSOFT_VISUAL_DESIGNER__') ||
/const \[data_[A-Za-z0-9_$]+, setData_/.test(code) ||
code.includes('const designerRefs')),
)
const migrateGeneratedCodeToDesignerDocument = ( const migrateGeneratedCodeToDesignerDocument = (
code: string | undefined, code: string | undefined,
definitions: Map<string, DesignerComponentDefinition>, definitions: Map<string, DesignerComponentDefinition>,
@ -1475,7 +1492,7 @@ const VisualComponentDesigner = () => {
? null ? null
: parseDesignerDocumentFromCode(component.code) : parseDesignerDocumentFromCode(component.code)
const migratedCodeDocument = const migratedCodeDocument =
hasStoredDesignerNodes || codeBackupDocument hasStoredDesignerNodes || codeBackupDocument || !isDesignerGeneratedCode(component.code)
? null ? null
: migrateGeneratedCodeToDesignerDocument(component.code, catalogByName) : migrateGeneratedCodeToDesignerDocument(component.code, catalogByName)
const visualDocument = hasStoredDesignerNodes const visualDocument = hasStoredDesignerNodes
@ -1488,7 +1505,8 @@ const VisualComponentDesigner = () => {
setManualCode(component.code || '') setManualCode(component.code || '')
setSavedCode(component.code || '') setSavedCode(component.code || '')
setSavedDesignerFingerprint(JSON.stringify(initialDocument)) setSavedDesignerFingerprint(JSON.stringify(initialDocument))
setWorkspaceTab('design') // Elle yazilmis bilesen kod sekmesiyle acilir; tasarim kanvasi onun icin bos olurdu.
setWorkspaceTab(initialDocument.sourceMode === 'visual' ? 'design' : 'code')
setLoadedId(id) setLoadedId(id)
undoStack.current = [] undoStack.current = []
redoStack.current = [] redoStack.current = []
@ -1587,6 +1605,22 @@ const VisualComponentDesigner = () => {
[document.nodes, selectedId], [document.nodes, selectedId],
) )
const selectedDefinition = selectedNode ? catalogByName.get(selectedNode.type) : undefined const selectedDefinition = selectedNode ? catalogByName.get(selectedNode.type) : undefined
/**
* Ok tuslari kardesler arasinda yer degistirir; sinira gelindiginde dugum bir ust
* seviyeye tasinir. Yalnizca kok seviyedeki ilk/son dugumun gidecegi yer yoktur.
*/
const selectedMoveRange = useMemo(() => {
if (!selectedNode) return { up: false, down: false }
const ancestors = findDesignerAncestors(document.nodes, selectedNode.id)
const parent = ancestors?.[ancestors.length - 1]
const siblings = parent ? parent.children : document.nodes
const index = siblings.findIndex((node) => node.id === selectedNode.id)
return {
up: Boolean(parent) || index > 0,
down: Boolean(parent) || index < siblings.length - 1,
}
}, [document.nodes, selectedNode])
/** Every addressable component of the page, keyed by ref. */ /** Every addressable component of the page, keyed by ref. */
const documentRefs = useMemo(() => collectDesignerRefs(document.nodes), [document.nodes]) const documentRefs = useMemo(() => collectDesignerRefs(document.nodes), [document.nodes])
@ -2804,10 +2838,14 @@ const VisualComponentDesigner = () => {
[commitDocument, selectDesignerNode], [commitDocument, selectDesignerNode],
) )
// Bagimlilik listesi yalnizca baska Custom Component'leri sayar; export bu listeyi izleyerek
// bilesenin zincirini toplar. Katalog dugumleri (PlatformIcon, public site parcalari) runtime
// kapsamindan gelir, veritabaninda karsiligi yoktur — listeye girerse export cozulemeyen bir
// referans arar.
const collectDependencies = useCallback(() => { const collectDependencies = useCallback(() => {
const names = new Set<string>() const names = new Set<string>()
walkDesignerNodes(document.nodes, (node) => { walkDesignerNodes(document.nodes, (node) => {
if (node.kind === 'custom') names.add(node.type) if (node.kind === 'custom' && !(node.type in RUNTIME_NODE_COMPONENTS)) names.add(node.type)
}) })
return [...names] return [...names]
}, [document.nodes]) }, [document.nodes])
@ -5269,10 +5307,7 @@ const VisualComponentDesigner = () => {
onDropComponent={addComponent} onDropComponent={addComponent}
onDropComponentBeside={addComponentBeside} onDropComponentBeside={addComponentBeside}
onMoveIntoContainer={moveNodeIntoContainerNode} onMoveIntoContainer={moveNodeIntoContainerNode}
onMove={moveNode}
onReorder={reorderNode} onReorder={reorderNode}
onDuplicate={duplicateNode}
onDelete={deleteNode}
/> />
) )
@ -5372,7 +5407,7 @@ const VisualComponentDesigner = () => {
)} )}
</div> </div>
<p className="truncate text-[11px] text-slate-500"> <p className="truncate text-[11px] text-slate-500">
Visual Component Designer · {component.routePath} Visual Component Designer{component.routePath ? ` · ${component.routePath}` : ''}
</p> </p>
</div> </div>
</div> </div>
@ -5406,6 +5441,9 @@ const VisualComponentDesigner = () => {
> >
{translate('::App.Platform.Reset')} {translate('::App.Platform.Reset')}
</Button> </Button>
{/* Rotasi olmayan bilesen (baska bir ekranin icinde cizilen alt bilgi gibi)
tek basina acilamaz; onizleme dugmesi yalnizca rotasi olanda cikar. */}
{component.routePath?.trim() ? (
<Button <Button
color="green-600" color="green-600"
icon={<FaEye />} icon={<FaEye />}
@ -5421,6 +5459,7 @@ const VisualComponentDesigner = () => {
> >
{translate('::App.Listform.ListformField.Preview')} {translate('::App.Listform.ListformField.Preview')}
</Button> </Button>
) : null}
<Button <Button
loading={isSaving} loading={isSaving}
icon={<FaRegSave />} icon={<FaRegSave />}
@ -5602,7 +5641,13 @@ const VisualComponentDesigner = () => {
className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-gray-900 dark:text-gray-100" className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-gray-900 dark:text-gray-100"
style={{ width: CANVAS_WIDTHS[document.canvas.width], maxWidth: '100%' }} style={{ width: CANVAS_WIDTHS[document.canvas.width], maxWidth: '100%' }}
> >
{canvas} {/* Cihaz secildiginde kanvas kendi belgesinde cizilir: `md:`/`lg:` kurallari
tarayici penceresine degil, secilen genislige gore cozulur. */}
{document.canvas.width === 'responsive' ? (
canvas
) : (
<CanvasFrame width={CANVAS_WIDTHS[document.canvas.width]}>{canvas}</CanvasFrame>
)}
</div> </div>
) : workspaceTab === 'data' ? ( ) : workspaceTab === 'data' ? (
<div className="w-full space-y-4"> <div className="w-full space-y-4">
@ -5932,12 +5977,52 @@ const VisualComponentDesigner = () => {
<div className="text-[10px] uppercase tracking-wider text-slate-400"> <div className="text-[10px] uppercase tracking-wider text-slate-400">
Selected component Selected component
</div> </div>
<div className="mt-1 flex items-center justify-between"> {/*
<strong className="text-sm text-slate-800 dark:text-white"> Tasi/kopyala/sil kanvastaki secim seridinden buraya tasindi: serit
kucuk bir ikonun ya da ayirici cizginin ustunu tamamen ortuyordu.
Kanvasta yalnizca kose tutamagi kaldi.
*/}
<div className="mt-1 flex items-center justify-between gap-2">
<strong className="truncate text-sm text-slate-800 dark:text-white">
{selectedNode.type} {selectedNode.type}
</strong> </strong>
<div className="flex shrink-0 items-center">
<button <button
className="rounded p-1.5 text-red-500 hover:bg-red-50" className="rounded p-1.5 text-slate-500 enabled:hover:bg-slate-200 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-300 dark:enabled:hover:bg-gray-800"
disabled={!selectedMoveRange.up}
title={translate(
selectedMoveRange.up
? '::App.Platform.MoveUp'
: '::App.VisualDesignerCanvas.AlreadyAtTop',
)}
type="button"
onClick={() => moveNode(selectedNode.id, -1)}
>
<FaArrowUp />
</button>
<button
className="rounded p-1.5 text-slate-500 enabled:hover:bg-slate-200 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-300 dark:enabled:hover:bg-gray-800"
disabled={!selectedMoveRange.down}
title={translate(
selectedMoveRange.down
? '::App.Platform.MoveDown'
: '::App.VisualDesignerCanvas.AlreadyAtBottom',
)}
type="button"
onClick={() => moveNode(selectedNode.id, 1)}
>
<FaArrowDown />
</button>
<button
className="rounded p-1.5 text-slate-500 hover:bg-slate-200 dark:text-gray-300 dark:hover:bg-gray-800"
title={translate('::App.Platform.Duplicate')}
type="button"
onClick={() => duplicateNode(selectedNode.id)}
>
<FaClone />
</button>
<button
className="rounded p-1.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
title={translate('::App.ComponentAction.Delete')} title={translate('::App.ComponentAction.Delete')}
type="button" type="button"
onClick={() => deleteNode(selectedNode.id)} onClick={() => deleteNode(selectedNode.id)}
@ -5945,6 +6030,7 @@ const VisualComponentDesigner = () => {
<FaTrash /> <FaTrash />
</button> </button>
</div> </div>
</div>
<code className="text-[10px] text-slate-400">{selectedNode.id}</code> <code className="text-[10px] text-slate-400">{selectedNode.id}</code>
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700"> <div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700">
<span className="flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-400"> <span className="flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-400">

View file

@ -1,691 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import type { FC } from 'react'
import PageTitle from '@/components/shared/PageTitle'
import { AboutDto } from '@/proxy/about/models'
import { getAbout, saveAboutPage } from '@/services/about'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { PUBLIC_PAGE_DESIGN } from '@/constants/permission.constant'
import {
DesignerLayer,
DesignerSelection,
SelectableBlock,
TranslateFn,
iconField,
imageField,
isLikelyLocalizationKey,
numberField,
booleanField,
resolveLocalizedValue,
styleField,
textAreaField,
textField,
translateLabel,
usePageDesigner,
} from './designer'
import { designerPageClass, getIconColor } from './pageStyles'
import { PageIcon, PublicPageLoader } from './shared'
interface AboutStatContent {
icon: string
value: string
label: string
labelKey: string
styleClassKey: string
styleClass: string
valueStyleClassKey: string
valueStyleClass: string
labelStyleClassKey: string
labelStyleClass: string
useCounter: boolean
counterEnd: string
counterSuffix: string
counterDuration: number
}
interface AboutDescriptionContent {
key: string
text: string
styleClassKey: string
styleClass: string
}
interface AboutSectionContent {
title: string
description: string
titleKey: string
descriptionKey: string
cardStyleClassKey: string
cardStyleClass: string
titleStyleClassKey: string
titleStyleClass: string
descriptionStyleClassKey: string
descriptionStyleClass: string
}
interface AboutContent {
heroTitle: string
heroTitleKey: string
heroTitleStyleClassKey: string
heroTitleStyleClass: string
heroSubtitle: string
heroSubtitleKey: string
heroSubtitleStyleClassKey: string
heroSubtitleStyleClass: string
heroImage: string
heroImageKey: string
heroSectionStyleClassKey: string
heroSectionStyleClass: string
descriptionsContainerStyleClassKey: string
descriptionsContainerStyleClass: 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 = 'App.PublicAbout.Subtitle'
const ABOUT_HERO_IMAGE_KEY = 'Public.about.heroImage'
const ABOUT_HERO_SECTION_STYLE_KEY = 'Public.about.hero.sectionStyleClass'
const ABOUT_HERO_TITLE_STYLE_KEY = 'Public.about.hero.titleStyleClass'
const ABOUT_HERO_SUBTITLE_STYLE_KEY = 'Public.about.hero.subtitleStyleClass'
const ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY = 'Public.about.descriptions.containerStyleClass'
const HERO_SECTION_STYLE_DEFAULT = 'relative bg-blue-900 text-white py-12'
const HERO_TITLE_STYLE_DEFAULT = 'text-5xl font-bold ml-4 mt-3 mb-2 text-white'
const HERO_SUBTITLE_STYLE_DEFAULT = 'text-xl max-w-3xl ml-4'
const DESCRIPTIONS_CONTAINER_STYLE_DEFAULT =
'p-5 mx-auto text-gray-800 dark:text-gray-200 text-lg leading-relaxed shadow-md bg-white dark:bg-gray-900 border-l-4 border-blue-600'
const STAT_STYLE_DEFAULT = 'text-center rounded-xl px-4 py-6'
const STAT_VALUE_STYLE_DEFAULT = 'text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2'
const STAT_LABEL_STYLE_DEFAULT = 'text-gray-600 dark:text-gray-300'
const SECTION_CARD_STYLE_DEFAULT =
'bg-white dark:bg-gray-900 p-8 rounded-xl shadow-lg dark:shadow-gray-950/40'
const SECTION_TITLE_STYLE_DEFAULT = 'text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4'
const SECTION_DESCRIPTION_STYLE_DEFAULT = 'text-gray-700 dark:text-gray-300'
function getCounterParts(value: string, suffix: string) {
const numericMatch = (value || '0').replace(',', '.').match(/\d+(?:\.\d+)?/)
if (!numericMatch) {
return null
}
const endValue = Number(numericMatch[0])
if (!Number.isFinite(endValue)) {
return null
}
const matchIndex = numericMatch.index ?? 0
return {
endValue,
prefix: value.slice(0, matchIndex),
suffix: suffix || value.slice(matchIndex + numericMatch[0].length),
hasDecimals: numericMatch[0].includes('.'),
}
}
/**
* Sayac animasyonu yalnizca deger ekrana girdiginde bir kez calisir;
* boylece sayfanin altindaki istatistikler kullanici oraya gelmeden bitmis olmaz.
*/
const AnimatedStatValue: FC<{ stat: AboutStatContent }> = ({ stat }) => {
const counterParts = useMemo(
() => getCounterParts(stat.counterEnd || stat.value, stat.counterSuffix),
[stat.counterEnd, stat.counterSuffix, stat.value],
)
const [currentValue, setCurrentValue] = useState(0)
const [isVisible, setIsVisible] = useState(false)
const containerRef = useRef<HTMLSpanElement>(null)
useEffect(() => {
const element = containerRef.current
if (!stat.useCounter || !element || typeof IntersectionObserver === 'undefined') {
setIsVisible(true)
return
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.3 },
)
observer.observe(element)
return () => observer.disconnect()
}, [stat.useCounter])
useEffect(() => {
if (!stat.useCounter || !counterParts || !isVisible) {
return
}
const duration = Math.max(stat.counterDuration || 2000, 0)
let animationFrameId = 0
let startTime: number | null = null
const animate = (timestamp: number) => {
startTime ??= timestamp
const progress = duration === 0 ? 1 : Math.min((timestamp - startTime) / duration, 1)
setCurrentValue(counterParts.endValue * (1 - Math.pow(1 - progress, 3)))
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate)
}
}
setCurrentValue(0)
animationFrameId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrameId)
}, [counterParts, isVisible, stat.counterDuration, stat.useCounter])
if (!stat.useCounter || !counterParts) {
return <>{stat.value}</>
}
return (
<span ref={containerRef}>
{counterParts.prefix}
{counterParts.hasDecimals ? currentValue.toFixed(1) : Math.floor(currentValue)}
{counterParts.suffix}
</span>
)
}
function buildAboutContent(about: AboutDto | undefined, translate: TranslateFn): AboutContent {
return {
heroTitle: resolveLocalizedValue(translate, ABOUT_HERO_TITLE_KEY, 'About'),
heroTitleKey: ABOUT_HERO_TITLE_KEY,
heroTitleStyleClassKey: ABOUT_HERO_TITLE_STYLE_KEY,
heroTitleStyleClass: resolveLocalizedValue(
translate,
ABOUT_HERO_TITLE_STYLE_KEY,
HERO_TITLE_STYLE_DEFAULT,
),
heroSubtitle: resolveLocalizedValue(translate, ABOUT_HERO_SUBTITLE_KEY),
heroSubtitleKey: ABOUT_HERO_SUBTITLE_KEY,
heroSubtitleStyleClassKey: ABOUT_HERO_SUBTITLE_STYLE_KEY,
heroSubtitleStyleClass: resolveLocalizedValue(
translate,
ABOUT_HERO_SUBTITLE_STYLE_KEY,
HERO_SUBTITLE_STYLE_DEFAULT,
),
heroImage: resolveLocalizedValue(translate, ABOUT_HERO_IMAGE_KEY, ABOUT_HERO_IMAGE),
heroImageKey: ABOUT_HERO_IMAGE_KEY,
heroSectionStyleClassKey: ABOUT_HERO_SECTION_STYLE_KEY,
heroSectionStyleClass: resolveLocalizedValue(
translate,
ABOUT_HERO_SECTION_STYLE_KEY,
HERO_SECTION_STYLE_DEFAULT,
),
descriptionsContainerStyleClassKey: ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
descriptionsContainerStyleClass: resolveLocalizedValue(
translate,
ABOUT_DESCRIPTIONS_CONTAINER_STYLE_KEY,
DESCRIPTIONS_CONTAINER_STYLE_DEFAULT,
),
stats: (about?.statsDto ?? []).map((stat, index) => {
const prefix = `Public.about.dynamic.stat.${index + 1}`
return {
icon: stat.icon || '',
value: stat.value,
label: resolveLocalizedValue(translate, stat.labelKey, stat.labelKey),
labelKey:
(isLikelyLocalizationKey(stat.labelKey) ? stat.labelKey : '') || `${prefix}.label`,
styleClassKey: `${prefix}.styleClass`,
styleClass: resolveLocalizedValue(translate, `${prefix}.styleClass`, STAT_STYLE_DEFAULT),
valueStyleClassKey: `${prefix}.valueStyleClass`,
valueStyleClass: resolveLocalizedValue(
translate,
`${prefix}.valueStyleClass`,
STAT_VALUE_STYLE_DEFAULT,
),
labelStyleClassKey: `${prefix}.labelStyleClass`,
labelStyleClass: resolveLocalizedValue(
translate,
`${prefix}.labelStyleClass`,
STAT_LABEL_STYLE_DEFAULT,
),
useCounter: stat.useCounter ?? false,
counterEnd: stat.counterEnd ?? '',
counterSuffix: stat.counterSuffix ?? '',
counterDuration: stat.counterDuration ?? 2000,
}
}),
descriptions: (about?.descriptionsDto ?? []).map((item, index) => {
const prefix = `Public.about.dynamic.description.${index + 1}`
return {
key: (isLikelyLocalizationKey(item) ? item : '') || prefix,
text: resolveLocalizedValue(translate, item, item),
styleClassKey: `${prefix}.styleClass`,
styleClass: resolveLocalizedValue(
translate,
`${prefix}.styleClass`,
index % 2 === 0 ? '' : 'text-center p-5 text-blue-800 dark:text-white',
),
}
}),
sections: (about?.sectionsDto ?? []).map((section) => {
const prefix = `Public.about.dynamic.section.${section.key}`
return {
title: resolveLocalizedValue(translate, section.key, section.key),
description: resolveLocalizedValue(translate, section.descKey, section.descKey),
titleKey: (isLikelyLocalizationKey(section.key) ? section.key : '') || `${prefix}.title`,
descriptionKey:
(isLikelyLocalizationKey(section.descKey) ? section.descKey : '') ||
`${prefix}.description`,
cardStyleClassKey: `${prefix}.cardStyleClass`,
cardStyleClass: resolveLocalizedValue(
translate,
`${prefix}.cardStyleClass`,
SECTION_CARD_STYLE_DEFAULT,
),
titleStyleClassKey: `${prefix}.titleStyleClass`,
titleStyleClass: resolveLocalizedValue(
translate,
`${prefix}.titleStyleClass`,
SECTION_TITLE_STYLE_DEFAULT,
),
descriptionStyleClassKey: `${prefix}.descriptionStyleClass`,
descriptionStyleClass: resolveLocalizedValue(
translate,
`${prefix}.descriptionStyleClass`,
SECTION_DESCRIPTION_STYLE_DEFAULT,
),
}
}),
}
}
function toSaveInput(content: AboutContent, cultureName: string) {
return {
cultureName,
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,
})),
styleTexts: [
{ key: content.heroSectionStyleClassKey, value: content.heroSectionStyleClass },
{ key: content.heroTitleStyleClassKey, value: content.heroTitleStyleClass },
{ key: content.heroSubtitleStyleClassKey, value: content.heroSubtitleStyleClass },
{
key: content.descriptionsContainerStyleClassKey,
value: content.descriptionsContainerStyleClass,
},
...content.stats.flatMap((stat) => [
{ key: stat.styleClassKey, value: stat.styleClass },
{ key: stat.valueStyleClassKey, value: stat.valueStyleClass },
{ key: stat.labelStyleClassKey, value: stat.labelStyleClass },
]),
...content.descriptions.map((item) => ({
key: item.styleClassKey,
value: item.styleClass,
})),
...content.sections.flatMap((section) => [
{ key: section.cardStyleClassKey, value: section.cardStyleClass },
{ key: section.titleStyleClassKey, value: section.titleStyleClass },
{ key: section.descriptionStyleClassKey, value: section.descriptionStyleClass },
]),
],
}
}
const About: FC = () => {
const { translate } = useLocalization()
const [loading, setLoading] = useState(true)
const [about, setAbout] = useState<AboutDto>()
useEffect(() => {
const fetchAbout = async () => {
setLoading(true)
try {
const result = await getAbout()
setAbout(result.data)
} catch (error) {
console.error('About alinirken hata olustu:', error)
} finally {
setLoading(false)
}
}
fetchAbout()
}, [])
// Referansi kararli tutmak icin memoize edilir; aksi halde tasarim state'i her render'da yeniden tohumlanir.
const initialContent = useMemo(
() => (loading ? null : buildAboutContent(about, translate)),
[about, loading, translate],
)
const designer = usePageDesigner<AboutContent>({
pageKey: 'About',
permission: PUBLIC_PAGE_DESIGN.ABOUT,
initialContent,
onSave: (content, cultureName) => saveAboutPage(toSaveInput(content, cultureName)),
})
const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer
const selection: DesignerSelection | null = useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: content.heroTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc4'),
fields: [
textField('heroTitle', content.heroTitleKey, content.heroTitle),
textAreaField('heroSubtitle', content.heroSubtitleKey, content.heroSubtitle),
imageField('heroImage', content.heroImageKey, content.heroImage, { group: 'media' }),
styleField(
'heroTitleStyleClass',
content.heroTitleStyleClassKey,
content.heroTitleStyleClass,
),
styleField(
'heroSubtitleStyleClass',
content.heroSubtitleStyleClassKey,
content.heroSubtitleStyleClass,
),
styleField(
'heroSectionStyleClass',
content.heroSectionStyleClassKey,
content.heroSectionStyleClass,
),
],
}
}
if (selectedBlockId === 'descriptions') {
return {
id: 'descriptions',
title: 'Public.about.description.*',
description: translateLabel(translate, 'Public.designer.descriptionsHint'),
fields: [
...content.descriptions.map((item, index) =>
textAreaField(`descriptions.${index}.text`, item.key, item.text, {
rows: index % 2 === 0 ? 4 : 3,
}),
),
styleField(
'descriptionsContainerStyleClass',
content.descriptionsContainerStyleClassKey,
content.descriptionsContainerStyleClass,
),
...content.descriptions.map((item, index) =>
styleField(`descriptions.${index}.styleClass`, item.styleClassKey, item.styleClass),
),
],
}
}
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: translateLabel(translate, 'App.PublicDesigner.DesignerDesc1'),
fields: [
iconField(
`stats.${index}.icon`,
translateLabel(translate, 'App.PublicDesigner.IkonAnahtari'),
stat.icon,
{ placeholder: translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaUsers' }) },
),
textField(
`stats.${index}.value`,
translate('::App.Listform.ListformField.Value'),
stat.value,
),
textField(`stats.${index}.label`, stat.labelKey, stat.label),
booleanField(`stats.${index}.useCounter`, 'useCounter', stat.useCounter, {
group: 'advanced',
placeholder: translateLabel(translate, 'Public.designer.useCounter'),
}),
textField(`stats.${index}.counterEnd`, 'counterEnd', stat.counterEnd, {
group: 'advanced',
helpText: translateLabel(translate, 'Public.designer.counterEndHint'),
}),
textField(`stats.${index}.counterSuffix`, 'counterSuffix', stat.counterSuffix, {
group: 'advanced',
}),
numberField(
`stats.${index}.counterDuration`,
'counterDuration (ms)',
stat.counterDuration,
{
group: 'advanced',
},
),
styleField(`stats.${index}.styleClass`, stat.styleClassKey, stat.styleClass),
styleField(
`stats.${index}.valueStyleClass`,
stat.valueStyleClassKey,
stat.valueStyleClass,
),
styleField(
`stats.${index}.labelStyleClass`,
stat.labelStyleClassKey,
stat.labelStyleClass,
),
],
}
}
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: translateLabel(translate, 'Public.designer.sectionHint'),
fields: [
textField(`sections.${index}.title`, section.titleKey, section.title),
textAreaField(
`sections.${index}.description`,
section.descriptionKey,
section.description,
),
styleField(
`sections.${index}.cardStyleClass`,
section.cardStyleClassKey,
section.cardStyleClass,
),
styleField(
`sections.${index}.titleStyleClass`,
section.titleStyleClassKey,
section.titleStyleClass,
),
styleField(
`sections.${index}.descriptionStyleClass`,
section.descriptionStyleClassKey,
section.descriptionStyleClass,
),
],
}
}
return null
}, [content, selectedBlockId, translate])
if (loading) {
return <PublicPageLoader />
}
return (
<>
<PageTitle title={translate('::App.About')} />
<div className={designerPageClass(isDesignMode, isPanelVisible)}>
{/* Hero */}
<SelectableBlock
id="hero"
label="Hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div
className={`${content?.heroSectionStyleClass || HERO_SECTION_STYLE_DEFAULT} dark:bg-gray-950`}
>
<div
className="absolute inset-0 opacity-20 dark:opacity-35"
style={{
backgroundImage: `url("${content?.heroImage || ABOUT_HERO_IMAGE}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<div className="absolute inset-0 bg-blue-950/25 dark:bg-gray-950/45" />
<div className="container relative mx-auto pt-20">
<h1 className={content?.heroTitleStyleClass || HERO_TITLE_STYLE_DEFAULT}>
{content?.heroTitle}
</h1>
<p className={content?.heroSubtitleStyleClass || HERO_SUBTITLE_STYLE_DEFAULT}>
{content?.heroSubtitle}
</p>
</div>
</div>
</SelectableBlock>
{/* Stats */}
<div className="bg-white py-10 dark:bg-gray-900">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-5">
{content?.stats.map((stat, index) => (
<SelectableBlock
key={`${stat.labelKey}-${index}`}
id={`stat-${index}`}
label={`Stat ${index + 1}`}
isActive={selectedBlockId === `stat-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={stat.styleClass || STAT_STYLE_DEFAULT}>
<PageIcon
name={stat.icon}
className={`mx-auto mb-4 h-12 w-12 ${getIconColor(index)}`}
/>
<div className={stat.valueStyleClass || STAT_VALUE_STYLE_DEFAULT}>
<AnimatedStatValue stat={stat} />
</div>
<div className={stat.labelStyleClass || STAT_LABEL_STYLE_DEFAULT}>
{stat.label}
</div>
</div>
</SelectableBlock>
))}
</div>
</div>
</div>
{/* Descriptions + sections */}
<div className="py-6">
<div className="container mx-auto px-4">
<div className="mb-6">
<SelectableBlock
id="descriptions"
label={translateLabel(translate, 'Public.designer.group.content')}
isActive={selectedBlockId === 'descriptions'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div
className={
content?.descriptionsContainerStyleClass || DESCRIPTIONS_CONTAINER_STYLE_DEFAULT
}
>
{content?.descriptions.map((item, index) => (
<p key={item.key || `description-${index}`} className={item.styleClass}>
{item.text}
</p>
))}
</div>
</SelectableBlock>
</div>
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
{content?.sections.map((section, index) => (
<SelectableBlock
key={`${section.titleKey}-${index}`}
id={`section-${index}`}
label={`Section ${index + 1}`}
isActive={selectedBlockId === `section-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={section.cardStyleClass || SECTION_CARD_STYLE_DEFAULT}>
<h3 className={section.titleStyleClass || SECTION_TITLE_STYLE_DEFAULT}>
{section.title}
</h3>
<p
className={section.descriptionStyleClass || SECTION_DESCRIPTION_STYLE_DEFAULT}
>
{section.description}
</p>
</div>
</SelectableBlock>
))}
</div>
</div>
</div>
<DesignerLayer pageTitle="About" selection={selection} designer={designer} />
</div>
</>
)
}
export default About

View file

@ -7,7 +7,7 @@ import { BlogCategory, BlogPost } from '@/proxy/blog/blog'
import { blogService } from '@/services/blog.service' import { blogService } from '@/services/blog.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button } from '@/components/ui' import { Button } from '@/components/ui'
import { translateLabel } from './designer' import { translateLabel } from './localization'
import { PublicPageLoader } from './shared' import { PublicPageLoader } from './shared'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'

View file

@ -7,7 +7,7 @@ import { blogService } from '@/services/blog.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { translateLabel } from './designer' import { translateLabel } from './localization'
import { PublicPageLoader } from './shared' import { PublicPageLoader } from './shared'
const CenteredMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => ( const CenteredMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => (

View file

@ -1,608 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import {
FaPhone,
FaBuilding,
FaCalendarAlt,
FaCalendarCheck,
FaRegComment,
FaMapMarkerAlt,
FaEnvelope,
FaIdCard,
} from 'react-icons/fa'
import PageTitle from '@/components/shared/PageTitle'
import { ContactDto } from '@/proxy/contact/models'
import { getContact, saveContactPage } from '@/services/contact.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { PUBLIC_PAGE_DESIGN } from '@/constants/permission.constant'
import {
DesignerLayer,
DesignerSelection,
SelectableBlock,
TranslateFn,
booleanField,
imageField,
resolveLocalizedValue,
styleField,
textAreaField,
textField,
translateLabel,
usePageDesigner,
} from './designer'
import { designerPageClass } from './pageStyles'
import { PublicPageLoader } from './shared'
interface ContactContent {
heroTitle: string
heroTitleKey: string
heroSectionStyleClass: string
heroSectionStyleClassKey: string
heroTitleStyleClass: string
heroTitleStyleClassKey: string
heroSubtitle: string
heroSubtitleKey: string
heroSubtitleStyleClass: string
heroSubtitleStyleClassKey: string
heroImage: string
heroImageKey: string
contactInfoTitle: string
contactInfoTitleKey: string
contactInfoCardStyleClass: string
contactInfoCardStyleClassKey: string
contactInfoTitleStyleClass: string
contactInfoTitleStyleClassKey: string
address: string
addressKey: string
phoneNumber: string
email: string
location: string
taxNumber: string
bankTitle: string
bankTitleKey: string
bankAccountHolder: string
bankBranch: string
bankAccountNumber: string
bankIban: string
bankStyleClass: string
workHoursTitle: string
workHoursTitleKey: string
workWeekday: string
workWeekdayKey: string
workWeekend: string
workWeekendKey: string
workWhatsapp: string
workWhatsappKey: string
workHoursStyleClass: string
mapTitle: string
mapTitleKey: string
mapSrc: string
mapWidth: string
mapHeight: string
mapAllowFullScreen: boolean
mapLoading: string
mapReferrerPolicy: string
mapContainerStyleClass: string
mapFrameStyleClass: string
}
const CONTACT_HERO_TITLE_KEY = 'App.Contact'
const CONTACT_HERO_SUBTITLE_KEY = 'App.PublicContact.Subtitle'
const CONTACT_HERO_IMAGE_KEY = 'Public.contact.heroImage'
const CONTACT_HERO_IMAGE_DEFAULT =
'https://images.pexels.com/photos/3183171/pexels-photo-3183171.jpeg?auto=compress&cs=tinysrgb&w=1920'
const CONTACT_INFO_TITLE_KEY = 'Abp.Identity.User.UserInformation.ContactInformation'
const CONTACT_BANK_TITLE_KEY = 'App.ContactBank.BankTitle'
const CONTACT_WORK_HOURS_TITLE_KEY = 'App.Definitions.WorkHour'
const CONTACT_HERO_SECTION_STYLE_KEY = 'Public.contact.hero.sectionStyleClass'
const CONTACT_HERO_TITLE_STYLE_KEY = 'Public.contact.hero.titleStyleClass'
const CONTACT_HERO_SUBTITLE_STYLE_KEY = 'Public.contact.hero.subtitleStyleClass'
const CONTACT_INFO_CARD_STYLE_KEY = 'Public.contact.info.cardStyleClass'
const CONTACT_INFO_TITLE_STYLE_KEY = 'Public.contact.info.titleStyleClass'
const HERO_SECTION_STYLE_DEFAULT = 'relative bg-blue-900 py-12 text-white dark:bg-gray-950'
const HERO_TITLE_STYLE_DEFAULT = 'ml-4 mb-2 mt-3 text-5xl font-bold text-white'
const HERO_SUBTITLE_STYLE_DEFAULT = 'ml-4 max-w-3xl text-xl'
const CARD_STYLE_DEFAULT =
'rounded-xl bg-white p-8 shadow-lg dark:bg-gray-900 dark:shadow-gray-950/40'
const INFO_TITLE_STYLE_DEFAULT = 'mb-6 text-2xl font-bold text-gray-900 dark:text-gray-100'
const MAP_FRAME_STYLE_DEFAULT =
'aspect-w-16 aspect-h-9 overflow-hidden rounded-xl bg-gray-200 dark:bg-gray-800'
function buildContactContent(
contact: ContactDto | undefined,
translate: TranslateFn,
): ContactContent {
return {
heroTitle: resolveLocalizedValue(translate, CONTACT_HERO_TITLE_KEY, 'Contact'),
heroTitleKey: CONTACT_HERO_TITLE_KEY,
heroSectionStyleClass: resolveLocalizedValue(
translate,
CONTACT_HERO_SECTION_STYLE_KEY,
HERO_SECTION_STYLE_DEFAULT,
),
heroSectionStyleClassKey: CONTACT_HERO_SECTION_STYLE_KEY,
heroTitleStyleClass: resolveLocalizedValue(
translate,
CONTACT_HERO_TITLE_STYLE_KEY,
HERO_TITLE_STYLE_DEFAULT,
),
heroTitleStyleClassKey: CONTACT_HERO_TITLE_STYLE_KEY,
heroSubtitle: resolveLocalizedValue(translate, CONTACT_HERO_SUBTITLE_KEY),
heroSubtitleKey: CONTACT_HERO_SUBTITLE_KEY,
heroSubtitleStyleClass: resolveLocalizedValue(
translate,
CONTACT_HERO_SUBTITLE_STYLE_KEY,
HERO_SUBTITLE_STYLE_DEFAULT,
),
heroSubtitleStyleClassKey: CONTACT_HERO_SUBTITLE_STYLE_KEY,
heroImage: resolveLocalizedValue(translate, CONTACT_HERO_IMAGE_KEY, CONTACT_HERO_IMAGE_DEFAULT),
heroImageKey: CONTACT_HERO_IMAGE_KEY,
contactInfoTitle: resolveLocalizedValue(translate, CONTACT_INFO_TITLE_KEY),
contactInfoTitleKey: CONTACT_INFO_TITLE_KEY,
contactInfoCardStyleClass: resolveLocalizedValue(
translate,
CONTACT_INFO_CARD_STYLE_KEY,
CARD_STYLE_DEFAULT,
),
contactInfoCardStyleClassKey: CONTACT_INFO_CARD_STYLE_KEY,
contactInfoTitleStyleClass: resolveLocalizedValue(
translate,
CONTACT_INFO_TITLE_STYLE_KEY,
INFO_TITLE_STYLE_DEFAULT,
),
contactInfoTitleStyleClassKey: CONTACT_INFO_TITLE_STYLE_KEY,
address: resolveLocalizedValue(translate, contact?.address, ''),
addressKey: contact?.address || '',
phoneNumber: contact?.phoneNumber || '',
email: contact?.email || '',
location: contact?.location || '',
taxNumber: contact?.taxNumber || '',
bankTitle: resolveLocalizedValue(translate, CONTACT_BANK_TITLE_KEY),
bankTitleKey: CONTACT_BANK_TITLE_KEY,
bankAccountHolder: contact?.bankDto?.accountHolder || '',
bankBranch: contact?.bankDto?.branch || '',
bankAccountNumber: contact?.bankDto?.accountNumber || '',
bankIban: contact?.bankDto?.iban || '',
bankStyleClass: contact?.bankDto?.styleClass || CARD_STYLE_DEFAULT,
workHoursTitle: resolveLocalizedValue(translate, CONTACT_WORK_HOURS_TITLE_KEY),
workHoursTitleKey: CONTACT_WORK_HOURS_TITLE_KEY,
workWeekday: resolveLocalizedValue(translate, contact?.workHoursDto?.weekday, ''),
workWeekdayKey: contact?.workHoursDto?.weekday || '',
workWeekend: resolveLocalizedValue(translate, contact?.workHoursDto?.weekend, ''),
workWeekendKey: contact?.workHoursDto?.weekend || '',
workWhatsapp: resolveLocalizedValue(translate, contact?.workHoursDto?.whatsapp, ''),
workWhatsappKey: contact?.workHoursDto?.whatsapp || '',
workHoursStyleClass: contact?.workHoursDto?.styleClass || CARD_STYLE_DEFAULT,
mapTitle: resolveLocalizedValue(translate, contact?.mapDto?.title, ''),
mapTitleKey: contact?.mapDto?.title || '',
mapSrc: contact?.mapDto?.src || '',
mapWidth: contact?.mapDto?.width || '100%',
mapHeight: contact?.mapDto?.height || '700',
mapAllowFullScreen: contact?.mapDto?.allowFullScreen ?? true,
mapLoading: contact?.mapDto?.loading || 'lazy',
mapReferrerPolicy: contact?.mapDto?.referrerPolicy || 'no-referrer-when-downgrade',
mapContainerStyleClass: contact?.mapDto?.containerStyleClass || CARD_STYLE_DEFAULT,
mapFrameStyleClass: contact?.mapDto?.frameStyleClass || MAP_FRAME_STYLE_DEFAULT,
}
}
function toSaveInput(content: ContactContent, cultureName: string) {
return {
cultureName,
heroTitleKey: content.heroTitleKey,
heroTitleValue: content.heroTitle,
heroSubtitleKey: content.heroSubtitleKey,
heroSubtitleValue: content.heroSubtitle,
heroImageKey: content.heroImageKey,
heroImageValue: content.heroImage,
contactInfoTitleKey: content.contactInfoTitleKey,
contactInfoTitleValue: content.contactInfoTitle,
addressKey: content.addressKey,
addressValue: content.address,
phoneNumber: content.phoneNumber,
email: content.email,
location: content.location,
taxNumber: content.taxNumber,
bankTitleKey: content.bankTitleKey,
bankTitleValue: content.bankTitle,
bankAccountHolder: content.bankAccountHolder,
bankBranch: content.bankBranch,
bankAccountNumber: content.bankAccountNumber,
bankIban: content.bankIban,
bankStyleClass: content.bankStyleClass,
workHoursTitleKey: content.workHoursTitleKey,
workHoursTitleValue: content.workHoursTitle,
workWeekdayKey: content.workWeekdayKey,
workWeekdayValue: content.workWeekday,
workWeekendKey: content.workWeekendKey,
workWeekendValue: content.workWeekend,
workWhatsappKey: content.workWhatsappKey,
workWhatsappValue: content.workWhatsapp,
workHoursStyleClass: content.workHoursStyleClass,
mapTitleKey: content.mapTitleKey,
mapTitleValue: content.mapTitle,
mapSrc: content.mapSrc,
mapWidth: content.mapWidth,
mapHeight: content.mapHeight,
mapAllowFullScreen: content.mapAllowFullScreen,
mapLoading: content.mapLoading,
mapReferrerPolicy: content.mapReferrerPolicy,
mapContainerStyleClass: content.mapContainerStyleClass,
mapFrameStyleClass: content.mapFrameStyleClass,
styleTexts: [
{ key: content.heroSectionStyleClassKey, value: content.heroSectionStyleClass },
{ key: content.heroTitleStyleClassKey, value: content.heroTitleStyleClass },
{ key: content.heroSubtitleStyleClassKey, value: content.heroSubtitleStyleClass },
{ key: content.contactInfoCardStyleClassKey, value: content.contactInfoCardStyleClass },
{ key: content.contactInfoTitleStyleClassKey, value: content.contactInfoTitleStyleClass },
],
}
}
const InfoRow: React.FC<{ icon: React.ReactNode; children: React.ReactNode }> = ({
icon,
children,
}) => (
<div className="flex items-start space-x-2">
<span className="mt-1 flex-shrink-0 text-blue-600 dark:text-blue-400">{icon}</span>
<p className="text-gray-600 dark:text-gray-300">{children}</p>
</div>
)
const Contact: React.FC = () => {
const { translate } = useLocalization()
const [loading, setLoading] = useState(true)
const [contact, setContact] = useState<ContactDto>()
useEffect(() => {
const fetchContact = async () => {
setLoading(true)
try {
const result = await getContact()
setContact(result.data)
} catch (error) {
console.error('Contact alinirken hata olustu:', error)
} finally {
setLoading(false)
}
}
fetchContact()
}, [])
const initialContent = useMemo(
() => (loading ? null : buildContactContent(contact, translate)),
[contact, loading, translate],
)
const designer = usePageDesigner<ContactContent>({
pageKey: 'Contact',
permission: PUBLIC_PAGE_DESIGN.CONTACT,
initialContent,
onSave: (content, cultureName) => saveContactPage(toSaveInput(content, cultureName)),
})
const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer
const selection: DesignerSelection | null = useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: 'Public.contact.hero.*',
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc4'),
fields: [
textField('heroTitle', content.heroTitleKey, content.heroTitle),
textAreaField('heroSubtitle', content.heroSubtitleKey, content.heroSubtitle),
imageField('heroImage', content.heroImageKey, content.heroImage, { group: 'media' }),
styleField(
'heroTitleStyleClass',
content.heroTitleStyleClassKey,
content.heroTitleStyleClass,
),
styleField(
'heroSubtitleStyleClass',
content.heroSubtitleStyleClassKey,
content.heroSubtitleStyleClass,
),
styleField(
'heroSectionStyleClass',
content.heroSectionStyleClassKey,
content.heroSectionStyleClass,
),
],
}
}
if (selectedBlockId === 'contact-info') {
return {
id: selectedBlockId,
title: content.contactInfoTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc5'),
fields: [
textField('contactInfoTitle', content.contactInfoTitleKey, content.contactInfoTitle),
textAreaField('address', content.addressKey || 'Public.contact.address', content.address),
textField('phoneNumber', 'Public.contact.phone', content.phoneNumber),
textField('email', 'Public.contact.email', content.email),
textField('location', 'Public.contact.location.plain', content.location),
textField('taxNumber', 'App.PublicContact.TaxNumber', content.taxNumber),
styleField(
'contactInfoTitleStyleClass',
content.contactInfoTitleStyleClassKey,
content.contactInfoTitleStyleClass,
),
styleField(
'contactInfoCardStyleClass',
content.contactInfoCardStyleClassKey,
content.contactInfoCardStyleClass,
),
],
}
}
if (selectedBlockId === 'bank') {
return {
id: selectedBlockId,
title: content.bankTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc6'),
fields: [
textField('bankTitle', content.bankTitleKey, content.bankTitle),
textField(
'bankAccountHolder',
'Public.contact.bank.accountHolder',
content.bankAccountHolder,
),
textField('bankBranch', 'Public.contact.bank.branch', content.bankBranch),
textField(
'bankAccountNumber',
'Public.contact.bank.accountNumber',
content.bankAccountNumber,
),
textField('bankIban', 'Public.contact.bank.iban', content.bankIban),
styleField('bankStyleClass', 'Public.contact.bank.styleClass', content.bankStyleClass),
],
}
}
if (selectedBlockId === 'work-hours') {
return {
id: selectedBlockId,
title: content.workHoursTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc7'),
fields: [
textField('workHoursTitle', content.workHoursTitleKey, content.workHoursTitle),
textField('workWeekday', content.workWeekdayKey || 'weekday', content.workWeekday),
textField('workWeekend', content.workWeekendKey || 'weekend', content.workWeekend),
textField('workWhatsapp', content.workWhatsappKey || 'whatsapp', content.workWhatsapp),
styleField(
'workHoursStyleClass',
'Public.contact.workHours.styleClass',
content.workHoursStyleClass,
),
],
}
}
if (selectedBlockId === 'map') {
return {
id: selectedBlockId,
title: content.mapTitleKey || 'Public.contact.map',
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc8'),
fields: [
textField(
'mapTitle',
content.mapTitleKey || 'Public.contact.map.title',
content.mapTitle,
),
textAreaField('mapSrc', 'Public.contact.map.src', content.mapSrc, { group: 'media' }),
textField('mapWidth', 'Public.contact.map.width', content.mapWidth, {
group: 'advanced',
}),
textField('mapHeight', 'Public.contact.map.height', content.mapHeight, {
group: 'advanced',
}),
booleanField(
'mapAllowFullScreen',
'Public.contact.map.allowFullScreen',
content.mapAllowFullScreen,
{ group: 'advanced' },
),
textField('mapLoading', 'Public.contact.map.loading', content.mapLoading, {
group: 'advanced',
placeholder: 'lazy | eager',
}),
textField(
'mapReferrerPolicy',
'Public.contact.map.referrerPolicy',
content.mapReferrerPolicy,
{
group: 'advanced',
},
),
styleField(
'mapContainerStyleClass',
'Public.contact.map.containerStyleClass',
content.mapContainerStyleClass,
),
styleField(
'mapFrameStyleClass',
'Public.contact.map.frameStyleClass',
content.mapFrameStyleClass,
),
],
}
}
return null
}, [content, selectedBlockId, translate])
if (loading) {
return <PublicPageLoader />
}
return (
<div className={designerPageClass(isDesignMode, isPanelVisible)}>
<PageTitle title={translate('::App.Contact')} />
<SelectableBlock
id="hero"
label="Hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.heroSectionStyleClass || HERO_SECTION_STYLE_DEFAULT}>
<div
className="absolute inset-0 opacity-20 dark:opacity-35"
style={{
backgroundImage: `url("${content?.heroImage || CONTACT_HERO_IMAGE_DEFAULT}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<div className="absolute inset-0 bg-blue-950/25 dark:bg-gray-950/45" />
<div className="container relative mx-auto pt-20">
<h1 className={content?.heroTitleStyleClass || HERO_TITLE_STYLE_DEFAULT}>
{content?.heroTitle}
</h1>
<p className={content?.heroSubtitleStyleClass || HERO_SUBTITLE_STYLE_DEFAULT}>
{content?.heroSubtitle}
</p>
</div>
</div>
</SelectableBlock>
<div className="py-6">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 gap-12 lg:grid-cols-2">
<div className="space-y-4">
<SelectableBlock
id="contact-info"
label="Contact info"
isActive={selectedBlockId === 'contact-info'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.contactInfoCardStyleClass || CARD_STYLE_DEFAULT}>
<h2 className={content?.contactInfoTitleStyleClass || INFO_TITLE_STYLE_DEFAULT}>
{content?.contactInfoTitle}
</h2>
<div className="space-y-4">
<InfoRow icon={<FaMapMarkerAlt className="h-5 w-5" />}>
{content?.address}
</InfoRow>
<InfoRow icon={<FaPhone className="h-5 w-5" />}>{content?.phoneNumber}</InfoRow>
<InfoRow icon={<FaEnvelope className="h-5 w-5" />}>
<a
href={`mailto:${content?.email}`}
className="text-blue-600 hover:underline dark:text-blue-400"
>
{content?.email}
</a>
</InfoRow>
<InfoRow icon={<FaBuilding className="h-5 w-5" />}>{content?.location}</InfoRow>
<InfoRow icon={<FaIdCard className="h-5 w-5" />}>{content?.taxNumber}</InfoRow>
</div>
</div>
</SelectableBlock>
<SelectableBlock
id="bank"
label="Bank"
isActive={selectedBlockId === 'bank'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.bankStyleClass || CARD_STYLE_DEFAULT}>
<h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-gray-100">
{content?.bankTitle}
</h2>
<img
src="/img/enpara.svg"
alt={content?.bankAccountHolder || 'Bank'}
className="mb-2 w-24 object-contain"
/>
<h3 className="mb-1 font-semibold text-gray-900 dark:text-gray-100">
{content?.bankAccountHolder}
</h3>
<p className="mb-1 ml-1 text-gray-600 dark:text-gray-300">
{content?.bankBranch}
</p>
<p className="mb-1 ml-1 text-gray-600 dark:text-gray-300">
{content?.bankAccountNumber}
</p>
<p className="mb-1 ml-1 text-gray-600 dark:text-gray-300">{content?.bankIban}</p>
</div>
</SelectableBlock>
<SelectableBlock
id="work-hours"
label="Work hours"
isActive={selectedBlockId === 'work-hours'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.workHoursStyleClass || CARD_STYLE_DEFAULT}>
<h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-gray-100">
{content?.workHoursTitle}
</h2>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<FaCalendarAlt className="h-5 w-5 text-blue-500 dark:text-blue-400" />
<p className="text-gray-600 dark:text-gray-300">{content?.workWeekday}</p>
</div>
<div className="flex items-center space-x-2">
<FaCalendarCheck className="h-5 w-5 text-blue-500 dark:text-blue-400" />
<p className="text-gray-600 dark:text-gray-300">{content?.workWeekend}</p>
</div>
<div className="flex items-center space-x-2">
<FaRegComment className="h-5 w-5 text-green-500 dark:text-green-400" />
<p className="text-gray-600 dark:text-gray-300">{content?.workWhatsapp}</p>
</div>
</div>
</div>
</SelectableBlock>
</div>
<SelectableBlock
id="map"
label="Map"
isActive={selectedBlockId === 'map'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.mapContainerStyleClass || CARD_STYLE_DEFAULT}>
<h2 className="mb-2 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">
{content?.mapTitle}
</h2>
<div className={content?.mapFrameStyleClass || MAP_FRAME_STYLE_DEFAULT}>
{content?.mapSrc && (
<iframe
title={content.mapTitle || 'map'}
src={content.mapSrc}
width={content.mapWidth}
height={content.mapHeight}
style={{ border: 0 }}
allowFullScreen={content.mapAllowFullScreen}
loading={content.mapLoading === 'eager' ? 'eager' : 'lazy'}
referrerPolicy={
content.mapReferrerPolicy as React.HTMLAttributeReferrerPolicy | undefined
}
/>
)}
</div>
</div>
</SelectableBlock>
</div>
</div>
</div>
<DesignerLayer pageTitle="Contact" selection={selection} designer={designer} />
</div>
)
}
export default Contact

View file

@ -14,7 +14,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { createDemoAsync } from '@/services/demo.service' import { createDemoAsync } from '@/services/demo.service'
import { DemoDto } from '@/proxy/demo/models' import { DemoDto } from '@/proxy/demo/models'
import { Button, Notification, toast } from '@/components/ui' import { Button, Notification, toast } from '@/components/ui'
import { translateLabel } from './designer' import { translateLabel } from './localization'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
interface DemoModalProps { interface DemoModalProps {

View file

@ -1,904 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { FaArrowRight, FaChevronLeft, FaChevronRight } from 'react-icons/fa'
import PageTitle from '@/components/shared/PageTitle'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { PUBLIC_PAGE_DESIGN } from '@/constants/permission.constant'
import { Button } from '@/components/ui'
import { useOptionalDemo } from '@/contexts/DemoContext'
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
import {
DesignerLayer,
DesignerField,
DesignerSelection,
SelectableBlock,
TranslateFn,
iconField,
imageField,
resolveLocalizedValue,
styleField,
textAreaField,
textField,
translateLabel,
usePageDesigner,
} from './designer'
import { designerPageClass } from './pageStyles'
import { PageIcon, PublicPageLoader } from './shared'
interface HomeSlideServiceContent {
icon: string
title: string
titleKey: string
description: string
descriptionKey: string
styleClass: string
}
interface HomeSlideContent {
title: string
titleKey: string
subtitle: string
subtitleKey: string
styleClass: string
services: HomeSlideServiceContent[]
}
interface HomeCardContent {
icon: string
title: string
titleKey: string
description: string
descriptionKey: string
styleClass: string
}
interface HomeSolutionContent extends HomeCardContent {
colorClass: string
}
interface HomeContent {
heroBackgroundImage: string
heroBackgroundImageKey: string
heroPrimaryCtaLabel: string
heroPrimaryCtaKey: string
heroPrimaryCtaStyle: string
heroPrimaryCtaStyleKey: string
heroSecondaryCtaLabel: string
heroSecondaryCtaKey: string
heroSecondaryCtaStyle: string
heroSecondaryCtaStyleKey: string
featuresTitle: string
featuresTitleKey: string
featuresTitleStyle: string
featuresTitleStyleKey: string
featuresSubtitle: string
featuresSubtitleKey: string
featuresSubtitleStyle: string
featuresSubtitleStyleKey: string
solutionsTitle: string
solutionsTitleKey: string
solutionsTitleStyle: string
solutionsTitleStyleKey: string
solutionsSubtitle: string
solutionsSubtitleKey: string
solutionsSubtitleStyle: string
solutionsSubtitleStyleKey: string
ctaTitle: string
ctaTitleKey: string
ctaTitleStyle: string
ctaTitleStyleKey: string
ctaSubtitle: string
ctaSubtitleKey: string
ctaSubtitleStyle: string
ctaSubtitleStyleKey: string
ctaButtonLabel: string
ctaButtonLabelKey: string
ctaButtonStyle: string
ctaButtonStyleKey: string
slides: HomeSlideContent[]
features: HomeCardContent[]
solutions: HomeSolutionContent[]
}
const SLIDE_INTERVAL_MS = 10000
const HOME_HERO_DEFAULT_IMAGE =
'https://images.pexels.com/photos/3183150/pexels-photo-3183150.jpeg?auto=compress&cs=tinysrgb&w=1920'
const HOME_HERO_PRIMARY_CTA_STYLE_KEY = 'Public.home.hero.primaryCta.style'
const HOME_HERO_SECONDARY_CTA_STYLE_KEY = 'Public.home.hero.secondaryCta.style'
const HOME_FEATURES_TITLE_STYLE_KEY = 'Public.home.features.title.style'
const HOME_FEATURES_SUBTITLE_STYLE_KEY = 'Public.home.features.subtitle.style'
const HOME_SOLUTIONS_TITLE_STYLE_KEY = 'Public.home.solutions.title.style'
const HOME_SOLUTIONS_SUBTITLE_STYLE_KEY = 'Public.home.solutions.subtitle.style'
const HOME_CTA_TITLE_STYLE_KEY = 'Public.home.cta.title.style'
const HOME_CTA_SUBTITLE_STYLE_KEY = 'Public.home.cta.subtitle.style'
const HOME_CTA_BUTTON_STYLE_KEY = 'Public.home.cta.button.style'
const HERO_PRIMARY_CTA_STYLE_DEFAULT =
'inline-flex items-center justify-center px-8 py-4 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white rounded-lg font-semibold transition-all transform hover:scale-105'
const HERO_SECONDARY_CTA_STYLE_DEFAULT =
'inline-flex items-center justify-center px-8 py-4 bg-white/10 hover:bg-white/20 text-white rounded-lg font-semibold backdrop-blur-sm transition-all transform hover:scale-105'
const FEATURES_TITLE_STYLE_DEFAULT = 'text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4'
const FEATURES_SUBTITLE_STYLE_DEFAULT = 'text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto'
const SOLUTIONS_TITLE_STYLE_DEFAULT = 'text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4'
const SOLUTIONS_SUBTITLE_STYLE_DEFAULT =
'text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto'
const CTA_TITLE_STYLE_DEFAULT = 'text-3xl font-bold text-white mb-4'
const CTA_SUBTITLE_STYLE_DEFAULT = 'text-white text-lg mb-8'
const CTA_BUTTON_STYLE_DEFAULT =
'inline-block bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition-colors'
const SLIDE_STYLE_DEFAULT = 'max-w-4xl mx-auto text-center'
const SLIDE_SERVICE_STYLE_DEFAULT =
'bg-white/5 backdrop-blur-sm rounded-2xl p-8 text-center hover:scale-105 hover:bg-white/10 transition-all'
const FEATURE_CARD_STYLE_DEFAULT =
'p-8 bg-white dark:bg-gray-900 rounded-xl shadow-lg hover:shadow-xl transition-shadow dark:shadow-gray-950/40'
const SOLUTION_CARD_STYLE_DEFAULT = 'p-8 h-full rounded-2xl'
function buildHomeContent(home: HomeDto | undefined, translate: TranslateFn): HomeContent {
return {
heroBackgroundImage: resolveLocalizedValue(
translate,
home?.heroBackgroundImageKey,
HOME_HERO_DEFAULT_IMAGE,
),
heroBackgroundImageKey: home?.heroBackgroundImageKey || '',
heroPrimaryCtaLabel: resolveLocalizedValue(translate, home?.heroPrimaryCtaKey, ''),
heroPrimaryCtaKey: home?.heroPrimaryCtaKey || '',
heroPrimaryCtaStyle: resolveLocalizedValue(
translate,
HOME_HERO_PRIMARY_CTA_STYLE_KEY,
HERO_PRIMARY_CTA_STYLE_DEFAULT,
),
heroPrimaryCtaStyleKey: HOME_HERO_PRIMARY_CTA_STYLE_KEY,
heroSecondaryCtaLabel: resolveLocalizedValue(translate, home?.heroSecondaryCtaKey, ''),
heroSecondaryCtaKey: home?.heroSecondaryCtaKey || '',
heroSecondaryCtaStyle: resolveLocalizedValue(
translate,
HOME_HERO_SECONDARY_CTA_STYLE_KEY,
HERO_SECONDARY_CTA_STYLE_DEFAULT,
),
heroSecondaryCtaStyleKey: HOME_HERO_SECONDARY_CTA_STYLE_KEY,
featuresTitle: resolveLocalizedValue(translate, home?.featuresTitleKey, ''),
featuresTitleKey: home?.featuresTitleKey || '',
featuresTitleStyle: resolveLocalizedValue(
translate,
HOME_FEATURES_TITLE_STYLE_KEY,
FEATURES_TITLE_STYLE_DEFAULT,
),
featuresTitleStyleKey: HOME_FEATURES_TITLE_STYLE_KEY,
featuresSubtitle: resolveLocalizedValue(translate, home?.featuresSubtitleKey, ''),
featuresSubtitleKey: home?.featuresSubtitleKey || '',
featuresSubtitleStyle: resolveLocalizedValue(
translate,
HOME_FEATURES_SUBTITLE_STYLE_KEY,
FEATURES_SUBTITLE_STYLE_DEFAULT,
),
featuresSubtitleStyleKey: HOME_FEATURES_SUBTITLE_STYLE_KEY,
solutionsTitle: resolveLocalizedValue(translate, home?.solutionsTitleKey, ''),
solutionsTitleKey: home?.solutionsTitleKey || '',
solutionsTitleStyle: resolveLocalizedValue(
translate,
HOME_SOLUTIONS_TITLE_STYLE_KEY,
SOLUTIONS_TITLE_STYLE_DEFAULT,
),
solutionsTitleStyleKey: HOME_SOLUTIONS_TITLE_STYLE_KEY,
solutionsSubtitle: resolveLocalizedValue(translate, home?.solutionsSubtitleKey, ''),
solutionsSubtitleKey: home?.solutionsSubtitleKey || '',
solutionsSubtitleStyle: resolveLocalizedValue(
translate,
HOME_SOLUTIONS_SUBTITLE_STYLE_KEY,
SOLUTIONS_SUBTITLE_STYLE_DEFAULT,
),
solutionsSubtitleStyleKey: HOME_SOLUTIONS_SUBTITLE_STYLE_KEY,
ctaTitle: resolveLocalizedValue(translate, home?.ctaTitleKey, ''),
ctaTitleKey: home?.ctaTitleKey || '',
ctaTitleStyle: resolveLocalizedValue(
translate,
HOME_CTA_TITLE_STYLE_KEY,
CTA_TITLE_STYLE_DEFAULT,
),
ctaTitleStyleKey: HOME_CTA_TITLE_STYLE_KEY,
ctaSubtitle: resolveLocalizedValue(translate, home?.ctaSubtitleKey, ''),
ctaSubtitleKey: home?.ctaSubtitleKey || '',
ctaSubtitleStyle: resolveLocalizedValue(
translate,
HOME_CTA_SUBTITLE_STYLE_KEY,
CTA_SUBTITLE_STYLE_DEFAULT,
),
ctaSubtitleStyleKey: HOME_CTA_SUBTITLE_STYLE_KEY,
ctaButtonLabel: resolveLocalizedValue(translate, home?.ctaButtonLabelKey, ''),
ctaButtonLabelKey: home?.ctaButtonLabelKey || '',
ctaButtonStyle: resolveLocalizedValue(
translate,
HOME_CTA_BUTTON_STYLE_KEY,
CTA_BUTTON_STYLE_DEFAULT,
),
ctaButtonStyleKey: HOME_CTA_BUTTON_STYLE_KEY,
slides: (home?.slidesDto ?? []).map((slide) => ({
title: resolveLocalizedValue(translate, slide.titleKey, slide.titleKey),
titleKey: slide.titleKey || '',
subtitle: resolveLocalizedValue(translate, slide.subtitleKey, slide.subtitleKey),
subtitleKey: slide.subtitleKey || '',
styleClass: slide.styleClass || SLIDE_STYLE_DEFAULT,
services: (slide.services ?? []).map((service) => ({
icon: service.icon || '',
title: resolveLocalizedValue(translate, service.titleKey, service.titleKey),
titleKey: service.titleKey || '',
description: resolveLocalizedValue(
translate,
service.descriptionKey,
service.descriptionKey,
),
descriptionKey: service.descriptionKey || '',
styleClass: service.styleClass || SLIDE_SERVICE_STYLE_DEFAULT,
})),
})),
features: (home?.featuresDto ?? []).map((feature) => ({
icon: feature.icon || '',
title: resolveLocalizedValue(translate, feature.titleKey, feature.titleKey),
titleKey: feature.titleKey || '',
description: resolveLocalizedValue(translate, feature.descriptionKey, feature.descriptionKey),
descriptionKey: feature.descriptionKey || '',
styleClass: feature.styleClass || FEATURE_CARD_STYLE_DEFAULT,
})),
solutions: (home?.solutionsDto ?? []).map((solution) => ({
icon: solution.icon || '',
colorClass: solution.colorClass || '',
title: resolveLocalizedValue(translate, solution.titleKey, solution.titleKey),
titleKey: solution.titleKey || '',
description: resolveLocalizedValue(
translate,
solution.descriptionKey,
solution.descriptionKey,
),
descriptionKey: solution.descriptionKey || '',
styleClass: solution.styleClass || SOLUTION_CARD_STYLE_DEFAULT,
})),
}
}
function toSaveInput(content: HomeContent, cultureName: string) {
return {
cultureName,
heroBackgroundImageKey: content.heroBackgroundImageKey,
heroBackgroundImageValue: content.heroBackgroundImage,
heroPrimaryCtaKey: content.heroPrimaryCtaKey,
heroPrimaryCtaValue: content.heroPrimaryCtaLabel,
heroPrimaryCtaStyleKey: content.heroPrimaryCtaStyleKey,
heroPrimaryCtaStyleValue: content.heroPrimaryCtaStyle,
heroSecondaryCtaKey: content.heroSecondaryCtaKey,
heroSecondaryCtaValue: content.heroSecondaryCtaLabel,
heroSecondaryCtaStyleKey: content.heroSecondaryCtaStyleKey,
heroSecondaryCtaStyleValue: content.heroSecondaryCtaStyle,
featuresTitleKey: content.featuresTitleKey,
featuresTitleValue: content.featuresTitle,
featuresTitleStyleKey: content.featuresTitleStyleKey,
featuresTitleStyleValue: content.featuresTitleStyle,
featuresSubtitleKey: content.featuresSubtitleKey,
featuresSubtitleValue: content.featuresSubtitle,
featuresSubtitleStyleKey: content.featuresSubtitleStyleKey,
featuresSubtitleStyleValue: content.featuresSubtitleStyle,
solutionsTitleKey: content.solutionsTitleKey,
solutionsTitleValue: content.solutionsTitle,
solutionsTitleStyleKey: content.solutionsTitleStyleKey,
solutionsTitleStyleValue: content.solutionsTitleStyle,
solutionsSubtitleKey: content.solutionsSubtitleKey,
solutionsSubtitleValue: content.solutionsSubtitle,
solutionsSubtitleStyleKey: content.solutionsSubtitleStyleKey,
solutionsSubtitleStyleValue: content.solutionsSubtitleStyle,
ctaTitleKey: content.ctaTitleKey,
ctaTitleValue: content.ctaTitle,
ctaTitleStyleKey: content.ctaTitleStyleKey,
ctaTitleStyleValue: content.ctaTitleStyle,
ctaSubtitleKey: content.ctaSubtitleKey,
ctaSubtitleValue: content.ctaSubtitle,
ctaSubtitleStyleKey: content.ctaSubtitleStyleKey,
ctaSubtitleStyleValue: content.ctaSubtitleStyle,
ctaButtonLabelKey: content.ctaButtonLabelKey,
ctaButtonLabelValue: content.ctaButtonLabel,
ctaButtonStyleKey: content.ctaButtonStyleKey,
ctaButtonStyleValue: content.ctaButtonStyle,
slides: content.slides.map((slide) => ({
titleKey: slide.titleKey,
titleValue: slide.title,
subtitleKey: slide.subtitleKey,
subtitleValue: slide.subtitle,
styleClass: slide.styleClass,
services: slide.services.map((service) => ({
icon: service.icon,
titleKey: service.titleKey,
titleValue: service.title,
descriptionKey: service.descriptionKey,
descriptionValue: service.description,
styleClass: service.styleClass,
})),
})),
features: content.features.map((feature) => ({
icon: feature.icon,
titleKey: feature.titleKey,
titleValue: feature.title,
descriptionKey: feature.descriptionKey,
descriptionValue: feature.description,
styleClass: feature.styleClass,
})),
solutions: content.solutions.map((solution) => ({
icon: solution.icon,
colorClass: solution.colorClass,
titleKey: solution.titleKey,
titleValue: solution.title,
descriptionKey: solution.descriptionKey,
descriptionValue: solution.description,
styleClass: solution.styleClass,
})),
}
}
function cardFields(
path: string,
item: HomeCardContent,
iconLabel: string,
iconPlaceholder: string,
): DesignerField[] {
return [
iconField(`${path}.icon`, iconLabel, item.icon, { placeholder: iconPlaceholder }),
textField(`${path}.title`, item.titleKey, item.title),
textAreaField(`${path}.description`, item.descriptionKey, item.description),
styleField(`${path}.styleClass`, `${path}.styleClass`, item.styleClass),
]
}
const Home: React.FC = () => {
const { openDemo } = useOptionalDemo()
const { translate } = useLocalization()
const [home, setHome] = useState<HomeDto>()
const [loading, setLoading] = useState(true)
const [currentSlide, setCurrentSlide] = useState(0)
useEffect(() => {
const fetchHome = async () => {
setLoading(true)
try {
const result = await getHome()
setHome(result.data)
} catch (error) {
console.error('Home alinirken hata olustu:', error)
} finally {
setLoading(false)
}
}
fetchHome()
}, [])
const initialContent = useMemo(
() => (loading ? null : buildHomeContent(home, translate)),
[home, loading, translate],
)
const designer = usePageDesigner<HomeContent>({
pageKey: 'Home',
permission: PUBLIC_PAGE_DESIGN.HOME,
initialContent,
onSave: (content, cultureName) => saveHomePage(toSaveInput(content, cultureName)),
})
const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer
const slides = useMemo(() => content?.slides ?? [], [content?.slides])
const slideCount = slides.length
useEffect(() => {
if (slideCount > 0 && currentSlide >= slideCount) {
setCurrentSlide(0)
}
}, [currentSlide, slideCount])
// Tasarim modunda otomatik gecis kapalidir; aksi halde duzenlenen slayt kayiyor.
useEffect(() => {
if (slideCount < 2 || isDesignMode) {
return
}
const timer = setInterval(
() => setCurrentSlide((previous) => (previous + 1) % slideCount),
SLIDE_INTERVAL_MS,
)
return () => clearInterval(timer)
}, [isDesignMode, slideCount])
const goToSlide = (offset: number) =>
setCurrentSlide(
(previous) => (previous + offset + Math.max(slideCount, 1)) % Math.max(slideCount, 1),
)
const selection: DesignerSelection | null = useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
const iconLabel = translateLabel(translate, 'App.PublicDesigner.IkonAnahtari')
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: 'Public.home.hero.*',
description: translateLabel(translate, 'Public.designer.heroHint'),
fields: [
textField('heroPrimaryCtaLabel', content.heroPrimaryCtaKey, content.heroPrimaryCtaLabel),
textField(
'heroSecondaryCtaLabel',
content.heroSecondaryCtaKey,
content.heroSecondaryCtaLabel,
),
imageField(
'heroBackgroundImage',
content.heroBackgroundImageKey,
content.heroBackgroundImage,
{
group: 'media',
},
),
styleField(
'heroPrimaryCtaStyle',
content.heroPrimaryCtaStyleKey,
content.heroPrimaryCtaStyle,
),
styleField(
'heroSecondaryCtaStyle',
content.heroSecondaryCtaStyleKey,
content.heroSecondaryCtaStyle,
),
],
}
}
if (selectedBlockId.startsWith('slide-')) {
const index = Number(selectedBlockId.replace('slide-', ''))
const slide = content.slides[index]
if (!slide) {
return null
}
return {
id: selectedBlockId,
title: slide.titleKey || `Slide ${index + 1}`,
description: translateLabel(translate, 'Public.designer.slideHint'),
fields: [
textField(`slides.${index}.title`, slide.titleKey, slide.title),
textAreaField(`slides.${index}.subtitle`, slide.subtitleKey, slide.subtitle),
styleField(
`slides.${index}.styleClass`,
`Slide${index + 1}.StyleClass`,
slide.styleClass,
),
...slide.services.flatMap((service, serviceIndex) => {
const path = `slides.${index}.services.${serviceIndex}`
return [
iconField(
`${path}.icon`,
`Slide${index + 1}.Service${serviceIndex + 1}.Icon`,
service.icon,
{
placeholder: translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaUsers' }),
},
),
textField(`${path}.title`, service.titleKey, service.title),
textAreaField(`${path}.description`, service.descriptionKey, service.description),
styleField(
`${path}.styleClass`,
`Slide${index + 1}.Service${serviceIndex + 1}.StyleClass`,
service.styleClass,
),
]
}),
],
}
}
if (selectedBlockId === 'features-heading') {
return {
id: selectedBlockId,
title: content.featuresTitleKey,
description: translateLabel(translate, 'Public.designer.headingHint'),
fields: [
textField('featuresTitle', content.featuresTitleKey, content.featuresTitle),
textAreaField('featuresSubtitle', content.featuresSubtitleKey, content.featuresSubtitle),
styleField(
'featuresTitleStyle',
content.featuresTitleStyleKey,
content.featuresTitleStyle,
),
styleField(
'featuresSubtitleStyle',
content.featuresSubtitleStyleKey,
content.featuresSubtitleStyle,
),
],
}
}
if (selectedBlockId === 'solutions-heading') {
return {
id: selectedBlockId,
title: content.solutionsTitleKey,
description: translateLabel(translate, 'Public.designer.headingHint'),
fields: [
textField('solutionsTitle', content.solutionsTitleKey, content.solutionsTitle),
textAreaField(
'solutionsSubtitle',
content.solutionsSubtitleKey,
content.solutionsSubtitle,
),
styleField(
'solutionsTitleStyle',
content.solutionsTitleStyleKey,
content.solutionsTitleStyle,
),
styleField(
'solutionsSubtitleStyle',
content.solutionsSubtitleStyleKey,
content.solutionsSubtitleStyle,
),
],
}
}
if (selectedBlockId.startsWith('feature-')) {
const index = Number(selectedBlockId.replace('feature-', ''))
const item = content.features[index]
return item
? {
id: selectedBlockId,
title: item.titleKey || `Feature ${index + 1}`,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc1'),
fields: cardFields(
`features.${index}`,
item,
iconLabel,
translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaChartBar' }),
),
}
: null
}
if (selectedBlockId.startsWith('solution-')) {
const index = Number(selectedBlockId.replace('solution-', ''))
const item = content.solutions[index]
return item
? {
id: selectedBlockId,
title: item.titleKey || `Solution ${index + 1}`,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc1'),
fields: [
...cardFields(
`solutions.${index}`,
item,
iconLabel,
translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaChartBar' }),
),
styleField(
`solutions.${index}.colorClass`,
`Solution${index + 1}.ColorClass`,
item.colorClass,
),
],
}
: null
}
if (selectedBlockId === 'cta') {
return {
id: selectedBlockId,
title: content.ctaTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc2'),
fields: [
textField('ctaTitle', content.ctaTitleKey, content.ctaTitle),
textAreaField('ctaSubtitle', content.ctaSubtitleKey, content.ctaSubtitle),
textField('ctaButtonLabel', content.ctaButtonLabelKey, content.ctaButtonLabel),
styleField('ctaTitleStyle', content.ctaTitleStyleKey, content.ctaTitleStyle),
styleField('ctaSubtitleStyle', content.ctaSubtitleStyleKey, content.ctaSubtitleStyle),
styleField('ctaButtonStyle', content.ctaButtonStyleKey, content.ctaButtonStyle),
],
}
}
return null
}, [content, selectedBlockId, translate])
if (loading) {
return <PublicPageLoader />
}
return (
<div className={designerPageClass(isDesignMode, isPanelVisible)}>
<PageTitle title={translate('::App.Home')} />
{/* Hero carousel */}
<div className="relative">
<SelectableBlock
id="hero"
label="Hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className="relative min-h-screen overflow-hidden bg-blue-900 p-20 text-white dark:bg-gray-950">
<div className="absolute inset-0 bg-gradient-to-br from-blue-900 via-indigo-900 to-purple-900 dark:from-gray-950 dark:via-gray-950 dark:to-gray-950" />
<div
className="absolute inset-0 opacity-20 dark:opacity-35"
style={{
backgroundImage: `url("${content?.heroBackgroundImage || HOME_HERO_DEFAULT_IMAGE}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<div className="absolute inset-0 bg-blue-950/25 dark:bg-gray-950/45" />
<div className="container relative mx-auto flex h-screen items-center px-4 pb-16 pt-32">
{slides.map((slide, index) => (
<div
key={slide.titleKey || index}
aria-hidden={index !== currentSlide}
className={`absolute inset-0 transition-all duration-700 ease-in-out ${
index === currentSlide
? 'opacity-100 translate-x-0'
: index < currentSlide
? 'pointer-events-none opacity-0 -translate-x-full'
: 'pointer-events-none opacity-0 translate-x-full'
}`}
>
<SelectableBlock
id={`slide-${index}`}
label={`Slide ${index + 1}`}
isActive={selectedBlockId === `slide-${index}`}
isDesignMode={isDesignMode}
className="h-full"
onSelect={(id) => {
setCurrentSlide(index)
selectBlock(id)
}}
>
<div className={`container mx-auto px-4 pt-16 ${slide.styleClass}`}>
<div className="mx-auto max-w-4xl text-center">
<h1 className="animate-fade-in mb-6 text-3xl font-bold text-white md:text-6xl">
{slide.title}
</h1>
<p className="animate-fade-in-delay mb-12 text-xl text-gray-300 md:text-2xl">
{slide.subtitle}
</p>
<div className="mb-16 flex flex-col justify-center gap-6 md:flex-row">
<button
type="button"
className={
content?.heroPrimaryCtaStyle || HERO_PRIMARY_CTA_STYLE_DEFAULT
}
onClick={openDemo}
>
{content?.heroPrimaryCtaLabel}
<FaArrowRight className="ml-2" size={20} />
</button>
<Link
to={ROUTES_ENUM.public.products}
className={
content?.heroSecondaryCtaStyle || HERO_SECONDARY_CTA_STYLE_DEFAULT
}
>
{content?.heroSecondaryCtaLabel}
</Link>
</div>
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-3">
{slide.services.map((service, serviceIndex) => (
<div
key={service.titleKey || serviceIndex}
className={service.styleClass || SLIDE_SERVICE_STYLE_DEFAULT}
>
<PageIcon
name={service.icon}
className="mx-auto mb-4 text-blue-300"
size={40}
/>
<h3 className="mb-3 text-xl font-semibold text-white">
{service.title}
</h3>
<p className="text-gray-300">{service.description}</p>
</div>
))}
</div>
</div>
</div>
</SelectableBlock>
</div>
))}
</div>
{slideCount > 1 && (
<>
<Button
variant="plain"
shape="circle"
icon={<FaChevronLeft size={24} />}
className="absolute left-4 top-1/2 z-10 -translate-y-1/2 !h-14 !w-14 !px-0 bg-white/10 text-white backdrop-blur-sm transition-all hover:!bg-white/20"
aria-label="Previous slide"
onClick={() => goToSlide(-1)}
/>
<Button
variant="plain"
shape="circle"
icon={<FaChevronRight size={24} />}
className="absolute right-4 top-1/2 z-10 -translate-y-1/2 !h-14 !w-14 !px-0 bg-white/10 text-white backdrop-blur-sm transition-all hover:!bg-white/20"
aria-label="Next slide"
onClick={() => goToSlide(1)}
/>
<div className="absolute bottom-8 left-1/2 z-10 flex -translate-x-1/2 gap-3">
{slides.map((slide, index) => (
<Button
key={`indicator-${slide.titleKey || index}`}
variant="plain"
shape="circle"
className={`!h-3 !min-h-3 !px-0 rounded-full transition-all ${
index === currentSlide
? '!w-8 bg-white'
: '!w-3 bg-white/50 hover:bg-white/70'
}`}
aria-label={`Go to slide ${index + 1}`}
onClick={() => setCurrentSlide(index)}
/>
))}
</div>
</>
)}
</div>
</SelectableBlock>
{/* Tasarim modunda slayt secici; secilebilir blogun disinda durur ki tiklamalar yutulmasin. */}
{isDesignMode && slideCount > 0 && (
<div className="absolute bottom-24 left-1/2 z-30 flex -translate-x-1/2 gap-2 rounded-full bg-black/40 px-3 py-2 backdrop-blur">
{slides.map((slide, index) => (
<Button
key={`editor-slide-${slide.titleKey || index}`}
type="button"
variant="plain"
className={`!h-auto !rounded-full !px-3 !py-1 text-xs font-semibold text-white transition ${
selectedBlockId === `slide-${index}`
? 'bg-sky-500'
: 'bg-white/20 hover:bg-white/30'
}`}
onClick={() => {
setCurrentSlide(index)
selectBlock(`slide-${index}`)
}}
>
Slide {index + 1}
</Button>
))}
</div>
)}
</div>
{/* Features */}
<section className="bg-white py-20 dark:bg-gray-950">
<div className="container mx-auto px-4">
<SelectableBlock
id="features-heading"
label="Features"
isActive={selectedBlockId === 'features-heading'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className="mb-16 text-center">
<h2 className={content?.featuresTitleStyle || FEATURES_TITLE_STYLE_DEFAULT}>
{content?.featuresTitle}
</h2>
<p className={content?.featuresSubtitleStyle || FEATURES_SUBTITLE_STYLE_DEFAULT}>
{content?.featuresSubtitle}
</p>
</div>
</SelectableBlock>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
{content?.features.map((feature, index) => (
<SelectableBlock
key={feature.titleKey || index}
id={`feature-${index}`}
label={`Feature ${index + 1}`}
isActive={selectedBlockId === `feature-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={feature.styleClass || FEATURE_CARD_STYLE_DEFAULT}>
<PageIcon name={feature.icon} className="mb-6 h-12 w-12 text-blue-500" />
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{feature.title}
</h2>
<p className="text-gray-600 dark:text-gray-300">{feature.description}</p>
</div>
</SelectableBlock>
))}
</div>
</div>
</section>
{/* Solutions */}
<section className="bg-gray-50 py-20 dark:bg-gray-900">
<div className="container mx-auto px-4">
<SelectableBlock
id="solutions-heading"
label="Solutions"
isActive={selectedBlockId === 'solutions-heading'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className="mb-16 text-center">
<h2 className={content?.solutionsTitleStyle || SOLUTIONS_TITLE_STYLE_DEFAULT}>
{content?.solutionsTitle}
</h2>
<p className={content?.solutionsSubtitleStyle || SOLUTIONS_SUBTITLE_STYLE_DEFAULT}>
{content?.solutionsSubtitle}
</p>
</div>
</SelectableBlock>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
{content?.solutions.map((solution, index) => (
<SelectableBlock
key={solution.titleKey || index}
id={`solution-${index}`}
label={`Solution ${index + 1}`}
isActive={selectedBlockId === `solution-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div
className={`${solution.colorClass} ${solution.styleClass || SOLUTION_CARD_STYLE_DEFAULT}`}
>
<PageIcon name={solution.icon} className="mb-6 h-16 w-16 text-white" />
<h3 className="mb-4 text-2xl font-semibold text-white">{solution.title}</h3>
<p className="text-white/90">{solution.description}</p>
</div>
</SelectableBlock>
))}
</div>
</div>
</section>
{/* CTA */}
<SelectableBlock
id="cta"
label="CTA"
isActive={selectedBlockId === 'cta'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<section className="bg-blue-900 py-16 dark:bg-gray-800">
<div className="container mx-auto px-4 text-center">
<h2 className={content?.ctaTitleStyle || CTA_TITLE_STYLE_DEFAULT}>
{content?.ctaTitle}
</h2>
<p className={content?.ctaSubtitleStyle || CTA_SUBTITLE_STYLE_DEFAULT}>
{content?.ctaSubtitle}
</p>
<Link
to={ROUTES_ENUM.public.contact}
className={content?.ctaButtonStyle || CTA_BUTTON_STYLE_DEFAULT}
>
{content?.ctaButtonLabel}
</Link>
</div>
</section>
</SelectableBlock>
<DesignerLayer pageTitle="Home" selection={selection} designer={designer} />
</div>
)
}
export default Home

View file

@ -1,740 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import { FaCheckCircle } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import PageTitle from '@/components/shared/PageTitle'
import { ServiceDto } from '@/proxy/services/models'
import { getServices, saveServicesPage } from '@/services/service.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { PUBLIC_PAGE_DESIGN } from '@/constants/permission.constant'
import {
DesignerLayer,
DesignerField,
DesignerSelection,
SelectableBlock,
TranslateFn,
iconField,
imageField,
isLikelyLocalizationKey,
resolveLocalizedValue,
styleField,
textAreaField,
textField,
translateLabel,
usePageDesigner,
} from './designer'
import { designerPageClass, getIconColor } from './pageStyles'
import { PageIcon, PublicPageLoader } from './shared'
interface ServiceFeatureContent {
key: string
value: string
}
interface ServiceCardContent {
icon: string
title: string
titleKey: string
description: string
descriptionKey: string
cardStyleClassKey: string
cardStyleClass: string
titleStyleClassKey: string
titleStyleClass: string
descriptionStyleClassKey: string
descriptionStyleClass: string
featureStyleClassKey: string
featureStyleClass: string
features: ServiceFeatureContent[]
}
interface SupportCardContent {
icon: string
title: string
titleKey: string
cardStyleClassKey: string
cardStyleClass: string
titleStyleClassKey: string
titleStyleClass: string
features: ServiceFeatureContent[]
}
interface ServicesContent {
heroTitle: string
heroTitleKey: string
heroSectionStyleClassKey: string
heroSectionStyleClass: string
heroTitleStyleClassKey: string
heroTitleStyleClass: string
heroSubtitle: string
heroSubtitleKey: string
heroSubtitleStyleClassKey: string
heroSubtitleStyleClass: string
heroImage: string
heroImageKey: string
serviceItems: ServiceCardContent[]
supportTitle: string
supportTitleKey: string
supportTitleStyleClassKey: string
supportTitleStyleClass: string
supportButtonStyleClassKey: string
supportButtonStyleClass: string
supportItems: SupportCardContent[]
supportButtonLabel: string
supportButtonLabelKey: string
ctaTitle: string
ctaTitleKey: string
ctaSectionStyleClassKey: string
ctaSectionStyleClass: string
ctaTitleStyleClassKey: string
ctaTitleStyleClass: string
ctaDescription: string
ctaDescriptionKey: string
ctaDescriptionStyleClassKey: string
ctaDescriptionStyleClass: string
ctaButtonLabel: string
ctaButtonLabelKey: string
ctaButtonStyleClassKey: string
ctaButtonStyleClass: 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 = 'App.PublicServices.ServicesTitle'
const SERVICES_HERO_SUBTITLE_KEY = 'App.PublicServices.Subtitle'
const SERVICES_HERO_IMAGE_KEY = 'Public.services.heroImage'
const SERVICES_SUPPORT_TITLE_KEY = 'App.ServicesSupport.SupportTitle'
const SERVICES_SUPPORT_BUTTON_KEY = 'App.ServicesSupport.ContactButton'
const SERVICES_CTA_TITLE_KEY = 'App.ServicesCta.CtaTitle'
const SERVICES_CTA_DESCRIPTION_KEY = 'App.ServicesCta.CtaDescription'
// Destek karti ve CTA butonu ayni anahtari paylastigi icin biri duzenlendiginde digeri de
// degisiyordu; CTA butonu artik kendi anahtarini kullanir, ceviri yoksa destek metnine duser.
const SERVICES_CTA_BUTTON_KEY = 'Public.services.cta.contactButton'
const SERVICES_HERO_SECTION_STYLE_KEY = 'Public.services.hero.sectionStyleClass'
const SERVICES_HERO_TITLE_STYLE_KEY = 'Public.services.hero.titleStyleClass'
const SERVICES_HERO_SUBTITLE_STYLE_KEY = 'Public.services.hero.subtitleStyleClass'
const SERVICES_SUPPORT_TITLE_STYLE_KEY = 'Public.services.support.titleStyleClass'
const SERVICES_SUPPORT_BUTTON_STYLE_KEY = 'Public.services.support.buttonStyleClass'
const SERVICES_CTA_SECTION_STYLE_KEY = 'Public.services.cta.sectionStyleClass'
const SERVICES_CTA_TITLE_STYLE_KEY = 'Public.services.cta.titleStyleClass'
const SERVICES_CTA_DESCRIPTION_STYLE_KEY = 'Public.services.cta.descriptionStyleClass'
const SERVICES_CTA_BUTTON_STYLE_KEY = 'Public.services.cta.buttonStyleClass'
const HERO_SECTION_STYLE_DEFAULT = 'relative bg-blue-900 text-white py-12 dark:bg-gray-950'
const HERO_TITLE_STYLE_DEFAULT = 'text-5xl font-bold ml-4 mt-3 mb-2 text-white'
const HERO_SUBTITLE_STYLE_DEFAULT = 'text-xl max-w-3xl ml-4'
const SERVICE_CARD_STYLE_DEFAULT =
'bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 hover:shadow-xl transition-shadow dark:shadow-gray-950/40'
const SERVICE_TITLE_STYLE_DEFAULT = 'text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4'
const SERVICE_DESCRIPTION_STYLE_DEFAULT = 'text-gray-600 dark:text-gray-300 mb-6'
const SERVICE_FEATURE_STYLE_DEFAULT = 'flex items-center text-gray-700 dark:text-gray-300'
const SUPPORT_CARD_STYLE_DEFAULT =
'bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border border-gray-200 dark:border-gray-700 dark:shadow-gray-950/40'
const SUPPORT_TITLE_STYLE_DEFAULT = 'text-xl font-bold mb-4 text-gray-900 dark:text-gray-100'
const SUPPORT_HEADING_STYLE_DEFAULT =
'text-3xl font-bold text-center mb-10 text-gray-900 dark:text-gray-100'
const SUPPORT_BUTTON_STYLE_DEFAULT =
'block text-center bg-blue-600 text-white font-medium px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white'
const CTA_SECTION_STYLE_DEFAULT = 'bg-blue-900 text-white py-16 dark:bg-gray-800'
const CTA_TITLE_STYLE_DEFAULT = 'text-3xl font-bold mb-6 text-white'
const CTA_DESCRIPTION_STYLE_DEFAULT = 'text-xl mb-8 max-w-2xl mx-auto'
const CTA_BUTTON_STYLE_DEFAULT =
'inline-block bg-white text-blue-900 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition-colors'
function buildFeatures(features: string[] | undefined, prefix: string, translate: TranslateFn) {
return (features ?? []).map((feature, index) => ({
key: (isLikelyLocalizationKey(feature) ? feature : '') || `${prefix}.feature.${index + 1}`,
value: resolveLocalizedValue(translate, feature, feature),
}))
}
function buildServicesContent(services: ServiceDto[], translate: TranslateFn): ServicesContent {
const supportButtonLabel = resolveLocalizedValue(translate, SERVICES_SUPPORT_BUTTON_KEY)
return {
heroTitle: resolveLocalizedValue(translate, SERVICES_HERO_TITLE_KEY, 'Services'),
heroTitleKey: SERVICES_HERO_TITLE_KEY,
heroSectionStyleClassKey: SERVICES_HERO_SECTION_STYLE_KEY,
heroSectionStyleClass: resolveLocalizedValue(
translate,
SERVICES_HERO_SECTION_STYLE_KEY,
HERO_SECTION_STYLE_DEFAULT,
),
heroTitleStyleClassKey: SERVICES_HERO_TITLE_STYLE_KEY,
heroTitleStyleClass: resolveLocalizedValue(
translate,
SERVICES_HERO_TITLE_STYLE_KEY,
HERO_TITLE_STYLE_DEFAULT,
),
heroSubtitle: resolveLocalizedValue(translate, SERVICES_HERO_SUBTITLE_KEY),
heroSubtitleKey: SERVICES_HERO_SUBTITLE_KEY,
heroSubtitleStyleClassKey: SERVICES_HERO_SUBTITLE_STYLE_KEY,
heroSubtitleStyleClass: resolveLocalizedValue(
translate,
SERVICES_HERO_SUBTITLE_STYLE_KEY,
HERO_SUBTITLE_STYLE_DEFAULT,
),
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) => {
const prefix = `Public.services.dynamic.service.${index + 1}`
return {
icon: item.icon || '',
title: resolveLocalizedValue(translate, item.title, item.title),
titleKey: (isLikelyLocalizationKey(item.title) ? item.title : '') || `${prefix}.title`,
description: resolveLocalizedValue(translate, item.description, item.description || ''),
descriptionKey:
(item.description && isLikelyLocalizationKey(item.description)
? item.description
: '') || `${prefix}.description`,
cardStyleClassKey: `${prefix}.cardStyleClass`,
cardStyleClass: resolveLocalizedValue(
translate,
`${prefix}.cardStyleClass`,
SERVICE_CARD_STYLE_DEFAULT,
),
titleStyleClassKey: `${prefix}.titleStyleClass`,
titleStyleClass: resolveLocalizedValue(
translate,
`${prefix}.titleStyleClass`,
SERVICE_TITLE_STYLE_DEFAULT,
),
descriptionStyleClassKey: `${prefix}.descriptionStyleClass`,
descriptionStyleClass: resolveLocalizedValue(
translate,
`${prefix}.descriptionStyleClass`,
SERVICE_DESCRIPTION_STYLE_DEFAULT,
),
featureStyleClassKey: `${prefix}.featureStyleClass`,
featureStyleClass: resolveLocalizedValue(
translate,
`${prefix}.featureStyleClass`,
SERVICE_FEATURE_STYLE_DEFAULT,
),
features: buildFeatures(item.features, prefix, translate),
}
}),
supportTitle: resolveLocalizedValue(translate, SERVICES_SUPPORT_TITLE_KEY),
supportTitleKey: SERVICES_SUPPORT_TITLE_KEY,
supportTitleStyleClassKey: SERVICES_SUPPORT_TITLE_STYLE_KEY,
supportTitleStyleClass: resolveLocalizedValue(
translate,
SERVICES_SUPPORT_TITLE_STYLE_KEY,
SUPPORT_HEADING_STYLE_DEFAULT,
),
supportButtonStyleClassKey: SERVICES_SUPPORT_BUTTON_STYLE_KEY,
supportButtonStyleClass: resolveLocalizedValue(
translate,
SERVICES_SUPPORT_BUTTON_STYLE_KEY,
SUPPORT_BUTTON_STYLE_DEFAULT,
),
supportItems: services
.filter((item) => item.type === 'support')
.map((item, index) => {
const prefix = `Public.services.dynamic.support.${index + 1}`
return {
icon: item.icon || '',
title: resolveLocalizedValue(translate, item.title, item.title),
titleKey: (isLikelyLocalizationKey(item.title) ? item.title : '') || `${prefix}.title`,
cardStyleClassKey: `${prefix}.cardStyleClass`,
cardStyleClass: resolveLocalizedValue(
translate,
`${prefix}.cardStyleClass`,
SUPPORT_CARD_STYLE_DEFAULT,
),
titleStyleClassKey: `${prefix}.titleStyleClass`,
titleStyleClass: resolveLocalizedValue(
translate,
`${prefix}.titleStyleClass`,
SUPPORT_TITLE_STYLE_DEFAULT,
),
features: buildFeatures(item.features, prefix, translate),
}
}),
supportButtonLabel,
supportButtonLabelKey: SERVICES_SUPPORT_BUTTON_KEY,
ctaTitle: resolveLocalizedValue(translate, SERVICES_CTA_TITLE_KEY),
ctaTitleKey: SERVICES_CTA_TITLE_KEY,
ctaSectionStyleClassKey: SERVICES_CTA_SECTION_STYLE_KEY,
ctaSectionStyleClass: resolveLocalizedValue(
translate,
SERVICES_CTA_SECTION_STYLE_KEY,
CTA_SECTION_STYLE_DEFAULT,
),
ctaTitleStyleClassKey: SERVICES_CTA_TITLE_STYLE_KEY,
ctaTitleStyleClass: resolveLocalizedValue(
translate,
SERVICES_CTA_TITLE_STYLE_KEY,
CTA_TITLE_STYLE_DEFAULT,
),
ctaDescription: resolveLocalizedValue(translate, SERVICES_CTA_DESCRIPTION_KEY),
ctaDescriptionKey: SERVICES_CTA_DESCRIPTION_KEY,
ctaDescriptionStyleClassKey: SERVICES_CTA_DESCRIPTION_STYLE_KEY,
ctaDescriptionStyleClass: resolveLocalizedValue(
translate,
SERVICES_CTA_DESCRIPTION_STYLE_KEY,
CTA_DESCRIPTION_STYLE_DEFAULT,
),
ctaButtonLabel: resolveLocalizedValue(translate, SERVICES_CTA_BUTTON_KEY, supportButtonLabel),
ctaButtonLabelKey: SERVICES_CTA_BUTTON_KEY,
ctaButtonStyleClassKey: SERVICES_CTA_BUTTON_STYLE_KEY,
ctaButtonStyleClass: resolveLocalizedValue(
translate,
SERVICES_CTA_BUTTON_STYLE_KEY,
CTA_BUTTON_STYLE_DEFAULT,
),
}
}
function toSaveInput(content: ServicesContent, cultureName: string) {
return {
cultureName,
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,
})),
})),
styleTexts: [
{ key: content.heroSectionStyleClassKey, value: content.heroSectionStyleClass },
{ key: content.heroTitleStyleClassKey, value: content.heroTitleStyleClass },
{ key: content.heroSubtitleStyleClassKey, value: content.heroSubtitleStyleClass },
{ key: content.supportTitleStyleClassKey, value: content.supportTitleStyleClass },
{ key: content.supportButtonStyleClassKey, value: content.supportButtonStyleClass },
{ key: content.ctaSectionStyleClassKey, value: content.ctaSectionStyleClass },
{ key: content.ctaTitleStyleClassKey, value: content.ctaTitleStyleClass },
{ key: content.ctaDescriptionStyleClassKey, value: content.ctaDescriptionStyleClass },
{ key: content.ctaButtonStyleClassKey, value: content.ctaButtonStyleClass },
...content.serviceItems.flatMap((item) => [
{ key: item.cardStyleClassKey, value: item.cardStyleClass },
{ key: item.titleStyleClassKey, value: item.titleStyleClass },
{ key: item.descriptionStyleClassKey, value: item.descriptionStyleClass },
{ key: item.featureStyleClassKey, value: item.featureStyleClass },
]),
...content.supportItems.flatMap((item) => [
{ key: item.cardStyleClassKey, value: item.cardStyleClass },
{ key: item.titleStyleClassKey, value: item.titleStyleClass },
]),
],
}
}
function featureFields(
pathPrefix: string,
features: ServiceFeatureContent[],
fallbackPrefix: string,
): DesignerField[] {
return features.map((feature, index) =>
textField(
`${pathPrefix}.features.${index}.value`,
feature.key || `${fallbackPrefix}.feature.${index + 1}`,
feature.value,
),
)
}
const Services: React.FC = () => {
const { translate } = useLocalization()
const [services, setServices] = useState<ServiceDto[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchServices = async () => {
setLoading(true)
try {
const result = await getServices()
setServices(result?.data ?? [])
} catch (error) {
console.error('Service listesi alinirken hata olustu:', error)
setServices([])
} finally {
setLoading(false)
}
}
fetchServices()
}, [])
const initialContent = useMemo(
() => (loading ? null : buildServicesContent(services, translate)),
[loading, services, translate],
)
const designer = usePageDesigner<ServicesContent>({
pageKey: 'Services',
permission: PUBLIC_PAGE_DESIGN.SERVICES,
initialContent,
onSave: (content, cultureName) => saveServicesPage(toSaveInput(content, cultureName)),
})
const { content, isDesignMode, isPanelVisible, selectedBlockId, selectBlock } = designer
const selection: DesignerSelection | null = useMemo(() => {
if (!content || !selectedBlockId) {
return null
}
if (selectedBlockId === 'hero') {
return {
id: 'hero',
title: content.heroTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc4'),
fields: [
textField('heroTitle', content.heroTitleKey, content.heroTitle),
textAreaField('heroSubtitle', content.heroSubtitleKey, content.heroSubtitle),
imageField('heroImage', content.heroImageKey, content.heroImage, { group: 'media' }),
styleField(
'heroTitleStyleClass',
content.heroTitleStyleClassKey,
content.heroTitleStyleClass,
),
styleField(
'heroSubtitleStyleClass',
content.heroSubtitleStyleClassKey,
content.heroSubtitleStyleClass,
),
styleField(
'heroSectionStyleClass',
content.heroSectionStyleClassKey,
content.heroSectionStyleClass,
),
],
}
}
if (selectedBlockId === 'support-heading') {
return {
id: 'support-heading',
title: content.supportTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc3'),
fields: [
textField('supportTitle', content.supportTitleKey, content.supportTitle),
textField(
'supportButtonLabel',
content.supportButtonLabelKey,
content.supportButtonLabel,
),
styleField(
'supportTitleStyleClass',
content.supportTitleStyleClassKey,
content.supportTitleStyleClass,
),
styleField(
'supportButtonStyleClass',
content.supportButtonStyleClassKey,
content.supportButtonStyleClass,
),
],
}
}
if (selectedBlockId === 'cta') {
return {
id: 'cta',
title: content.ctaTitleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc2'),
fields: [
textField('ctaTitle', content.ctaTitleKey, content.ctaTitle),
textAreaField('ctaDescription', content.ctaDescriptionKey, content.ctaDescription),
textField('ctaButtonLabel', content.ctaButtonLabelKey, content.ctaButtonLabel),
styleField(
'ctaTitleStyleClass',
content.ctaTitleStyleClassKey,
content.ctaTitleStyleClass,
),
styleField(
'ctaDescriptionStyleClass',
content.ctaDescriptionStyleClassKey,
content.ctaDescriptionStyleClass,
),
styleField(
'ctaButtonStyleClass',
content.ctaButtonStyleClassKey,
content.ctaButtonStyleClass,
),
styleField(
'ctaSectionStyleClass',
content.ctaSectionStyleClassKey,
content.ctaSectionStyleClass,
),
],
}
}
if (selectedBlockId.startsWith('service-')) {
const index = Number(selectedBlockId.replace('service-', ''))
const item = content.serviceItems[index]
if (!item) {
return null
}
const path = `serviceItems.${index}`
return {
id: selectedBlockId,
title: item.titleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc1'),
fields: [
iconField(
`${path}.icon`,
translateLabel(translate, 'App.PublicDesigner.IkonAnahtari'),
item.icon,
{ placeholder: translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaServer' }) },
),
textField(`${path}.title`, item.titleKey, item.title),
textAreaField(`${path}.description`, item.descriptionKey, item.description),
...featureFields(path, item.features, `Public.services.dynamic.service.${index + 1}`),
styleField(`${path}.cardStyleClass`, item.cardStyleClassKey, item.cardStyleClass),
styleField(`${path}.titleStyleClass`, item.titleStyleClassKey, item.titleStyleClass),
styleField(
`${path}.descriptionStyleClass`,
item.descriptionStyleClassKey,
item.descriptionStyleClass,
),
styleField(
`${path}.featureStyleClass`,
item.featureStyleClassKey,
item.featureStyleClass,
),
],
}
}
if (selectedBlockId.startsWith('support-')) {
const index = Number(selectedBlockId.replace('support-', ''))
const item = content.supportItems[index]
if (!item) {
return null
}
const path = `supportItems.${index}`
return {
id: selectedBlockId,
title: item.titleKey,
description: translateLabel(translate, 'App.PublicDesigner.DesignerDesc1'),
fields: [
iconField(
`${path}.icon`,
translateLabel(translate, 'App.PublicDesigner.IkonAnahtari'),
item.icon,
{ placeholder: translate('::App.PublicDesigner.IconPlaceholder', { icon: 'FaLifeRing' }) },
),
textField(`${path}.title`, item.titleKey, item.title),
...featureFields(path, item.features, `Public.services.dynamic.support.${index + 1}`),
styleField(`${path}.cardStyleClass`, item.cardStyleClassKey, item.cardStyleClass),
styleField(`${path}.titleStyleClass`, item.titleStyleClassKey, item.titleStyleClass),
],
}
}
return null
}, [content, selectedBlockId, translate])
if (loading) {
return <PublicPageLoader />
}
return (
<div className={designerPageClass(isDesignMode, isPanelVisible)}>
<PageTitle title={translate('::App.Services')} />
{/* Hero */}
<SelectableBlock
id="hero"
label="Hero"
isActive={selectedBlockId === 'hero'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.heroSectionStyleClass || HERO_SECTION_STYLE_DEFAULT}>
<div
className="absolute inset-0 opacity-20 dark:opacity-35"
style={{
backgroundImage: `url("${content?.heroImage || SERVICES_HERO_IMAGE}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<div className="absolute inset-0 bg-blue-950/25 dark:bg-gray-950/45" />
<div className="container relative mx-auto pt-20">
<h1 className={content?.heroTitleStyleClass || HERO_TITLE_STYLE_DEFAULT}>
{content?.heroTitle}
</h1>
<p className={content?.heroSubtitleStyleClass || HERO_SUBTITLE_STYLE_DEFAULT}>
{content?.heroSubtitle}
</p>
</div>
</div>
</SelectableBlock>
{/* Services */}
<div className="py-6">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{content?.serviceItems.map((service, index) => (
<SelectableBlock
key={`${service.titleKey}-${index}`}
id={`service-${index}`}
label={`Service ${index + 1}`}
isActive={selectedBlockId === `service-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={service.cardStyleClass || SERVICE_CARD_STYLE_DEFAULT}>
<PageIcon
name={service.icon}
className={`mb-6 h-12 w-12 ${getIconColor(index)}`}
/>
<h3 className={service.titleStyleClass || SERVICE_TITLE_STYLE_DEFAULT}>
{service.title}
</h3>
<p className={service.descriptionStyleClass || SERVICE_DESCRIPTION_STYLE_DEFAULT}>
{service.description}
</p>
<ul className="space-y-2">
{service.features.map((feature, featureIndex) => (
<li
key={feature.key || featureIndex}
className={service.featureStyleClass || SERVICE_FEATURE_STYLE_DEFAULT}
>
<span className="mr-2 h-2 w-2 rounded-full bg-blue-600" />
{feature.value}
</li>
))}
</ul>
</div>
</SelectableBlock>
))}
</div>
</div>
</div>
{/* Support plans */}
<div className="bg-white py-10 dark:bg-gray-900">
<div className="container mx-auto px-4">
<SelectableBlock
id="support-heading"
label="Support"
isActive={selectedBlockId === 'support-heading'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<h2 className={content?.supportTitleStyleClass || SUPPORT_HEADING_STYLE_DEFAULT}>
{content?.supportTitle}
</h2>
</SelectableBlock>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{content?.supportItems.map((plan, index) => (
<SelectableBlock
key={`${plan.titleKey}-${index}`}
id={`support-${index}`}
label={`Support ${index + 1}`}
isActive={selectedBlockId === `support-${index}`}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={plan.cardStyleClass || SUPPORT_CARD_STYLE_DEFAULT}>
<PageIcon name={plan.icon} className={`mb-6 h-12 w-12 ${getIconColor(index)}`} />
<h3 className={plan.titleStyleClass || SUPPORT_TITLE_STYLE_DEFAULT}>
{plan.title}
</h3>
<ul className="mb-8 space-y-3">
{plan.features.map((feature, featureIndex) => (
<li
key={feature.key || featureIndex}
className="flex items-center space-x-2 text-gray-700 dark:text-gray-300"
>
<FaCheckCircle className="h-5 w-5 flex-shrink-0 text-green-500" />
<span>{feature.value}</span>
</li>
))}
</ul>
<Link
to={ROUTES_ENUM.public.contact}
className={content?.supportButtonStyleClass || SUPPORT_BUTTON_STYLE_DEFAULT}
>
{content?.supportButtonLabel}
</Link>
</div>
</SelectableBlock>
))}
</div>
</div>
</div>
{/* CTA */}
<SelectableBlock
id="cta"
label="CTA"
isActive={selectedBlockId === 'cta'}
isDesignMode={isDesignMode}
onSelect={selectBlock}
>
<div className={content?.ctaSectionStyleClass || CTA_SECTION_STYLE_DEFAULT}>
<div className="container mx-auto px-4 text-center">
<h2 className={content?.ctaTitleStyleClass || CTA_TITLE_STYLE_DEFAULT}>
{content?.ctaTitle}
</h2>
<p className={content?.ctaDescriptionStyleClass || CTA_DESCRIPTION_STYLE_DEFAULT}>
{content?.ctaDescription}
</p>
<Link
to={ROUTES_ENUM.public.contact}
className={content?.ctaButtonStyleClass || CTA_BUTTON_STYLE_DEFAULT}
>
{content?.ctaButtonLabel}
</Link>
</div>
</div>
</SelectableBlock>
<DesignerLayer pageTitle="Services" selection={selection} designer={designer} />
</div>
)
}
export default Services

View file

@ -1,410 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import Avatar from '@/components/ui/Avatar'
import { Button, Input } from '@/components/ui'
import { IconPickerField } from '@/views/shared/MenuAddDialog'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { normalizeLanguageKey, translateLabel } from './localization'
import { DesignerField, DesignerFieldValue, DesignerLanguageOption, DesignerSelection } from './types'
const GROUP_ORDER = ['content', 'media', 'style', 'advanced']
const GROUP_FALLBACK_LABELS: Record<string, string> = {
content: 'Icerik',
media: 'Gorsel',
style: 'Stil',
advanced: 'Gelismis',
}
interface DesignerDrawerProps {
isOpen: boolean
selection: DesignerSelection | null
pageTitle: string
selectedLanguage: string
languages: DesignerLanguageOption[]
isSaving?: boolean
isDirty?: boolean
canUndo?: boolean
canRedo?: boolean
onClose: () => void
onSave: () => void
onLanguageChange: (cultureName: string) => void
onReset: () => void
onFieldChange: (fieldKey: string, value: DesignerFieldValue) => void
onUndo?: () => void
onRedo?: () => void
}
function toDisplayString(value: DesignerFieldValue) {
if (Array.isArray(value)) {
return value.join('\n')
}
if (typeof value === 'boolean' || typeof value === 'number') {
return String(value)
}
return value ?? ''
}
const StyleClassEditor: React.FC<{
field: DesignerField
onChange: (value: string) => void
}> = ({ field, onChange }) => {
const tokens = toDisplayString(field.value).split(/\s+/).filter(Boolean)
return (
<div className="space-y-2">
<Input
textArea
rows={field.rows ?? 3}
value={toDisplayString(field.value)}
placeholder={field.placeholder}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value)}
/>
{tokens.length > 0 && (
<div className="flex flex-wrap gap-1">
{tokens.map((token, index) => (
<button
key={`${token}-${index}`}
type="button"
title="Kaldir"
className="rounded-full bg-slate-100 px-2 py-0.5 font-mono text-[11px] text-slate-600 transition-colors hover:bg-rose-100 hover:text-rose-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-rose-900/40 dark:hover:text-rose-300"
onClick={() => onChange(tokens.filter((_, i) => i !== index).join(' '))}
>
{token}
</button>
))}
</div>
)}
</div>
)
}
const ImageFieldEditor: React.FC<{
field: DesignerField
onChange: (value: string) => void
}> = ({ field, onChange }) => {
const value = toDisplayString(field.value)
return (
<div className="space-y-2">
<Input
value={value}
placeholder={field.placeholder ?? 'https://...'}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value)}
/>
{value && (
<div className="overflow-hidden rounded-lg border border-slate-200 bg-slate-50 dark:border-gray-700 dark:bg-gray-800">
<img
src={value}
alt={field.label}
loading="lazy"
className="h-28 w-full object-cover"
onError={(event) => {
event.currentTarget.style.display = 'none'
}}
/>
</div>
)}
</div>
)
}
const FieldEditor: React.FC<{
field: DesignerField
onChange: (fieldKey: string, value: DesignerFieldValue) => void
}> = ({ field, onChange }) => {
switch (field.type) {
case 'icon':
return (
<IconPickerField
value={toDisplayString(field.value)}
onChange={(iconKey) => onChange(field.key, iconKey)}
/>
)
case 'image':
return <ImageFieldEditor field={field} onChange={(value) => onChange(field.key, value)} />
case 'style':
return <StyleClassEditor field={field} onChange={(value) => onChange(field.key, value)} />
case 'boolean':
return (
<label className="flex cursor-pointer items-center gap-2 text-sm text-slate-600 dark:text-gray-300">
<Input
unstyle
type="checkbox"
checked={field.value === true || field.value === 'true'}
className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500 dark:border-gray-600"
onChange={(event) => onChange(field.key, event.target.checked)}
/>
{field.placeholder ?? field.label}
</label>
)
case 'number':
return (
<Input
type="number"
value={toDisplayString(field.value)}
placeholder={field.placeholder}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const parsed = Number(event.target.value)
onChange(field.key, Number.isFinite(parsed) ? parsed : 0)
}}
/>
)
case 'list':
return (
<Input
textArea
rows={field.rows ?? 5}
value={toDisplayString(field.value)}
placeholder={field.placeholder}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(
field.key,
event.target.value
.split('\n')
.map((item) => item.trim())
.filter(Boolean),
)
}
/>
)
case 'textarea':
return (
<Input
textArea
rows={field.rows ?? 4}
value={toDisplayString(field.value)}
placeholder={field.placeholder}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(field.key, event.target.value)
}
/>
)
default:
return (
<Input
value={toDisplayString(field.value)}
placeholder={field.placeholder}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(field.key, event.target.value)
}
/>
)
}
}
const DesignerDrawer: React.FC<DesignerDrawerProps> = ({
isOpen,
selection,
pageTitle,
selectedLanguage,
languages,
isSaving = false,
isDirty = false,
canUndo = false,
canRedo = false,
onClose,
onSave,
onLanguageChange,
onReset,
onFieldChange,
onUndo,
onRedo,
}) => {
const { translate } = useLocalization()
const [search, setSearch] = useState('')
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({})
useEffect(() => {
setSearch('')
}, [selection?.id])
const groupedFields = useMemo(() => {
const query = search.trim().toLowerCase()
const fields = (selection?.fields ?? []).filter(
(field) =>
!query ||
field.label.toLowerCase().includes(query) ||
field.key.toLowerCase().includes(query),
)
const groups = new Map<string, DesignerField[]>()
fields.forEach((field) => {
const group = field.group ?? 'content'
groups.set(group, [...(groups.get(group) ?? []), field])
})
return Array.from(groups.entries()).sort(([a], [b]) => {
const indexA = GROUP_ORDER.indexOf(a)
const indexB = GROUP_ORDER.indexOf(b)
return (indexA === -1 ? GROUP_ORDER.length : indexA) - (indexB === -1 ? GROUP_ORDER.length : indexB)
})
}, [search, selection])
const totalFieldCount = selection?.fields.length ?? 0
if (!isOpen) {
return null
}
return (
<aside className="fixed inset-y-0 right-0 z-50 flex w-full max-w-[420px] flex-col border-l border-slate-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
<header className="border-b border-slate-200 px-5 py-4 dark:border-gray-700">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-700 dark:text-sky-400">
{pageTitle} {translateLabel(translate, 'App.PublicDesigner.DesignerTitle')}
</p>
<h3 className="mt-2 truncate text-lg font-semibold text-slate-900 dark:text-gray-100">
{selection?.title ??
translateLabel(translate, 'App.PublicDesigner.NoSelection')}
</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-gray-400">
{selection?.description ??
translateLabel(translate, 'App.PublicDesigner.SelectField')}
</p>
</div>
<Button variant="plain" size="sm" onClick={onClose}>
{translateLabel(translate, 'App.Platform.Hide')}
</Button>
</div>
<div className="mt-4 flex flex-wrap items-center gap-1.5">
{languages.map((language) => (
<Button
key={`${language.cultureName}-${language.key}`}
type="button"
variant="plain"
shape="circle"
title={language.displayName}
aria-label={language.displayName}
aria-pressed={normalizeLanguageKey(selectedLanguage) === language.key}
className={`!h-9 !w-9 !px-0 border transition-all ${
normalizeLanguageKey(selectedLanguage) === language.key
? 'border-sky-500 bg-sky-50 ring-2 ring-sky-200 dark:bg-sky-900/20 dark:ring-sky-700'
: 'border-slate-200 hover:border-slate-400 hover:bg-slate-50 dark:border-gray-700 dark:hover:border-gray-500 dark:hover:bg-gray-800'
}`}
onClick={() => onLanguageChange(language.cultureName)}
>
<Avatar size={22} shape="circle" src={`/img/countries/${language.cultureName}.png`} />
</Button>
))}
<span className="ml-auto flex items-center gap-1">
<Button
type="button"
variant="plain"
size="sm"
disabled={!canUndo}
title={translateLabel(translate, 'Public.designer.undo')}
onClick={onUndo}
>
</Button>
<Button
type="button"
variant="plain"
size="sm"
disabled={!canRedo}
title={translateLabel(translate, 'Public.designer.redo')}
onClick={onRedo}
>
</Button>
</span>
</div>
{totalFieldCount > 6 && (
<div className="mt-3">
<Input
size="sm"
value={search}
placeholder={translateLabel(translate, 'Public.designer.searchField')}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setSearch(event.target.value)}
/>
</div>
)}
</header>
<div className="flex-1 space-y-5 overflow-y-auto px-5 py-5">
{!selection && (
<div className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-sm text-slate-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
{translateLabel(translate, 'App.PublicDesigner.NoSelectionDetails')}
</div>
)}
{selection && groupedFields.length === 0 && (
<p className="text-sm text-slate-500 dark:text-gray-400">
{translateLabel(translate, 'App.Platform.NoDataFound')}
</p>
)}
{groupedFields.map(([group, fields]) => {
const isCollapsed = collapsedGroups[group] === true
return (
<section key={group}>
<button
type="button"
className="mb-2 flex w-full items-center justify-between text-xs font-semibold uppercase tracking-[0.14em] text-slate-400 transition-colors hover:text-slate-600 dark:text-gray-500 dark:hover:text-gray-300"
onClick={() =>
setCollapsedGroups((previous) => ({ ...previous, [group]: !isCollapsed }))
}
>
<span>
{translateLabel(
translate,
`Public.designer.group.${group}`,
GROUP_FALLBACK_LABELS[group] ?? group,
)}
<span className="ml-2 font-normal normal-case tracking-normal">
({fields.length})
</span>
</span>
<span>{isCollapsed ? '+' : ''}</span>
</button>
{!isCollapsed && (
<div className="space-y-4">
{fields.map((field) => (
<div key={field.key}>
<label className="mb-1 block text-sm font-semibold text-slate-700 dark:text-gray-200">
{field.label}
</label>
{field.helpText && (
<p className="mb-2 text-xs text-slate-500 dark:text-gray-400">
{field.helpText}
</p>
)}
<FieldEditor field={field} onChange={onFieldChange} />
</div>
))}
</div>
)}
</section>
)
})}
</div>
<footer className="flex items-center gap-2 border-t border-slate-200 px-5 py-4 dark:border-gray-700">
<Button type="button" variant="plain" disabled={!isDirty || isSaving} onClick={onReset}>
{translateLabel(translate, 'Public.designer.reset')}
</Button>
<Button
block
type="button"
variant="solid"
loading={isSaving}
disabled={!isDirty || isSaving}
onClick={onSave}
>
{translate('::App.Platform.Save')}
</Button>
</footer>
</aside>
)
}
export default DesignerDrawer

View file

@ -1,100 +0,0 @@
import React from 'react'
import { Button } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DesignerDrawer from './DesignerDrawer'
import { translateLabel } from './localization'
import { DesignerFieldValue, DesignerLanguageOption, DesignerSelection } from './types'
export interface DesignerLayerController {
isDesignMode: boolean
isPanelVisible: boolean
isSaving: boolean
isDirty: boolean
canUndo: boolean
canRedo: boolean
selectedLanguage: string
drawerLanguages: DesignerLanguageOption[]
setIsPanelVisible: (visible: boolean) => void
changeLanguage: (cultureName: string) => void
changeField: (fieldKey: string, value: DesignerFieldValue) => void
resetContent: () => void
exitDesignMode: () => void
save: () => void
undo: () => void
redo: () => void
}
interface DesignerLayerProps {
pageTitle: string
selection: DesignerSelection | null
designer: DesignerLayerController
}
/**
* Tasarim modunun sayfadan bagimsiz arayuzu: durum cubugu ve duzenleme paneli.
* Tasarim modu kapaliyken hicbir sey render etmez.
*/
const DesignerLayer: React.FC<DesignerLayerProps> = ({ pageTitle, selection, designer }) => {
const { translate } = useLocalization()
if (!designer.isDesignMode) {
return null
}
return (
<>
<div className="fixed bottom-6 left-6 z-40 flex items-center gap-2 rounded-full bg-slate-900/95 px-3 py-2 text-sm font-medium text-white shadow-xl backdrop-blur">
<span className="pl-1">{pageTitle}</span>
<span
className={`h-2 w-2 rounded-full ${designer.isDirty ? 'bg-amber-400' : 'bg-emerald-400'}`}
title={
designer.isDirty
? translateLabel(translate, 'Public.designer.unsaved')
: translateLabel(translate, 'Public.designer.saved')
}
/>
<Button
type="button"
variant="plain"
size="sm"
className="!text-white"
onClick={() => designer.setIsPanelVisible(!designer.isPanelVisible)}
>
{designer.isPanelVisible
? translateLabel(translate, 'Public.designer.hidePanel')
: translateLabel(translate, 'App.PublicDesigner.ShowPanel')}
</Button>
<Button
type="button"
variant="plain"
size="sm"
className="!text-white"
onClick={designer.exitDesignMode}
>
{translateLabel(translate, 'Public.designer.exit')}
</Button>
</div>
<DesignerDrawer
isOpen={designer.isPanelVisible}
selection={selection}
pageTitle={pageTitle}
selectedLanguage={designer.selectedLanguage}
languages={designer.drawerLanguages}
isSaving={designer.isSaving}
isDirty={designer.isDirty}
canUndo={designer.canUndo}
canRedo={designer.canRedo}
onClose={() => designer.setIsPanelVisible(false)}
onSave={designer.save}
onLanguageChange={designer.changeLanguage}
onReset={designer.resetContent}
onFieldChange={designer.changeField}
onUndo={designer.undo}
onRedo={designer.redo}
/>
</>
)
}
export default DesignerLayer

View file

@ -1,77 +0,0 @@
import classNames from 'classnames'
import React, { PropsWithChildren, useCallback } from 'react'
interface SelectableBlockProps extends PropsWithChildren {
id: string
isActive: boolean
isDesignMode: boolean
onSelect: (id: string) => void
/** Blok uzerinde gorunen etiket. Verilmezse `id` kullanilir. */
label?: string
className?: string
}
/**
* Tasarim modunda icerigi secilebilir hale getirir.
* Tasarim modu kapaliyken hicbir sarmalayici DOM dugumu uretmez.
*/
const SelectableBlock: React.FC<SelectableBlockProps> = ({
id,
isActive,
isDesignMode,
onSelect,
label,
className,
children,
}) => {
const handleSelect = useCallback(
(event: React.SyntheticEvent) => {
// Tasarim modunda blok icindeki link/buton tiklamalari sayfayi degistirmemeli;
// tiklama yalnizca blogu secmeli.
event.preventDefault()
event.stopPropagation()
onSelect(id)
},
[id, onSelect],
)
if (!isDesignMode) {
return <>{children}</>
}
return (
<div
className={classNames(
'group/designer relative rounded-xl transition-all cursor-pointer',
isActive
? 'ring-2 ring-sky-500 ring-offset-4 ring-offset-white dark:ring-offset-gray-950'
: 'ring-1 ring-sky-200/80 hover:ring-sky-400 dark:ring-sky-700/80 dark:hover:ring-sky-500',
className,
)}
role="button"
tabIndex={0}
aria-pressed={isActive}
aria-label={label ?? id}
onClickCapture={handleSelect}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
handleSelect(event)
}
}}
>
<span
className={classNames(
'pointer-events-none absolute -top-2.5 left-3 z-20 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider shadow-sm transition-opacity',
isActive
? 'bg-sky-500 text-white opacity-100'
: 'bg-slate-900/80 text-white opacity-0 group-hover/designer:opacity-100',
)}
>
{label ?? id}
</span>
{children}
</div>
)
}
export default SelectableBlock

View file

@ -1,45 +0,0 @@
import { DesignerField } from './types'
interface FieldExtras {
group?: string
placeholder?: string
helpText?: string
rows?: number
}
function build(
type: DesignerField['type'],
key: string,
label: string,
value: DesignerField['value'],
extras?: FieldExtras,
): DesignerField {
return { key, label, type, value, ...extras }
}
export const textField = (key: string, label: string, value: string, extras?: FieldExtras) =>
build('text', key, label, value, extras)
export const textAreaField = (key: string, label: string, value: string, extras?: FieldExtras) =>
build('textarea', key, label, value, extras)
export const imageField = (key: string, label: string, value: string, extras?: FieldExtras) =>
build('image', key, label, value, extras)
export const iconField = (key: string, label: string, value: string, extras?: FieldExtras) =>
build('icon', key, label, value, extras)
export const numberField = (key: string, label: string, value: number, extras?: FieldExtras) =>
build('number', key, label, value, extras)
export const booleanField = (key: string, label: string, value: boolean, extras?: FieldExtras) =>
build('boolean', key, label, value, extras)
/**
* Tailwind sinif listesi alani. Panelde tek satirlik editor + sinif rozetleri ile gosterilir.
*/
export const styleField = (key: string, label: string, value: string, extras?: FieldExtras) =>
build('style', key, label, value, { group: extras?.group ?? 'style', ...extras })
/** Alan gruplarinin panelde gorunecegi sira. */
export const FIELD_GROUP_ORDER = ['content', 'media', 'style', 'advanced'] as const

View file

@ -1,10 +0,0 @@
export { default as DesignerDrawer } from './DesignerDrawer'
export { default as DesignerLayer } from './DesignerLayer'
export { default as SelectableBlock } from './SelectableBlock'
export * from './fields'
export * from './localization'
export * from './paths'
export * from './types'
export * from './useDesignerLanguages'
export { useDesignerState } from './useDesignerState'
export { usePageDesigner } from './usePageDesigner'

View file

@ -1,50 +0,0 @@
export type TranslateFn = (key: string) => string
const LOCALIZATION_KEY_PATTERN = /^[A-Za-z0-9_.-]+$/
/**
* Bir metnin lokalizasyon anahtari mi yoksa duz metin mi oldugunu tahmin eder.
* Anahtarlar bosluk icermez ve en az bir nokta ile ayrilmis segment tasir.
*/
export function isLikelyLocalizationKey(value?: string) {
return Boolean(value && LOCALIZATION_KEY_PATTERN.test(value) && value.includes('.'))
}
/**
* Deger bir lokalizasyon anahtari ise cevirisini, degilse degerin kendisini dondurur.
* Ceviri bulunamadiginda `fallback` kullanilir.
*/
export function resolveLocalizedValue(
translate: TranslateFn,
keyOrValue: string | undefined,
fallback = '',
) {
if (!keyOrValue) {
return fallback
}
if (!isLikelyLocalizationKey(keyOrValue)) {
return keyOrValue
}
const translatedValue = translate('::' + keyOrValue)
return translatedValue === keyOrValue ? fallback || keyOrValue : translatedValue
}
/**
* Designer arayuz metinleri icin: anahtar tanimli degilse okunabilir bir yedek metin dondurur.
*/
export function translateLabel(translate: TranslateFn, key: string, fallback?: string) {
const value = translate('::' + key)
return value === key ? (fallback ?? key) : value
}
export function normalizeLanguageKey(language?: string) {
if (!language) {
return 'tr'
}
return language.toLowerCase().split('-')[0]
}

View file

@ -1,61 +0,0 @@
type AnyRecord = Record<string, unknown>
function splitPath(path: string) {
return path.split('.').filter((segment) => segment.length > 0)
}
function isIndexSegment(segment: string) {
return /^\d+$/.test(segment)
}
/** `slides.0.services.1.icon` gibi bir yolu okur. */
export function getByPath(source: unknown, path: string): unknown {
return splitPath(path).reduce<unknown>((accumulator, segment) => {
if (accumulator === null || typeof accumulator !== 'object') {
return undefined
}
return (accumulator as AnyRecord)[segment]
}, source)
}
/**
* Yol boyunca yalnizca degisen dugumleri kopyalayarak yeni bir nesne dondurur.
* Yol gecersizse (ornegin dizi indeksi yoksa) kaynak nesne degistirilmeden dondurulur.
*/
export function setByPath<T>(source: T, path: string, value: unknown): T {
const segments = splitPath(path)
if (segments.length === 0 || source === null || typeof source !== 'object') {
return source
}
const [segment, ...rest] = segments
const container = source as unknown as AnyRecord
if (Array.isArray(source)) {
if (!isIndexSegment(segment)) {
return source
}
const index = Number(segment)
if (index < 0 || index >= source.length) {
return source
}
const next = [...(source as unknown[])]
next[index] = rest.length === 0 ? value : setByPath(next[index], rest.join('.'), value)
return next as unknown as T
}
if (!(segment in container)) {
return source
}
return {
...container,
[segment]: rest.length === 0 ? value : setByPath(container[segment], rest.join('.'), value),
} as unknown as T
}

View file

@ -1,40 +0,0 @@
export type DesignerFieldType =
| 'text'
| 'textarea'
| 'image'
| 'list'
| 'icon'
| 'style'
| 'number'
| 'boolean'
export type DesignerFieldValue = string | string[] | number | boolean
export interface DesignerField {
/**
* Duzenlenen icerik nesnesine giden nokta yolu.
* Ornek: `heroTitle`, `slides.0.title`, `slides.0.services.2.icon`
*/
key: string
label: string
type: DesignerFieldType
value: DesignerFieldValue
/** Panelde alanlari gruplamak icin kullanilir. */
group?: string
placeholder?: string
helpText?: string
rows?: number
}
export interface DesignerSelection {
id: string
title: string
description?: string
fields: DesignerField[]
}
export interface DesignerLanguageOption {
key: string
cultureName: string
displayName: string
}

View file

@ -1,65 +0,0 @@
import { useMemo } from 'react'
import { useStoreState } from '@/store'
import { normalizeLanguageKey } from './localization'
import { DesignerLanguageOption } from './types'
/**
* ABP yapilandirmasindaki dilleri designer paneli icin normalize eder.
* Dort public sayfada tekrarlanan dil hazirligi burada tek noktada toplanir.
*/
export function useDesignerLanguages() {
const configCultureName = useStoreState(
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
)
const localeCurrentLang = useStoreState((state) => state.locale?.currentLang)
const abpLanguages = useStoreState((state) => state.abpConfig.config?.localization.languages)
const currentLanguage = configCultureName || localeCurrentLang || 'tr'
const languageOptions = useMemo<DesignerLanguageOption[]>(
() =>
(abpLanguages ?? [])
.filter((language) => Boolean(language.cultureName))
.map((language) => {
const cultureName = language.cultureName as string
return {
key: normalizeLanguageKey(cultureName),
cultureName,
displayName: language.displayName || cultureName,
}
}),
[abpLanguages],
)
const editorLanguages = useMemo(
() =>
Array.from(
new Set(
(languageOptions.length > 0
? languageOptions.map((language) => language.key)
: [normalizeLanguageKey(currentLanguage)]
).filter(Boolean),
),
),
[currentLanguage, languageOptions],
)
return { currentLanguage, languageOptions, editorLanguages }
}
/** Panelde gosterilecek dil listesi; yapilandirma bos ise desteklenen dillerden uretilir. */
export function resolveDrawerLanguages(
languageOptions: DesignerLanguageOption[],
supportedLanguages: string[],
): DesignerLanguageOption[] {
if (languageOptions.length > 0) {
return languageOptions
}
return supportedLanguages.map((language) => ({
key: language,
cultureName: language,
displayName: language.toUpperCase(),
}))
}

View file

@ -1,272 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { normalizeLanguageKey } from './localization'
import { setByPath } from './paths'
const DESIGNER_QUERY_KEY = 'design'
const HISTORY_LIMIT = 50
interface UseDesignerStateOptions {
currentLanguage?: string
supportedLanguages?: string[]
/** Tasarim modunun acilabilmesi icin yetki kontrolu. Varsayilan: acik. */
enabled?: boolean
}
interface LanguageEntry<T> {
current: T
/** Sunucudan gelen son icerigin serilestirilmis hali; "kirli mi" karsilastirmasi icin. */
baseline: string
past: T[]
future: T[]
}
function serialize(value: unknown) {
try {
return JSON.stringify(value) ?? '__undefined__'
} catch {
return '__unserializable__'
}
}
function clone<T>(value: T): T {
try {
return JSON.parse(JSON.stringify(value)) as T
} catch {
return value
}
}
function createEntry<T>(content: T): LanguageEntry<T> {
return { current: clone(content), baseline: serialize(content), past: [], future: [] }
}
export function useDesignerState<T>(
pageKey: string,
initialContent: T | null,
options?: UseDesignerStateOptions,
) {
const location = useLocation()
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const isDesignerRequested =
searchParams.get(DESIGNER_QUERY_KEY) === '1' || location.pathname.endsWith('/designer')
const isDesignMode = isDesignerRequested && options?.enabled !== false
const currentLanguage = normalizeLanguageKey(options?.currentLanguage)
const supportedLanguages = useMemo(() => {
const base = (options?.supportedLanguages ?? [currentLanguage]).map(normalizeLanguageKey)
return Array.from(new Set(base.includes(currentLanguage) ? base : [currentLanguage, ...base]))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentLanguage, serialize(options?.supportedLanguages)])
const [entries, setEntries] = useState<Record<string, LanguageEntry<T>>>({})
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null)
const entriesRef = useRef(entries)
entriesRef.current = entries
const serializedInitial = serialize(initialContent)
// Sunucu icerigi degistiginde (ilk yukleme veya dil degisimi) o dilin taslagini tohumla.
// Kullanici o dilde duzenleme yaptiysa taslak korunur, uzerine yazilmaz.
useEffect(() => {
if (!initialContent) {
return
}
setEntries((previous) => {
const entry = previous[currentLanguage]
if (entry && serialize(entry.current) !== entry.baseline) {
return previous
}
if (entry && entry.baseline === serializedInitial) {
return previous
}
return { ...previous, [currentLanguage]: createEntry(initialContent) }
})
// initialContent yerine serilestirilmis hali izlenir: her render'da yeni referans gelse bile
// efekt yalnizca icerik gercekten degistiginde calisir.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentLanguage, serializedInitial])
useEffect(() => {
if (!isDesignMode) {
setSelectedBlockId(null)
}
}, [isDesignMode])
const entry = entries[currentLanguage]
const content = entry?.current ?? initialContent ?? null
const isDirty = Boolean(entry) && serialize(entry.current) !== entry.baseline
const mutate = useCallback(
(updater: (current: T) => T | null) => {
setEntries((previous) => {
const existing = previous[currentLanguage]
if (!existing) {
return previous
}
const next = updater(existing.current)
if (!next || serialize(next) === serialize(existing.current)) {
return previous
}
return {
...previous,
[currentLanguage]: {
...existing,
current: next,
past: [...existing.past, existing.current].slice(-HISTORY_LIMIT),
future: [],
},
}
})
},
[currentLanguage],
)
/** Nokta yolu ile tek bir alani gunceller. */
const setFieldValue = useCallback(
(path: string, value: unknown) => {
mutate((current) => setByPath(current, path, value))
},
[mutate],
)
const undo = useCallback(() => {
setEntries((previous) => {
const existing = previous[currentLanguage]
if (!existing?.past.length) {
return previous
}
const past = [...existing.past]
const restored = past.pop() as T
return {
...previous,
[currentLanguage]: {
...existing,
current: restored,
past,
future: [existing.current, ...existing.future].slice(0, HISTORY_LIMIT),
},
}
})
}, [currentLanguage])
const redo = useCallback(() => {
setEntries((previous) => {
const existing = previous[currentLanguage]
if (!existing?.future.length) {
return previous
}
const [restored, ...future] = existing.future
return {
...previous,
[currentLanguage]: {
...existing,
current: restored,
past: [...existing.past, existing.current].slice(-HISTORY_LIMIT),
future,
},
}
})
}, [currentLanguage])
/** Kaydedilmemis degisiklikleri atip sunucudan gelen son icerige doner. */
const resetContent = useCallback(() => {
setEntries((previous) => {
const existing = previous[currentLanguage]
if (!existing) {
return previous
}
return {
...previous,
[currentLanguage]: {
...existing,
current: JSON.parse(existing.baseline) as T,
past: [...existing.past, existing.current].slice(-HISTORY_LIMIT),
future: [],
},
}
})
}, [currentLanguage])
/** Kaydetme basarili olduktan sonra mevcut icerigi yeni referans nokta yapar. */
const commitContent = useCallback(() => {
setEntries((previous) => {
const existing = previous[currentLanguage]
if (!existing) {
return previous
}
return {
...previous,
[currentLanguage]: { ...existing, baseline: serialize(existing.current) },
}
})
}, [currentLanguage])
const exitDesignMode = useCallback(() => {
// Tasarim modu iki sekilde acilabiliyor: `?design=1` veya `/<sayfa>/designer` yolu.
if (location.pathname.endsWith('/designer')) {
navigate(location.pathname.replace(/\/designer$/, '') || '/', { replace: true })
return
}
const next = new URLSearchParams(searchParams)
next.delete(DESIGNER_QUERY_KEY)
setSearchParams(next, { replace: true })
}, [location.pathname, navigate, searchParams, setSearchParams])
// Tasarim modunda sekme kapatilirken kaydedilmemis degisiklikler icin uyari.
useEffect(() => {
if (!isDesignMode || !isDirty) {
return
}
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault()
event.returnValue = ''
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [isDesignMode, isDirty])
return {
pageKey,
content,
isDesignMode,
isDirty,
canUndo: Boolean(entry?.past.length),
canRedo: Boolean(entry?.future.length),
selectedBlockId,
selectedLanguage: currentLanguage,
supportedLanguages,
setContent: mutate,
setFieldValue,
setSelectedBlockId,
resetContent,
commitContent,
exitDesignMode,
undo,
redo,
}
}

View file

@ -1,188 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
import { Notification, toast } from '@/components/ui'
import { useStoreActions, useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { translateLabel } from './localization'
import { resolveDrawerLanguages, useDesignerLanguages } from './useDesignerLanguages'
import { useDesignerState } from './useDesignerState'
import { DesignerFieldValue } from './types'
interface UsePageDesignerOptions<T> {
/** Tasarim panelinde gosterilecek sayfa adi ve state anahtari. */
pageKey: string
/**
* Tasarim modunu acmak icin gereken ABP yetkisi (orn. `App.Home.Design`).
* Yetki `PermissionsData.json` icinde tanimlanir ve seeder ile yonetilir.
*/
permission: string
/** Sunucudan uretilen icerik. Referans kararliligi icin `useMemo` ile hesaplanmalidir. */
initialContent: T | null
/** Kaydetme cagrisi. Basarili donerse icerik yeni referans nokta olarak isaretlenir. */
onSave: (content: T, cultureName: string) => Promise<unknown>
}
/**
* Public sayfalarin tasarim modu icin ortak davranis:
* dil listesi, panel gorunurlugu, secim, kaydetme ve klavye kisayollari.
*/
export function usePageDesigner<T>({
pageKey,
permission,
initialContent,
onSave,
}: UsePageDesignerOptions<T>) {
const { translate } = useLocalization()
const { setLang } = useStoreActions((actions) => actions.locale)
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
const isAuthenticated = useStoreState(
(state) => state.abpConfig.config?.currentUser?.isAuthenticated,
)
const { checkPermission } = usePermission()
const { currentLanguage, languageOptions, editorLanguages } = useDesignerLanguages()
// Tasarim modu yalnizca oturum acmis ve ilgili sayfa yetkisi verilmis kullanicilar icin
// acilir; anonim ziyaretci ya da yetkisiz kullanici adres cubugundan `?design=1`
// ekleyerek paneli acamaz. Asil kontrol sunucuda `PublicAppService.Save*PageAsync`
// uzerindeki `[Authorize]` ile yapilir.
const canDesign = Boolean(isAuthenticated) && checkPermission(permission)
const designer = useDesignerState<T>(pageKey, initialContent, {
currentLanguage,
supportedLanguages: editorLanguages,
enabled: canDesign,
})
const [isPanelVisible, setIsPanelVisible] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const {
content,
isDesignMode,
selectedLanguage,
setFieldValue,
setSelectedBlockId,
commitContent,
undo,
redo,
} = designer
const selectBlock = useCallback(
(blockId: string) => {
setSelectedBlockId(blockId)
setIsPanelVisible(true)
},
[setSelectedBlockId],
)
const changeLanguage = useCallback(
(cultureName: string) => {
// Global locale degisimi cevirileri yeniden yukler; designer dili store'u takip eder.
setLang(cultureName)
},
[setLang],
)
const changeField = useCallback(
(path: string, value: DesignerFieldValue) => setFieldValue(path, value),
[setFieldValue],
)
const save = useCallback(async () => {
if (!content || isSaving || !canDesign) {
return
}
setIsSaving(true)
try {
await onSave(content, selectedLanguage)
await getConfig(false)
commitContent()
setSelectedBlockId(null)
toast.push(
<Notification type="success" duration={2000}>
{translate('::App.Platform.FormBilgileriKaydedildi')}
</Notification>,
{ placement: 'bottom-end' },
)
} catch (error) {
console.error(`${pageKey} tasarimi kaydedilemedi:`, error)
toast.push(
<Notification type="danger" duration={4000}>
{translateLabel(translate, 'Public.designer.saveFailed')}
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setIsSaving(false)
}
}, [
canDesign,
commitContent,
content,
getConfig,
isSaving,
onSave,
pageKey,
selectedLanguage,
setSelectedBlockId,
translate,
])
// Tasarim modu kisayollari: Ctrl+S kaydet, Ctrl+Z geri al, Ctrl+Shift+Z ileri al, Esc secimi birak.
useEffect(() => {
if (!isDesignMode) {
return
}
const handleKeyDown = (event: KeyboardEvent) => {
const isEditingText =
event.target instanceof HTMLElement &&
['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName)
const key = event.key.toLowerCase()
if ((event.ctrlKey || event.metaKey) && key === 's') {
event.preventDefault()
void save()
return
}
if ((event.ctrlKey || event.metaKey) && key === 'z') {
event.preventDefault()
if (event.shiftKey) {
redo()
} else {
undo()
}
return
}
if (event.key === 'Escape' && !isEditingText) {
setSelectedBlockId(null)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isDesignMode, redo, save, setSelectedBlockId, undo])
return {
...designer,
isSaving,
isPanelVisible,
setIsPanelVisible,
selectBlock,
changeLanguage,
changeField,
save,
translate,
drawerLanguages: resolveDrawerLanguages(languageOptions, designer.supportedLanguages),
}
}

View file

@ -0,0 +1,13 @@
/**
* Dil anahtari tanimli degilse okunabilir bir yedek metin dondurur; ceviri bulunamadiginda
* ekranda ham anahtarin gorunmesini engeller.
*/
export function translateLabel(
translate: (key: string) => string,
key: string,
fallback?: string,
) {
const value = translate('::' + key)
return value === key ? (fallback ?? key) : value
}

View file

@ -1,20 +0,0 @@
/** Public sayfalarda kart ikonlarinin donusumlu renk paleti. */
const ICON_COLOR_CLASSES = [
'text-blue-600',
'text-red-600',
'text-green-600',
'text-purple-600',
'text-yellow-600',
'text-indigo-600',
]
export function getIconColor(index: number) {
return ICON_COLOR_CLASSES[index % ICON_COLOR_CLASSES.length]
}
/** Tasarim paneli acikken icerigin panel altinda kalmamasi icin sag bosluk sinifi. */
export function designerPageClass(isDesignMode: boolean, isPanelVisible: boolean) {
return `min-h-screen bg-gray-50 dark:bg-gray-950 ${
isDesignMode && isPanelVisible ? 'xl:pr-[420px]' : ''
}`
}

View file

@ -6,7 +6,18 @@ const safeListFile = 'safelist.txt'
const SAFELIST_COLORS = 'colors' const SAFELIST_COLORS = 'colors'
export default { export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', './safelist.txt'], content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
'./safelist.txt',
// Custom Component'lerin kodu veritabaninda yasar ve derlemede taranmaz; kullandiklari
// siniflar yalnizca seed dosyasinda gorunur. Dosya tarandigi icin public sayfalarin
// Tailwind siniflari (gradient, opacity, /alpha tonlari) CSS'e giriyor.
// Ayni siniflar `safelist.txt` dosyasina da yazilir (twSafelistGenerator): derleme baglami depo
// kokunu gormedigin de — docker, ayri checkout — bu glob bos doner, safelist ise `ui/` icinde
// oldugu icin her ortamda taranir.
'../configs/seeds/*/data/App.DeveloperKit.CustomComponents.json',
],
safelist: [ safelist: [
{ {
// Covers most utility groups used by designer-entered class strings. // Covers most utility groups used by designer-entered class strings.

View file

@ -1,17 +1,112 @@
const fs = require('fs') const fs = require('fs')
const nodePath = require('path')
const plugin = require('tailwindcss/plugin') const plugin = require('tailwindcss/plugin')
const generator = require('./generator') const generator = require('./generator')
const crypto = require('crypto') const crypto = require('crypto')
module.exports = plugin.withOptions(({ path = 'safelist.txt', patterns = [] }) => ({ theme }) => { /**
* Uretilen renk/olcu listesiyle Custom Component siniflarini ayiran isaret. Bilesen siniflari
* dosyanin sonunda yasar; kaynak seed dosyasi bulunamadiginda onceki liste oldugu gibi korunur.
*/
const COMPONENT_MARKER = '# --- custom component classes ---'
const safeList = generator(theme)(patterns).join('\n') /** Seed kokunun `ui/` klasorune gore konumu; calisma dizininden bagimsiz cozulur. */
const currentSafeList = fs.readFileSync(path).toString() const SEEDS_ROOT = nodePath.resolve(__dirname, '../../configs/seeds')
const addTokens = (value, classes) =>
String(value)
.split(/\s+/)
.filter(Boolean)
.forEach((token) => classes.add(token))
const collectFromNodes = (nodes, classes) => {
if (!Array.isArray(nodes)) return
for (const node of nodes) {
for (const [name, value] of Object.entries((node && node.props) || {})) {
if (typeof value === 'string' && /class(Name)?$/i.test(name)) addTokens(value, classes)
}
collectFromNodes(node && node.children, classes)
}
}
const collectFromRow = (row, classes) => {
try {
const props = row && row.Props ? JSON.parse(row.Props) : null
collectFromNodes(props && props.visualDesigner && props.visualDesigner.nodes, classes)
} catch {
// Tasarimci belgesi olmayan (elle yazilmis) bilesen; siniflari asagida kodundan gelir.
}
const code = String((row && row.Code) || '')
const pattern = /className=\{?["'`]([^"'`]+)["'`]/g
let match = pattern.exec(code)
while (match) {
addTokens(match[1], classes)
match = pattern.exec(code)
}
}
/**
* Custom Component'lerin kullandigi siniflar. Bilesen kodu veritabaninda yasar; derlemede
* taranabilen tek kaynak seed dosyasidir ve o dosya `ui/` disindadir. Derleme baglami depo kokunu
* gormedigin de (docker, ayri checkout) seed'e ozgu siniflar CSS'e hic girmez ve uygulama sunucuda
* stilsiz kalir. Siniflar safelist'e yazilir; dosya depoya islendigi icin her ortamda taranir.
*/
const collectComponentClasses = () => {
if (!fs.existsSync(SEEDS_ROOT)) return []
const files = fs
.readdirSync(SEEDS_ROOT, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) =>
nodePath.join(SEEDS_ROOT, entry.name, 'data/App.DeveloperKit.CustomComponents.json'),
)
.filter((file) => fs.existsSync(file))
const classes = new Set()
for (const file of files) {
try {
const seed = JSON.parse(fs.readFileSync(file, 'utf8'))
for (const row of seed.Rows || []) collectFromRow(row, classes)
} catch {
// Bozuk bir seed dosyasi derlemeyi durdurmaz; diger kapsamlar islenir.
}
}
return [...classes].sort()
}
module.exports = plugin.withOptions(
({ path = 'safelist.txt', patterns = [] }) =>
({ theme }) => {
// Yol calisma dizinine gore cozulurse depo kokunden calistirilan her arac ikinci bir
// safelist dosyasi yaratir; eklentinin konumuna sabitlenir, tek dosya kalir.
const safeListPath = nodePath.isAbsolute(path)
? path
: nodePath.resolve(__dirname, '..', path)
const currentSafeList = fs.existsSync(safeListPath)
? fs.readFileSync(safeListPath).toString()
: ''
const previousComponents = currentSafeList.split(COMPONENT_MARKER)[1] || ''
const seedClasses = collectComponentClasses()
const components = seedClasses.length
? seedClasses
: previousComponents.split('\n').filter(Boolean)
const safeList = [...generator(theme)(patterns), COMPONENT_MARKER, ...components].join(
'\n',
)
const hash = crypto.createHash('md5').update(JSON.stringify(safeList)).digest('hex') const hash = crypto.createHash('md5').update(JSON.stringify(safeList)).digest('hex')
const prevHash = crypto.createHash('md5').update(JSON.stringify(currentSafeList)).digest('hex') const prevHash = crypto
.createHash('md5')
.update(JSON.stringify(currentSafeList))
.digest('hex')
if (hash !== prevHash) { if (hash !== prevHash) {
return fs.writeFileSync(path, safeList) return fs.writeFileSync(safeListPath, safeList)
} }
}) },
)