Public sayfalar Custom componente taşındı

This commit is contained in:
Sedat ÖZTÜRK 2026-09-08 17:00:15 +03:00
parent ad00e75911
commit ea0de1e611
91 changed files with 1853 additions and 8079 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,13 +7781,16 @@ 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,
IsActive = true, IsActive = true,
AllowSearch = true, AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson, ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name), PermissionJson = DefaultFieldPermissionJson(listForm.Name),

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

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

@ -4495,4 +4495,293 @@ mr-3.5
mr-1.2 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 */} <main className="flex-grow">
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1"> <View />
{navLinks </main>
.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"> {isComponentRegistered(PUBLIC_FOOTER_COMPONENT) && renderComponent(PUBLIC_FOOTER_COMPONENT)}
{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 */} <Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} />
<Button </div>
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">
<View />
</main>
<footer className="bg-gray-900 text-white pt-16 pb-8">
<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)} />
</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,336 @@
/**
* 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, 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()
return (
<MobileMenuContext.Provider value={() => setOpen(false)}>
<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)}
/>
</div>
{open && <div className={panelClassName}>{children}</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,
@ -1289,6 +1290,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,6 +1442,88 @@ 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, index,
@ -1483,6 +1578,11 @@ 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 canMoveUp = !isRoot || index > 0
@ -1983,7 +2083,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 +2103,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) => {
@ -2163,7 +2269,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 +2284,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' &&
@ -2277,9 +2387,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) => {
@ -2308,7 +2420,7 @@ const VisualCanvas = ({
</div> </div>
)} )}
{nodes.length ? ( {nodes.length ? (
<div className="space-y-4"> <div className={canvasWidth === 'responsive' ? 'space-y-4' : ''}>
{nodes.map((node, index) => ( {nodes.map((node, index) => (
<NodeView <NodeView
key={node.id} key={node.id}

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,22 +133,24 @@ 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
key: route.path, .filter((route) => route.componentType === 'dynamic' || hasPhysicalComponent(route.componentPath))
path: route.path, .map((route) => ({
getComponent: (registeredComponents, renderComponent, isComponentRegistered) => key: route.path,
loadComponent( path: route.path,
route.componentType, getComponent: (registeredComponents, renderComponent, isComponentRegistered) =>
route.componentPath, loadComponent(
registeredComponents, route.componentType,
renderComponent, route.componentPath,
isComponentRegistered, registeredComponents,
), renderComponent,
routeType: route.routeType, isComponentRegistered,
authority: route.authority, ),
componentType: route.componentType, routeType: route.routeType,
componentPath: route.componentPath, authority: route.authority,
})) componentType: route.componentType,
componentPath: route.componentPath,
}))
} }
// Custom components are the single source of truth for runtime routes. // Custom components are the single source of truth for runtime routes.

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

@ -3,13 +3,10 @@
public: { public: {
home: '/home', home: '/home',
about: '/about', 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', blogDetail: '/blog/:id',
demo: '/demo', demo: '/demo',

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

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

@ -41,6 +41,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 +845,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 +1491,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 +1504,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 = []
@ -2804,10 +2821,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])
@ -5372,7 +5393,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,21 +5427,25 @@ const VisualComponentDesigner = () => {
> >
{translate('::App.Platform.Reset')} {translate('::App.Platform.Reset')}
</Button> </Button>
<Button {/* Rotasi olmayan bilesen (baska bir ekranin icinde cizilen alt bilgi gibi)
color="green-600" tek basina acilamaz; onizleme dugmesi yalnizca rotasi olanda cikar. */}
icon={<FaEye />} {component.routePath?.trim() ? (
size="sm" <Button
title={translate('::App.DeveloperKitComponentDesigner.OpenInNewTab')} color="green-600"
variant="solid" icon={<FaEye />}
onClick={() => { size="sm"
const routePath = component.routePath.startsWith('/') title={translate('::App.DeveloperKitComponentDesigner.OpenInNewTab')}
? component.routePath variant="solid"
: `/${component.routePath}` onClick={() => {
window.open(routePath, '_blank') const routePath = component.routePath.startsWith('/')
}} ? component.routePath
> : `/${component.routePath}`
{translate('::App.Listform.ListformField.Preview')} window.open(routePath, '_blank')
</Button> }}
>
{translate('::App.Listform.ListformField.Preview')}
</Button>
) : null}
<Button <Button
loading={isSaving} loading={isSaving}
icon={<FaRegSave />} icon={<FaRegSave />}
@ -5602,7 +5627,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">

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 hash = crypto.createHash('md5').update(JSON.stringify(safeList)).digest('hex') const addTokens = (value, classes) =>
const prevHash = crypto.createHash('md5').update(JSON.stringify(currentSafeList)).digest('hex') String(value)
.split(/\s+/)
.filter(Boolean)
.forEach((token) => classes.add(token))
if (hash !== prevHash) { const collectFromNodes = (nodes, classes) => {
return fs.writeFileSync(path, safeList) 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 prevHash = crypto
.createHash('md5')
.update(JSON.stringify(currentSafeList))
.digest('hex')
if (hash !== prevHash) {
return fs.writeFileSync(safeListPath, safeList)
}
},
)