Public sayfalar Custom componente taşındı
This commit is contained in:
parent
ad00e75911
commit
6742393271
94 changed files with 1959 additions and 8263 deletions
119
README.md
119
README.md
|
|
@ -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. |
|
||||
| **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. |
|
||||
| **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. |
|
||||
|
||||
---
|
||||
|
|
@ -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
|
||||
ç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`
|
||||
`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
|
||||
|
|
@ -875,6 +890,9 @@ Route.Authority ──┘ └► Permission
|
|||
- Frontend'de `dynamicRoutesContext` route'ları çeker, `dynamicRouteLoader` bunları fiziksel
|
||||
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.
|
||||
- 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
|
||||
`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.SqlQueryManager` | `.CrudEndpoints` |
|
||||
| `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.
|
||||
|
||||
|
|
@ -943,9 +960,101 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
|
|||
|
||||
### 10.4. Public site
|
||||
|
||||
`/home` · `/about` · `/services` · `/products` · `/checkout` · `/payment` · `/success` ·
|
||||
`/blog` · `/blog/:id` · `/demo` · `/contact` — içerikler veritabanından yönetilir;
|
||||
`/about/designer` ve `/services/designer` sayfa tasarımcılarıdır.
|
||||
`/home` · `/about` · `/contact` (Custom Component) · `/products` · `/checkout` · `/payment` ·
|
||||
`/success` · `/blog` · `/blog/:id` · `/demo` (fiziksel sayfalar).
|
||||
|
||||
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`
|
||||
açıklama anahtarı.
|
||||
|
||||
Elle yazılmış bir bileşen Visual Designer'da **kod modunda** açı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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ namespace Sozsoft.Platform.DeveloperKit;
|
|||
public class CustomComponentDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
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? Props { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
|
@ -24,10 +25,13 @@ public class CustomComponentDto : FullAuditedEntityDto<Guid>
|
|||
public class CreateUpdateCustomComponentDto
|
||||
{
|
||||
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)]
|
||||
[RegularExpression(@"^/.*", ErrorMessage = "RoutePath must start with '/'.")]
|
||||
public string RoutePath { get; set; } = string.Empty;
|
||||
[RegularExpression(@"^(/.*)?$", ErrorMessage = "RoutePath must start with '/'.")]
|
||||
public string? RoutePath { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string? Props { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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); }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +66,8 @@ public class CustomComponentAppService : CrudAppService<
|
|||
|
||||
public override async Task<CustomComponentDto> CreateAsync(CreateUpdateCustomComponentDto input)
|
||||
{
|
||||
NormalizeRoutePath(input);
|
||||
|
||||
var result = await base.CreateAsync(input);
|
||||
await SyncDataSourcesAsync(result.Id);
|
||||
await _seedDataSynchronizer.SyncRowAsync(ListFormCode, result.Id, SeedSyncOperation.Insert);
|
||||
|
|
@ -76,6 +78,8 @@ public class CustomComponentAppService : CrudAppService<
|
|||
Guid id,
|
||||
CreateUpdateCustomComponentDto input)
|
||||
{
|
||||
NormalizeRoutePath(input);
|
||||
|
||||
var existingComponent = await Repository.GetAsync(id);
|
||||
|
||||
if (input.Props is null)
|
||||
|
|
@ -90,6 +94,16 @@ public class CustomComponentAppService : CrudAppService<
|
|||
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)
|
||||
{
|
||||
await base.DeleteAsync(id);
|
||||
|
|
|
|||
|
|
@ -277,7 +277,10 @@ public class MenuAppService : CrudAppService<
|
|||
await CheckGetPolicyAsync();
|
||||
|
||||
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 entityDtos = await MapToGetListOutputDtosAsync(entities);
|
||||
|
|
|
|||
|
|
@ -21,13 +21,11 @@ using static Sozsoft.Platform.Data.Seeds.SeedConsts;
|
|||
namespace Sozsoft.Platform.Public;
|
||||
|
||||
/// <summary>
|
||||
/// Public site uc noktalari. Okuma metotlari anonim erisime aciktir; sayfa tasarimini
|
||||
/// degistiren <c>Save*PageAsync</c> metotlari <see cref="AppCodes.WebSiteDesign"/>
|
||||
/// yetkileriyle korunur (yetkiler PermissionsData.json icinde tanimli, seeder ile yonetilir).
|
||||
/// Public site uc noktalari: demo talebi, urun/odeme listeleri, siparis olusturma ve
|
||||
/// ulke/il/ilce aramalari. Tamami anonim erisime aciktir.
|
||||
/// </summary>
|
||||
public class PublicAppService : PlatformAppService
|
||||
{
|
||||
private readonly IRepository<Service, Guid> _serviceRepository;
|
||||
private readonly ISettingProvider _settingProvider;
|
||||
private readonly ISozsoftEmailSender _emailSender;
|
||||
private readonly IRepository<Demo, Guid> _demoRepository;
|
||||
|
|
@ -37,18 +35,11 @@ public class PublicAppService : PlatformAppService
|
|||
private readonly IRepository<PaymentMethod, Guid> _paymentMethodRepository;
|
||||
private readonly IRepository<InstallmentOption> _installmentOptionRepository;
|
||||
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<City, Guid> _cityRepository;
|
||||
private readonly IRepository<District, Guid> _districtRepository;
|
||||
private readonly IRepository<LanguageKey, Guid> _languageKeyRepository;
|
||||
private readonly IRepository<LanguageText, Guid> _languageTextRepository;
|
||||
private readonly LanguageTextAppService _languageTextAppService;
|
||||
|
||||
public PublicAppService(
|
||||
IRepository<Service, Guid> serviceRepository,
|
||||
ISettingProvider settingProvider,
|
||||
ISozsoftEmailSender emailSender,
|
||||
IRepository<Demo, Guid> demoRepository,
|
||||
|
|
@ -58,18 +49,11 @@ public class PublicAppService : PlatformAppService
|
|||
IRepository<PaymentMethod, Guid> paymentMethodRepository,
|
||||
IRepository<InstallmentOption> installmentOptionRepository,
|
||||
IRepository<Order, Guid> orderRepository,
|
||||
IRepository<About, Guid> aboutRepository,
|
||||
IRepository<Home, Guid> homeRepository,
|
||||
IRepository<Contact, Guid> contactRepository,
|
||||
IRepository<Country, string> countryRepository,
|
||||
IRepository<City, Guid> cityRepository,
|
||||
IRepository<District, Guid> districtRepository,
|
||||
IRepository<LanguageKey, Guid> languageKeyRepository,
|
||||
IRepository<LanguageText, Guid> languageTextRepository,
|
||||
LanguageTextAppService languageTextAppService
|
||||
IRepository<District, Guid> districtRepository
|
||||
)
|
||||
{
|
||||
_serviceRepository = serviceRepository;
|
||||
_settingProvider = settingProvider;
|
||||
_emailSender = emailSender;
|
||||
_demoRepository = demoRepository;
|
||||
|
|
@ -79,235 +63,11 @@ public class PublicAppService : PlatformAppService
|
|||
_paymentMethodRepository = paymentMethodRepository;
|
||||
_installmentOptionRepository = installmentOptionRepository;
|
||||
_orderRepository = orderRepository;
|
||||
_aboutRepository = aboutRepository;
|
||||
_homeRepository = homeRepository;
|
||||
_contactRepository = contactRepository;
|
||||
_languageKeyRepository = languageKeyRepository;
|
||||
_languageTextRepository = languageTextRepository;
|
||||
_languageTextAppService = languageTextAppService;
|
||||
_countryRepository = countryRepository;
|
||||
_cityRepository = cityRepository;
|
||||
_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)
|
||||
{
|
||||
var demo = ObjectMapper.Map<DemoDto, Demo>(input);
|
||||
|
|
@ -565,7 +325,6 @@ public class PublicAppService : PlatformAppService
|
|||
Categories = categoryDtos
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<BlogPostDto> GetPostAsync(Guid id)
|
||||
{
|
||||
// Tek sorguda post ve category'yi çek (N+1 önleme)
|
||||
|
|
@ -586,7 +345,6 @@ public class PublicAppService : PlatformAppService
|
|||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<BlogPostDto> GetPostBySlugAsync(string slug)
|
||||
{
|
||||
var post = await _postRepository.FirstOrDefaultAsync(x => x.Slug == slug);
|
||||
|
|
@ -1043,19 +801,6 @@ public class PublicAppService : PlatformAppService
|
|||
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()
|
||||
{
|
||||
var queryable = await _countryRepository.GetQueryableAsync();
|
||||
|
|
@ -1128,121 +873,6 @@ public class PublicAppService : PlatformAppService
|
|||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
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)]
|
||||
public partial class CountryToCountryDtoMapper : MapperBase<Country, CountryDto>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1194,6 +1194,12 @@
|
|||
"en": "Web Site",
|
||||
"tr": "Web Site"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Public.Header",
|
||||
"en": "Site Menu",
|
||||
"tr": "Site Menüsü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.IdentityManagement.ClaimType",
|
||||
|
|
@ -7266,6 +7272,12 @@
|
|||
"tr": "Vergi Dairesi",
|
||||
"en": "Tax Office"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.PublicContact.WorkHours",
|
||||
"en": "Working Hours",
|
||||
"tr": "Çalışma Saatleri"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.PublicContact.TransferForm",
|
||||
|
|
@ -8934,90 +8946,6 @@
|
|||
"tr": "Müşteri Yorumları",
|
||||
"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",
|
||||
"key": "App.Listform.ListformField.DataSources",
|
||||
|
|
@ -24078,12 +24006,6 @@
|
|||
"en": "Blog post not found.",
|
||||
"tr": "Blog yazısı bulunamadı."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.PublicDesigner.IconPlaceholder",
|
||||
"en": "Example: {icon}",
|
||||
"tr": "Örnek: {icon}"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.PublicPayment.CreateOrderError",
|
||||
|
|
@ -24240,6 +24162,60 @@
|
|||
"en": "Date picker component",
|
||||
"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",
|
||||
"key": "App.ComponentDiv.DivDescription",
|
||||
|
|
|
|||
|
|
@ -7726,10 +7726,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
IsActive = 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[] {
|
||||
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,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
|
|
@ -7781,13 +7781,16 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Dependencies",
|
||||
EditorType2 = EditorTypes.dxTextBox,
|
||||
CaptionName = "App.Platform.Dependencies",
|
||||
EditGroupOrderNo = 1,
|
||||
EditOrderNo = 5,
|
||||
EditorType2 = EditorTypes.dxTextBox,
|
||||
ColSpan = 1,
|
||||
Width = 0,
|
||||
ListOrderNo = 6,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
|
||||
|
||||
AllowSearch = true,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public class MenuSeedDto
|
|||
public int Order { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public string CssClass { get; set; }
|
||||
public string RequiredPermissionName { get; set; }
|
||||
public bool IsDisabled { get; set; }
|
||||
public string ModuleId { get; set; }
|
||||
|
|
@ -120,6 +121,7 @@ public class MenuDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
Order = item.Order,
|
||||
Url = item.Url,
|
||||
Icon = item.Icon,
|
||||
CssClass = item.CssClass,
|
||||
RequiredPermissionName = item.RequiredPermissionName,
|
||||
IsDisabled = item.IsDisabled,
|
||||
ModuleId = item.ModuleId
|
||||
|
|
|
|||
|
|
@ -1,21 +1,5 @@
|
|||
{
|
||||
"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",
|
||||
"path": "/products",
|
||||
|
|
@ -48,14 +32,6 @@
|
|||
"routeType": "public",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "services",
|
||||
"path": "/services",
|
||||
"componentType": "normal",
|
||||
"componentPath": "@/views/public/Services",
|
||||
"routeType": "public",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "blog",
|
||||
"path": "/blog",
|
||||
|
|
@ -80,14 +56,6 @@
|
|||
"routeType": "public",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "contact",
|
||||
"path": "/contact",
|
||||
"componentType": "normal",
|
||||
"componentPath": "@/views/public/Contact",
|
||||
"routeType": "public",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "access-denied",
|
||||
"path": "/access-denied",
|
||||
|
|
@ -370,38 +338,6 @@
|
|||
"routeType": "protected",
|
||||
"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",
|
||||
"path": "/admin/videoroom/dashboard",
|
||||
|
|
@ -433,6 +369,9 @@
|
|||
},
|
||||
{
|
||||
"Name": "Adm"
|
||||
},
|
||||
{
|
||||
"Name": "Pub"
|
||||
}
|
||||
],
|
||||
"Menus": [
|
||||
|
|
@ -590,39 +529,6 @@
|
|||
"IsDisabled": false,
|
||||
"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",
|
||||
"Code": "App.Orders.Products",
|
||||
|
|
@ -700,17 +606,6 @@
|
|||
"IsDisabled": false,
|
||||
"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",
|
||||
"Code": "App.DevRequests",
|
||||
|
|
@ -1470,6 +1365,85 @@
|
|||
"RequiredPermissionName": "App.ForumManagement.Publish",
|
||||
"IsDisabled": false,
|
||||
"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"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -811,60 +811,6 @@
|
|||
"MultiTenancySide": 2,
|
||||
"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",
|
||||
"Name": "App.Orders.Products",
|
||||
|
|
@ -1315,24 +1261,6 @@
|
|||
"MultiTenancySide": 2,
|
||||
"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",
|
||||
"Name": "App.DevRequests",
|
||||
|
|
|
|||
|
|
@ -55,9 +55,6 @@ public enum TableNameEnum
|
|||
UomCategory,
|
||||
Uom,
|
||||
WorkHour,
|
||||
About,
|
||||
Home,
|
||||
Service,
|
||||
Product,
|
||||
PaymentMethod,
|
||||
InstallmentOption,
|
||||
|
|
@ -67,7 +64,6 @@ public enum TableNameEnum
|
|||
BlogCategory,
|
||||
BlogPost,
|
||||
Demo,
|
||||
Contact,
|
||||
Announcement,
|
||||
AnnouncementComment,
|
||||
AnnouncementLike,
|
||||
|
|
|
|||
|
|
@ -522,10 +522,6 @@ public static class PlatformConsts
|
|||
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 const string Default = Prefix.App + ".Orders";
|
||||
|
|
@ -544,19 +540,6 @@ public static class PlatformConsts
|
|||
public const string BlogCategory = Default + ".Category";
|
||||
}
|
||||
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
|
||||
public const string Administration = Prefix.App + ".Administration";
|
||||
|
|
@ -774,6 +757,23 @@ public static class PlatformConsts
|
|||
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 const string EmailConfirmation = "Account.EmailConfirmation";
|
||||
|
|
|
|||
|
|
@ -52,9 +52,6 @@ public static class TableNameResolver
|
|||
{ nameof(TableNameEnum.ForumCategory), (TablePrefix.TenantByName, MenuPrefix.Sas) },
|
||||
{ nameof(TableNameEnum.ForumTopic), (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.PaymentMethod), (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.BlogPost), (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.Module), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
|
||||
{ nameof(TableNameEnum.Menu), (TablePrefix.PlatformByName, MenuPrefix.Sas) },
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Sozsoft.Platform.Data;
|
|||
public interface IAfterAllMigrationsSqlExecutor
|
||||
{
|
||||
/// <param name="tenantId">
|
||||
/// null ise HostData/ExecuteData, aksi halde TenantData/ExecuteData klasörü işlenir.
|
||||
/// null ise host/, aksi halde tenants/{tenantId}/ kapsamındaki {sql|postgres}/execute klasörü işlenir.
|
||||
/// </param>
|
||||
Task ExecuteAsync(Guid? tenantId = null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,10 +422,6 @@ public static class SeedConsts
|
|||
public const string Licence = Prefix.App + ".Licence";
|
||||
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 const string Default = Prefix.App + ".Orders";
|
||||
|
|
@ -444,19 +440,6 @@ public static class SeedConsts
|
|||
public const string BlogCategory = Default + ".Category";
|
||||
}
|
||||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ namespace Sozsoft.Platform.Data.Seeds;
|
|||
/// <summary>
|
||||
/// Runtime'da duzenlenebilen seed dosyalarinin klasorunu cozer.
|
||||
/// <para>
|
||||
/// Kod ile birlikte versiyonlanan sabit seed'ler (ornegin TenantDataSeeder'in okudugu
|
||||
/// TenantData.json) buraya dahil degildir; onlar kendi projelerinin ciktisinda kalir.
|
||||
/// Kod ile birlikte versiyonlanan sabit seed'ler (DbMigrator projesindeki *Data.json dosyalari)
|
||||
/// buraya dahil degildir; onlar kendi projelerinin ciktisinda kalir.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Klasor bilincli olarak hicbir projenin icinde degildir: depoda <c>configs/seeds</c> altinda durur ve
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ namespace Sozsoft.Platform.Entities;
|
|||
public class CustomComponent : FullAuditedEntity<Guid>
|
||||
{
|
||||
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? Props { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
|
@ -21,7 +22,7 @@ public class CustomComponent : FullAuditedEntity<Guid>
|
|||
/// </summary>
|
||||
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;
|
||||
RoutePath = routePath;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -89,9 +89,6 @@ public class PlatformDbContext :
|
|||
public DbSet<IdentityLinkUser> LinkUsers { get; set; }
|
||||
public DbSet<IdentityUserDelegation> UserDelegations { 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<PaymentMethod> PaymentMethods { get; set; }
|
||||
public DbSet<InstallmentOption> InstallmentOptions { get; set; }
|
||||
|
|
@ -100,7 +97,6 @@ public class PlatformDbContext :
|
|||
public DbSet<BlogPost> BlogPosts { get; set; }
|
||||
public DbSet<BlogCategory> BlogCategories { get; set; }
|
||||
public DbSet<Demo> Demos { get; set; }
|
||||
public DbSet<Contact> Contacts { get; set; }
|
||||
public DbSet<Sector> Sectors { get; set; }
|
||||
public DbSet<SkillType> SkillTypes { get; set; }
|
||||
public DbSet<DevRequest> DevRequests { get; set; }
|
||||
|
|
@ -678,10 +674,10 @@ public class PlatformDbContext :
|
|||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
b.Property(x => x.RoutePath).IsRequired().HasMaxLength(512);
|
||||
b.Property(x => x.Code).IsRequired();
|
||||
// The visual designer document contains user-facing text. Keep it Unicode so
|
||||
// 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.Description).HasMaxLength(512);
|
||||
b.Property(x => x.Dependencies).HasMaxLength(2048);
|
||||
|
|
@ -689,7 +685,9 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.DataSources).HasColumnType("nvarchar(max)");
|
||||
|
||||
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 =>
|
||||
|
|
@ -957,64 +955,6 @@ public class PlatformDbContext :
|
|||
.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 =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Product)), Prefix.DbSchema);
|
||||
|
|
@ -1165,21 +1105,6 @@ public class PlatformDbContext :
|
|||
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 =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.LogEntry)), Prefix.DbSchema);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260904084415_Initial")]
|
||||
[Migration("20260908125150_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -630,55 +630,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1505,74 +1456,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1962,7 +1845,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoutePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
|
|
@ -1974,7 +1856,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasIndex("RoutePath")
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
.HasFilter("[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
|
||||
|
||||
b.ToTable("Sas_H_CustomComponent", (string)null);
|
||||
});
|
||||
|
|
@ -2957,95 +2839,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -5087,70 +4880,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
@ -922,27 +922,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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(
|
||||
name: "Sas_H_AiBot",
|
||||
columns: table => new
|
||||
|
|
@ -1059,32 +1038,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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(
|
||||
name: "Sas_H_ContactTitle",
|
||||
columns: table => new
|
||||
|
|
@ -1178,7 +1131,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", 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),
|
||||
Props = table.Column<string>(type: "nvarchar(max)", 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);
|
||||
});
|
||||
|
||||
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(
|
||||
name: "Sas_H_InstallmentOption",
|
||||
columns: table => new
|
||||
|
|
@ -1725,29 +1647,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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(
|
||||
name: "Sas_H_SettingDefinition",
|
||||
columns: table => new
|
||||
|
|
@ -4167,7 +4066,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
table: "Sas_H_CustomComponent",
|
||||
column: "RoutePath",
|
||||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
filter: "[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_CustomEndpoint_Name",
|
||||
|
|
@ -4322,13 +4221,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_Service_Title",
|
||||
table: "Sas_H_Service",
|
||||
column: "Title",
|
||||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_Skill_SkillTypeId_Name",
|
||||
table: "Sas_H_Skill",
|
||||
|
|
@ -4562,9 +4454,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_About");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_AiBot");
|
||||
|
||||
|
|
@ -4580,9 +4469,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_ChangeLog");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_Contact");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_ContactTitle");
|
||||
|
||||
|
|
@ -4616,9 +4502,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_DynamicService");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_Home");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_InstallmentOption");
|
||||
|
||||
|
|
@ -4658,9 +4541,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_Route");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_Service");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sas_H_SettingDefinition");
|
||||
|
||||
|
|
@ -627,55 +627,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1502,74 +1453,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1959,7 +1842,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoutePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
|
|
@ -1971,7 +1853,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasIndex("RoutePath")
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
.HasFilter("[IsDeleted] = 0 AND [RoutePath] IS NOT NULL");
|
||||
|
||||
b.ToTable("Sas_H_CustomComponent", (string)null);
|
||||
});
|
||||
|
|
@ -2954,95 +2836,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -5084,70 +4877,6 @@ namespace Sozsoft.Platform.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -7,22 +7,6 @@
|
|||
<RootNamespace>Sozsoft.Platform</RootNamespace>
|
||||
</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>
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="10.0.1" />
|
||||
<ProjectReference Include="..\..\modules\Sozsoft.Languages\Sozsoft.Languages.EntityFrameworkCore\Sozsoft.Languages.EntityFrameworkCore.csproj" />
|
||||
|
|
|
|||
|
|
@ -53,6 +53,28 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
|
|||
context.ApplicationConfiguration.SetProperty(
|
||||
"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>();
|
||||
if (currentUser.Id.HasValue)
|
||||
{
|
||||
|
|
|
|||
21
claude.md
21
claude.md
|
|
@ -184,6 +184,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
|||
`permission.constant.ts` + `LanguagesData.json`.
|
||||
- **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup
|
||||
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 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
|
||||
|
|
@ -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
|
||||
varsayılanı eklemektir, ezmek değil.**
|
||||
- **`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.
|
||||
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)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -47,28 +47,6 @@
|
|||
"IsQuantityBased": true,
|
||||
"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",
|
||||
"Name": "App.PublicProducts.RemoteBranchTraining",
|
||||
|
|
|
|||
291
ui/safelist.txt
291
ui/safelist.txt
|
|
@ -4495,4 +4495,293 @@ mr-3.5
|
|||
mr-1.2
|
||||
mr-1.3
|
||||
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
|
||||
|
|
@ -1703,6 +1703,52 @@ export const CUSTOM_COMPONENTS: 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',
|
||||
icon: 'Square',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ interface PlatformIconProps {
|
|||
/** Icon name as stored in the designer, e.g. `FaUser`. */
|
||||
name?: 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.
|
||||
* 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()
|
||||
if (!name) return null
|
||||
const Icon = navigationIcon[name]
|
||||
// The dictionary loads asynchronously; until it arrives there is nothing to
|
||||
// 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
|
||||
|
|
|
|||
27
ui/src/components/componentEditor/runtimeNodes.ts
Normal file
27
ui/src/components/componentEditor/runtimeNodes.ts
Normal 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>>
|
||||
|
|
@ -1,626 +1,39 @@
|
|||
import View from '@/views/Views'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useLocation, Link, useNavigate } 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 { useEffect, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import Demo from '@/views/public/Demo'
|
||||
import { ScrollContext } from '@/contexts/ScrollContext'
|
||||
import { DemoProvider } from '@/contexts/DemoContext'
|
||||
import useDarkMode from '@/utils/hooks/useDarkmode'
|
||||
import { THEME_ENUM } from '@/constants/theme.constant'
|
||||
import { Button } from '../ui'
|
||||
|
||||
interface NavLink {
|
||||
resourceKey?: string
|
||||
name: string
|
||||
path?: string
|
||||
action?: () => void
|
||||
icon?: React.ComponentType<any>
|
||||
}
|
||||
import { PUBLIC_FOOTER_COMPONENT, PUBLIC_HEADER_COMPONENT } from '@/utils/hooks/usePublicMenu'
|
||||
import { useComponents } from '@/contexts/ComponentContext'
|
||||
|
||||
/**
|
||||
* 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 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 currentYear = new Date().getFullYear()
|
||||
const { isComponentRegistered, renderComponent } = useComponents()
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0)
|
||||
}, [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 (
|
||||
<DemoProvider value={{ openDemo: () => setIsDemoOpen(true) }}>
|
||||
<ScrollContext.Provider value={scrolled}>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{/* HEADER */}
|
||||
<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"
|
||||
/>
|
||||
<div className="flex min-h-screen flex-col overflow-x-hidden">
|
||||
{isComponentRegistered(PUBLIC_HEADER_COMPONENT) &&
|
||||
renderComponent(PUBLIC_HEADER_COMPONENT)}
|
||||
|
||||
{/* Desktop / tablet navigation */}
|
||||
<nav className="hidden lg:flex lg:col-start-1 xl:col-start-2 items-center justify-self-start xl:justify-self-center gap-1">
|
||||
{navLinks
|
||||
.filter((l) => !isLoginLink(l.resourceKey))
|
||||
.map((link) => {
|
||||
const active = isActiveLink(link.path)
|
||||
const baseClass =
|
||||
'relative flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 group'
|
||||
const activeClass = active ? 'text-white' : 'text-gray-300 hover:text-white'
|
||||
return link.path ? (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={`${baseClass} ${activeClass}`}
|
||||
>
|
||||
{link.icon && (
|
||||
<link.icon
|
||||
size={22}
|
||||
strokeWidth={1.75}
|
||||
className={
|
||||
active
|
||||
? 'text-blue-400'
|
||||
: 'text-gray-400 group-hover:text-blue-400 transition-colors'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{link.name}
|
||||
<span
|
||||
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
|
||||
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
|
||||
}`}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Button
|
||||
key={link.name}
|
||||
onClick={link.action}
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={`${baseClass} ${activeClass}`}
|
||||
>
|
||||
{link.icon && (
|
||||
<link.icon
|
||||
size={22}
|
||||
strokeWidth={1.75}
|
||||
className="text-gray-400 group-hover:text-blue-400 transition-colors"
|
||||
/>
|
||||
)}
|
||||
{link.name}
|
||||
<span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 opacity-0 group-hover:opacity-60 transition-all duration-200" />
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<main className="flex-grow">
|
||||
<View />
|
||||
</main>
|
||||
|
||||
<div className="relative z-10 hidden lg:flex lg:col-start-2 xl:col-start-3 lg:justify-self-end items-center gap-3">
|
||||
{demoButton('rounded-lg')}
|
||||
<div className="h-5 w-px bg-white/20" />
|
||||
<div className="flex h-9 w-9 items-center justify-center">
|
||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||
</div>
|
||||
<div className="h-5 w-px bg-white/20" />
|
||||
{themeToggle}
|
||||
<div className="h-5 w-px bg-white/20" />
|
||||
{navLinks
|
||||
.filter((l) => isLoginLink(l.resourceKey))
|
||||
.map((link) =>
|
||||
link.path ? (
|
||||
<Button
|
||||
key={link.path}
|
||||
type="button"
|
||||
variant="solid"
|
||||
shape="round"
|
||||
onClick={() => navigate(link.path!)}
|
||||
>
|
||||
{link.name}
|
||||
</Button>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
{isComponentRegistered(PUBLIC_FOOTER_COMPONENT) && renderComponent(PUBLIC_FOOTER_COMPONENT)}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
className="lg:hidden col-start-3 justify-self-end flex items-center justify-center w-9 h-9 text-white rounded-md hover:bg-white/10 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
aria-label={translate('::App.Platform.ToggleMenu')}
|
||||
aria-expanded={isOpen}
|
||||
icon={
|
||||
isOpen ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />
|
||||
}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<div
|
||||
className={`lg:hidden transition-all duration-300 ease-in-out ${
|
||||
isOpen
|
||||
? 'max-h-screen overflow-visible opacity-100'
|
||||
: 'max-h-0 overflow-hidden opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="border-t border-white/10 bg-gray-950/98 backdrop-blur-md">
|
||||
<div className="container mx-auto px-6 py-3">
|
||||
<nav className="flex flex-col gap-1">
|
||||
<div className="lg:hidden flex flex-col gap-1">
|
||||
{navLinks.map((link) => {
|
||||
const active = isActiveLink(link.path)
|
||||
const isLogin = isLoginLink(link.resourceKey)
|
||||
if (isLogin) {
|
||||
return link.path ? (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
onClick={toggleMenu}
|
||||
className="mt-2 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
return link.path ? (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
onClick={toggleMenu}
|
||||
className={`flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium rounded-md transition-colors ${
|
||||
active
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-gray-300 hover:bg-white/5 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{link.icon && (
|
||||
<link.icon
|
||||
size={16}
|
||||
strokeWidth={1.75}
|
||||
className={active ? 'text-blue-400' : 'text-gray-400'}
|
||||
/>
|
||||
)}
|
||||
{link.name}
|
||||
</Link>
|
||||
) : (
|
||||
<Button
|
||||
key={link.name}
|
||||
onClick={() => {
|
||||
link.action?.()
|
||||
toggleMenu()
|
||||
}}
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white rounded-md transition-colors text-left"
|
||||
>
|
||||
{link.icon && (
|
||||
<link.icon size={16} strokeWidth={1.75} className="text-gray-400" />
|
||||
)}
|
||||
{link.name}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 border-t border-white/10 pt-2 pb-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
{demoButton('rounded-lg', toggleMenu)}
|
||||
<div className="flex h-10 min-w-10 items-center justify-center rounded-lg">
|
||||
<LanguageSelector className="!bg-transparent hover:!bg-transparent dark:hover:!bg-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-10 items-center rounded-lg px-1">{themeToggle}</div>
|
||||
{navLinks
|
||||
.filter((l) => isLoginLink(l.resourceKey))
|
||||
.map((link) =>
|
||||
link.path ? (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
onClick={toggleMenu}
|
||||
className="hidden h-10 lg:inline-flex items-center justify-center px-4 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-lg shadow-md shadow-blue-900/30 transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-grow">
|
||||
<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">
|
||||
© {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>
|
||||
<Demo isOpen={isDemoOpen} onClose={() => setIsDemoOpen(false)} />
|
||||
</div>
|
||||
</DemoProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import { FaCalendar, FaClock, FaMinus, FaPlus, FaShoppingCart } from 'react-icons/fa'
|
||||
import { BillingCycle } from '@/proxy/order/models'
|
||||
import { BasketData } from '@/utils/basketUtils'
|
||||
import { useScroll } from '@/contexts/ScrollContext'
|
||||
import { useScrolled } from '@/utils/hooks/useScrolled'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
|
|||
basketData,
|
||||
}) => {
|
||||
const hasBasketItems = basketData.items.length > 0
|
||||
const isScrolled = useScroll()
|
||||
const isScrolled = useScrolled()
|
||||
const { translate } = useLocalization()
|
||||
|
||||
return (
|
||||
|
|
|
|||
343
ui/src/components/publicSite/index.tsx
Normal file
343
ui/src/components/publicSite/index.tsx
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* Public site basliginin tasarlanabilir parcalari. Her biri Visual Designer
|
||||
* katalogunda bir dugumdur: `PublicHeader` custom component'i bunlari kanvasta
|
||||
* birlestirir, uretilen kod ayni bilesenleri cagirir. Menu, tema, dil ve demo
|
||||
* gibi davranislar burada yasar; yerlesim ve siniflar tasarimciya birakilir.
|
||||
*/
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { LuMenu, LuMoon, LuSun, LuX } from 'react-icons/lu'
|
||||
import Logo from '@/views/public/Logo'
|
||||
import LanguageSelector from '@/components/template/LanguageSelector'
|
||||
import { PageIcon } from '@/views/public/shared'
|
||||
import { Button } from '@/components/ui'
|
||||
import { THEME_ENUM } from '@/constants/theme.constant'
|
||||
import useDarkMode from '@/utils/hooks/useDarkmode'
|
||||
import { useScrolled } from '@/utils/hooks/useScrolled'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useOptionalDemo } from '@/contexts/DemoContext'
|
||||
import {
|
||||
PUBLIC_MENU_CTA_CLASS,
|
||||
usePublicMenu,
|
||||
type PublicMenuNode,
|
||||
} from '@/utils/hooks/usePublicMenu'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
|
||||
/** Mobil panel acikken bir baglantiya basilinca panel kendini kapatir. */
|
||||
const MobileMenuContext = createContext<(() => void) | null>(null)
|
||||
|
||||
const isCallToAction = (item: PublicMenuNode) =>
|
||||
item.cssClass?.split(/\s+/).includes(PUBLIC_MENU_CTA_CLASS) ?? false
|
||||
|
||||
// ------------------------------------------------------------------ HeaderBar
|
||||
|
||||
interface PublicHeaderBarProps {
|
||||
className?: string
|
||||
/** Sayfa kaydirildiginda `className` yerine bu uygulanir; bos birakilirsa baslik degismez. */
|
||||
scrolledClassName?: string
|
||||
/** Kaydirmanin "basladi" sayildigi piksel esigi. */
|
||||
scrollThreshold?: number
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const PublicHeaderBar = ({
|
||||
className = '',
|
||||
scrolledClassName = '',
|
||||
scrollThreshold = 10,
|
||||
children,
|
||||
}: PublicHeaderBarProps) => {
|
||||
const scrolled = useScrolled(scrollThreshold)
|
||||
|
||||
return (
|
||||
<header className={scrolled && scrolledClassName ? scrolledClassName : className}>
|
||||
{children}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------- Logo
|
||||
|
||||
interface PublicLogoProps {
|
||||
className?: string
|
||||
imgClassName?: string
|
||||
}
|
||||
|
||||
/** Logonun aydinlik/karanlik varyantini aktif temadan secer. */
|
||||
export const PublicLogo = ({ className = '', imgClassName = '' }: PublicLogoProps) => {
|
||||
const [isDarkMode] = useDarkMode()
|
||||
|
||||
return <Logo className={className} imgClass={imgClassName} mode={isDarkMode ? 'dark' : 'light'} />
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- Language
|
||||
|
||||
export const PublicLanguageSelector = ({ className = '' }: { className?: string }) => (
|
||||
<LanguageSelector className={className} />
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------- ThemeToggle
|
||||
|
||||
interface PublicThemeToggleProps {
|
||||
className?: string
|
||||
buttonClassName?: string
|
||||
activeClassName?: string
|
||||
iconSize?: number
|
||||
}
|
||||
|
||||
export const PublicThemeToggle = ({
|
||||
className = 'inline-flex h-9 items-center rounded-lg p-0.5',
|
||||
buttonClassName = '!h-8 !w-8 xl:!w-auto !px-0 xl:!px-2.5 !border-transparent !shadow-none !bg-transparent !text-gray-300 hover:!bg-transparent hover:!text-white',
|
||||
activeClassName = '!bg-white !text-gray-950',
|
||||
iconSize = 15,
|
||||
}: PublicThemeToggleProps) => {
|
||||
const [isDarkMode, setThemeMode] = useDarkMode()
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const modes = [
|
||||
{
|
||||
mode: THEME_ENUM.MODE_LIGHT,
|
||||
active: !isDarkMode,
|
||||
title: '::App.Theme.LightMode',
|
||||
icon: <LuSun size={iconSize} strokeWidth={1.8} />,
|
||||
shape: '!rounded-l-md !rounded-r-none',
|
||||
},
|
||||
{
|
||||
mode: THEME_ENUM.MODE_DARK,
|
||||
active: isDarkMode,
|
||||
title: '::App.Theme.DarkMode',
|
||||
icon: <LuMoon size={iconSize} strokeWidth={1.8} />,
|
||||
shape: '!rounded-r-md !rounded-l-none',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{modes.map((item) => (
|
||||
<Button
|
||||
key={item.mode}
|
||||
aria-pressed={item.active}
|
||||
className={`${buttonClassName} ${item.shape} ${item.active ? activeClassName : ''}`}
|
||||
icon={item.icon}
|
||||
size="xs"
|
||||
title={translate(item.title)}
|
||||
type="button"
|
||||
variant="plain"
|
||||
onClick={() => setThemeMode(item.mode)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- DemoButton
|
||||
|
||||
export const PublicDemoButton = ({ className = '' }: { className?: string }) => {
|
||||
const { translate } = useLocalization()
|
||||
const { openDemo } = useOptionalDemo()
|
||||
const closeMobileMenu = useContext(MobileMenuContext)
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`group !h-9 !w-9 !overflow-visible !bg-transparent !px-0 !text-gray-200 !shadow-none transition-colors hover:!bg-transparent hover:!text-white ${className}`}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
openDemo()
|
||||
closeMobileMenu?.()
|
||||
}}
|
||||
>
|
||||
<span className="relative inline-flex h-7 w-7 items-center justify-center overflow-visible rounded-full border-2 border-stone-700 bg-gradient-to-b from-red-400 via-red-600 to-red-800 shadow-[inset_0_-3px_6px_rgba(0,0,0,0.35),0_1px_3px_rgba(0,0,0,0.35)] ring-1 ring-black/60 transition-all duration-200 group-hover:scale-105 group-hover:from-red-300 group-hover:via-red-500 group-hover:to-red-700 group-hover:shadow-[inset_0_-3px_6px_rgba(0,0,0,0.3),0_2px_8px_rgba(239,68,68,0.35)]">
|
||||
<span className="absolute inset-x-1 top-1 h-2 rounded-full bg-white/35 blur-[1px] transition-colors group-hover:bg-white/50" />
|
||||
<span className="absolute left-1/2 top-1/2 z-10 w-12 -translate-x-1/2 -translate-y-1/2 text-center uppercase leading-none tracking-tight text-white drop-shadow">
|
||||
{translate('::App.Demos')}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------ Nav
|
||||
|
||||
interface PublicNavProps {
|
||||
/** `navigation` gezinme baglantilari · `actions` vurgulu (cta) kayitlar · `all` hepsi. */
|
||||
source?: 'navigation' | 'actions' | 'all'
|
||||
/** `horizontal` masaustu seridi · `vertical` mobil liste. */
|
||||
variant?: 'horizontal' | 'vertical'
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
activeItemClassName?: string
|
||||
iconSize?: number
|
||||
showIcons?: boolean
|
||||
}
|
||||
|
||||
const HORIZONTAL_ITEM =
|
||||
'group relative flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium text-gray-300 transition-all duration-200 hover:text-white'
|
||||
const VERTICAL_ITEM =
|
||||
'flex items-center gap-2.5 rounded-md px-3 py-2.5 text-sm font-medium text-gray-300 transition-colors hover:bg-white/5 hover:text-white'
|
||||
const ACTION_ITEM =
|
||||
'inline-flex items-center justify-center rounded-full bg-blue-600 px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-500'
|
||||
const ACTION_ITEM_VERTICAL =
|
||||
'mt-2 flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-500'
|
||||
|
||||
export const PublicNav = ({
|
||||
source = 'navigation',
|
||||
variant = 'horizontal',
|
||||
className = '',
|
||||
itemClassName = '',
|
||||
activeItemClassName = '',
|
||||
iconSize,
|
||||
showIcons = true,
|
||||
}: PublicNavProps) => {
|
||||
const { headerLinks } = usePublicMenu()
|
||||
const { translate } = useLocalization()
|
||||
const location = useLocation()
|
||||
const closeMobileMenu = useContext(MobileMenuContext)
|
||||
|
||||
const isAction = source === 'actions'
|
||||
const horizontal = variant === 'horizontal'
|
||||
const items = useMemo(
|
||||
() =>
|
||||
source === 'all'
|
||||
? headerLinks
|
||||
: headerLinks.filter((item) => isCallToAction(item) === isAction),
|
||||
[headerLinks, isAction, source],
|
||||
)
|
||||
|
||||
const label = (item: PublicMenuNode) => translate(`::${item.displayName ?? item.code}`)
|
||||
const isActive = (url?: string) => {
|
||||
if (!url) return false
|
||||
return url === ROUTES_ENUM.public.home
|
||||
? location.pathname === url
|
||||
: location.pathname.startsWith(url)
|
||||
}
|
||||
|
||||
const defaultItemClass = isAction
|
||||
? horizontal
|
||||
? ACTION_ITEM
|
||||
: ACTION_ITEM_VERTICAL
|
||||
: horizontal
|
||||
? HORIZONTAL_ITEM
|
||||
: VERTICAL_ITEM
|
||||
const baseItem = itemClassName || defaultItemClass
|
||||
const activeItem =
|
||||
activeItemClassName || (isAction ? '' : horizontal ? 'text-white' : 'bg-white/10 text-white')
|
||||
// Katalog varsayilani 0'dir: "otomatik" demek, varyanta gore boyut secilir.
|
||||
const size = iconSize || (horizontal ? 22 : 16)
|
||||
|
||||
const itemContent = (item: PublicMenuNode, active: boolean) => (
|
||||
<>
|
||||
{showIcons && (
|
||||
<PageIcon
|
||||
className={
|
||||
active ? 'text-blue-400' : 'text-gray-400 transition-colors group-hover:text-blue-400'
|
||||
}
|
||||
name={item.icon}
|
||||
size={size}
|
||||
/>
|
||||
)}
|
||||
{label(item)}
|
||||
{horizontal && !isAction && (
|
||||
<span
|
||||
className={`absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-blue-500 transition-all duration-200 ${
|
||||
active ? 'opacity-100' : 'opacity-0 group-hover:opacity-60'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const renderItem = (item: PublicMenuNode, extraClass = '') => {
|
||||
const active = isActive(item.url)
|
||||
const itemClass = `${baseItem} ${active ? activeItem : ''} ${extraClass}`
|
||||
|
||||
if (!item.url) {
|
||||
return (
|
||||
<span key={item.code} className={`${itemClass} cursor-default`}>
|
||||
{itemContent(item, active)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={item.code} className={itemClass} to={item.url} onClick={() => closeMobileMenu?.()}>
|
||||
{itemContent(item, active)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={className}>
|
||||
{items.map((item) => {
|
||||
if (!item.children.length) return renderItem(item)
|
||||
|
||||
// Alt menusu olan kayit: masaustunde acilir liste, mobilde girintili satirlar.
|
||||
if (horizontal) {
|
||||
return (
|
||||
<div key={item.code} className="group/menu relative">
|
||||
{renderItem(item)}
|
||||
<div className="invisible absolute left-0 top-full z-20 min-w-[13rem] rounded-lg border border-white/10 bg-gray-950/98 py-2 opacity-0 shadow-xl backdrop-blur-md transition-all duration-150 group-hover/menu:visible group-hover/menu:opacity-100">
|
||||
{item.children.map((child) => renderItem(child, 'px-4 py-2'))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={item.code} className="flex flex-col">
|
||||
{renderItem(item)}
|
||||
{item.children.map((child) => renderItem(child, 'ml-4'))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- MobileMenu
|
||||
|
||||
interface PublicMobileMenuProps {
|
||||
className?: string
|
||||
buttonClassName?: string
|
||||
panelClassName?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Hamburger dugmesi ve acilir panel. Panelin icerigi tasarimcidan gelir; panel
|
||||
* en yakin konumlandirilmis ust ogenin altina acilir.
|
||||
*/
|
||||
export const PublicMobileMenu = ({
|
||||
className = '',
|
||||
buttonClassName = 'flex h-9 w-9 items-center justify-center rounded-md text-white transition-colors hover:bg-white/10',
|
||||
panelClassName = 'absolute inset-x-0 top-full z-50 border-t border-white/10 bg-gray-950 px-6 py-3 shadow-xl shadow-black/40',
|
||||
children,
|
||||
}: PublicMobileMenuProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { translate } = useLocalization()
|
||||
const { pathname } = useLocation()
|
||||
|
||||
// Panelin icindeki her baglanti context'i kullanmaz (tasarimciya konan ham `a`
|
||||
// dugumleri, tarayici geri/ileri): adres degistiginde panel her hâlükârda kapanir.
|
||||
useEffect(() => setOpen(false), [pathname])
|
||||
|
||||
return (
|
||||
<MobileMenuContext.Provider value={() => setOpen(false)}>
|
||||
{/* Panel de sarmalayicinin icinde: `lg:hidden` gibi kirilim siniflari boylece
|
||||
panele de isler, masaustu genisligine donuldugunde acik kalmaz. */}
|
||||
<div className={className}>
|
||||
<Button
|
||||
aria-expanded={open}
|
||||
aria-label={translate('::App.Platform.ToggleMenu')}
|
||||
className={buttonClassName}
|
||||
icon={open ? <LuX size={20} strokeWidth={2} /> : <LuMenu size={20} strokeWidth={2} />}
|
||||
size="xs"
|
||||
type="button"
|
||||
variant="plain"
|
||||
onClick={() => setOpen((previous) => !previous)}
|
||||
/>
|
||||
{open && <div className={panelClassName}>{children}</div>}
|
||||
</div>
|
||||
</MobileMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
83
ui/src/components/visualDesigner/CanvasFrame.tsx
Normal file
83
ui/src/components/visualDesigner/CanvasFrame.tsx
Normal 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
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import * as UiKit from '@/components/ui'
|
||||
import PlatformIcon from '@/components/componentEditor/PlatformIcon'
|
||||
import { RUNTIME_NODE_COMPONENTS } from '@/components/componentEditor/runtimeNodes'
|
||||
import PlatformSelectComponent from '@/components/componentEditor/selectComponents'
|
||||
import PlatformViewHost, {
|
||||
type PlatformViewName,
|
||||
|
|
@ -8,7 +9,7 @@ import PlatformViewHost, {
|
|||
import apiService from '@/services/api.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { formatLocaleValue } from '@/utils/localeFormat'
|
||||
import { FaArrowDown, FaArrowUp, FaClone, FaGripVertical, FaTrash } from 'react-icons/fa'
|
||||
import { FaGripVertical } from 'react-icons/fa'
|
||||
import { beginDesignerDragSoon, endDesignerDrag, useDesignerDrag } from './designerDrag'
|
||||
import Input from '@/components/ui/Input'
|
||||
import {
|
||||
|
|
@ -167,10 +168,7 @@ interface VisualCanvasProps {
|
|||
) => void
|
||||
/** Moves an existing node into a container/table cell, or to the root. */
|
||||
onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void
|
||||
onMove?: (id: string, direction: -1 | 1) => void
|
||||
onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void
|
||||
onDuplicate?: (id: string) => void
|
||||
onDelete?: (id: string) => void
|
||||
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
|
||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
||||
dataValues?: Record<string, unknown>
|
||||
|
|
@ -1289,6 +1287,18 @@ const renderElement = (
|
|||
refs,
|
||||
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') {
|
||||
return renderCustomComponent?.(node.type, { ...props, children: content }) || null
|
||||
}
|
||||
|
|
@ -1429,21 +1439,97 @@ const HTML_PLACEHOLDER_ELEMENTS = new Set([
|
|||
'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 = ({
|
||||
node,
|
||||
index,
|
||||
siblingCount = 1,
|
||||
isRoot = false,
|
||||
selectedId,
|
||||
interactive,
|
||||
onSelect,
|
||||
onDropComponent,
|
||||
onDropComponentBeside,
|
||||
onMoveIntoContainer,
|
||||
onMove,
|
||||
onReorder,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onNodePropChange,
|
||||
renderCustomComponent,
|
||||
dataValues,
|
||||
|
|
@ -1451,9 +1537,6 @@ const NodeView = ({
|
|||
formScope,
|
||||
}: {
|
||||
node: DesignerNode
|
||||
index: number
|
||||
siblingCount?: number
|
||||
isRoot?: boolean
|
||||
selectedId: string | null
|
||||
interactive: boolean
|
||||
onSelect?: (id: string) => void
|
||||
|
|
@ -1464,10 +1547,7 @@ const NodeView = ({
|
|||
placement: 'before' | 'after',
|
||||
) => void
|
||||
onMoveIntoContainer?: (nodeId: string, parentId: string | null, slot?: string) => void
|
||||
onMove?: (id: string, direction: -1 | 1) => void
|
||||
onReorder?: (sourceId: string, targetId: string, placement: 'before' | 'after') => void
|
||||
onDuplicate?: (id: string) => void
|
||||
onDelete?: (id: string) => void
|
||||
onNodePropChange?: (id: string, propertyName: string, value: unknown) => void
|
||||
renderCustomComponent?: (name: string, props?: Record<string, unknown>) => React.ReactNode
|
||||
dataValues: Record<string, unknown>
|
||||
|
|
@ -1483,10 +1563,13 @@ const NodeView = ({
|
|||
const refOverrides = refOverride?.props || {}
|
||||
const refHidden = Boolean(refOverride?.hidden)
|
||||
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.
|
||||
// Only the first/last node at the root has nowhere left to go.
|
||||
const canMoveUp = !isRoot || index > 0
|
||||
const canMoveDown = !isRoot || index < siblingCount - 1
|
||||
const acceptsDroppedChildren = [
|
||||
'PageContainer',
|
||||
'FlexRow',
|
||||
|
|
@ -1633,8 +1716,6 @@ const NodeView = ({
|
|||
<NodeView
|
||||
key={`${child.id}_${itemIndex}`}
|
||||
node={child}
|
||||
index={childIndex}
|
||||
siblingCount={node.children.length}
|
||||
selectedId={selectedId}
|
||||
interactive={interactive}
|
||||
renderCustomComponent={renderCustomComponent}
|
||||
|
|
@ -1645,10 +1726,7 @@ const NodeView = ({
|
|||
onDropComponent={onDropComponent}
|
||||
onDropComponentBeside={onDropComponentBeside}
|
||||
onMoveIntoContainer={onMoveIntoContainer}
|
||||
onMove={onMove}
|
||||
onReorder={onReorder}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onNodePropChange={onNodePropChange}
|
||||
/>
|
||||
)),
|
||||
|
|
@ -1729,8 +1807,6 @@ const NodeView = ({
|
|||
<NodeView
|
||||
key={child.id}
|
||||
node={child}
|
||||
index={childIndex}
|
||||
siblingCount={tabNodes.length}
|
||||
selectedId={selectedId}
|
||||
interactive={interactive}
|
||||
renderCustomComponent={renderCustomComponent}
|
||||
|
|
@ -1741,10 +1817,7 @@ const NodeView = ({
|
|||
onDropComponent={onDropComponent}
|
||||
onDropComponentBeside={onDropComponentBeside}
|
||||
onMoveIntoContainer={onMoveIntoContainer}
|
||||
onMove={onMove}
|
||||
onReorder={onReorder}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onNodePropChange={onNodePropChange}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -1824,8 +1897,6 @@ const NodeView = ({
|
|||
<NodeView
|
||||
key={child.id}
|
||||
node={child}
|
||||
index={childIndex}
|
||||
siblingCount={node.children.length}
|
||||
selectedId={selectedId}
|
||||
interactive={interactive}
|
||||
renderCustomComponent={renderCustomComponent}
|
||||
|
|
@ -1836,10 +1907,7 @@ const NodeView = ({
|
|||
onDropComponent={onDropComponent}
|
||||
onDropComponentBeside={onDropComponentBeside}
|
||||
onMoveIntoContainer={onMoveIntoContainer}
|
||||
onMove={onMove}
|
||||
onReorder={onReorder}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onNodePropChange={onNodePropChange}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -1983,7 +2051,9 @@ const NodeView = ({
|
|||
return (
|
||||
<DesignerDraggedSubtreeContext.Provider value={insideDraggedSubtree}>
|
||||
<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.
|
||||
isDragSource ? 'opacity-40' : ''
|
||||
} ${
|
||||
|
|
@ -2001,7 +2071,11 @@ const NodeView = ({
|
|||
}`}
|
||||
data-designer-node={node.id}
|
||||
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)
|
||||
}}
|
||||
onClick={(event) => {
|
||||
|
|
@ -2045,12 +2119,6 @@ const NodeView = ({
|
|||
<div className="absolute inset-x-0 -bottom-2 z-20 h-2" {...edgeStripProps('after')} />
|
||||
</>
|
||||
)}
|
||||
{/*
|
||||
Taşıma tutamağı. Bir Input ya da Select'in gövdesi tamamen kontrolle
|
||||
kaplıdır ve tarayıcı oradan üst öğenin sürüklenmesini başlatmaz; tutamak
|
||||
her bileşen için garanti bir tutma noktası verir. Sürükleme sürerken
|
||||
gizlenir, aksi halde komşu düğümün bırakma alanını kapatırdı.
|
||||
*/}
|
||||
{/* Yanına bırakma çizgisi: bileşen tam olarak buraya girer. */}
|
||||
{dropHint === 'before' && (
|
||||
<div className="pointer-events-none absolute inset-x-0 -top-1 z-30 flex items-center gap-1">
|
||||
|
|
@ -2079,79 +2147,26 @@ const NodeView = ({
|
|||
{translate('::App.VisualDesignerCanvas.CannotMoveHere')}
|
||||
</span>
|
||||
)}
|
||||
{/*
|
||||
Tasima tutamagi. Bir Input ya da Select'in govdesi tamamen canli kontrolle
|
||||
kaplidir ve tarayici oradan ust ogenin suruklenmesini baslatmaz; bu tutamak
|
||||
her bilesen icin garanti bir tutma noktasi verir. Eskiden burada tasima ve
|
||||
sil/kopyala dugmelerini de tasiyan genis bir serit vardi; kucuk bir ikonun ya
|
||||
da ayirici cizginin ustunu tamamen ortuyordu. Dugmeler sag paneldeki secili
|
||||
bilesen basligina tasindi, kanvasta yalnizca kose tutamagi kaldi.
|
||||
*/}
|
||||
{interactive && (
|
||||
<div
|
||||
className={`absolute right-1 top-1 z-20 items-center overflow-hidden rounded-md bg-sky-600 text-white shadow-lg ${selected ? 'flex' : 'hidden'}`}
|
||||
<span
|
||||
draggable
|
||||
className={`absolute -right-2 -top-2 z-30 h-4 w-4 cursor-grab items-center justify-center rounded-full bg-sky-600 text-[8px] text-white shadow ring-1 ring-white active:cursor-grabbing dark:ring-gray-900 ${selected ? 'flex' : 'hidden'}`}
|
||||
title={translate('::App.VisualDesignerCanvas.DragToMove', {
|
||||
name: node.ref || node.type,
|
||||
})}
|
||||
onDragEnd={() => endDesignerDrag()}
|
||||
onDragStart={startNodeDrag}
|
||||
>
|
||||
{/*
|
||||
Taşıma tutamağı. Bir Input ya da Select'in gövdesi tamamen canlı
|
||||
kontrolle kaplıdır ve tarayıcı oradan üst öğenin sürüklenmesini
|
||||
başlatmaz; bu rozet her bileşen için garanti bir tutma noktası verir.
|
||||
*/}
|
||||
<span
|
||||
draggable
|
||||
className="flex cursor-grab items-center gap-1 px-2 text-[10px] font-semibold active:cursor-grabbing"
|
||||
title={translate('::App.VisualDesignerCanvas.DragToMove', {
|
||||
name: node.ref || node.type,
|
||||
})}
|
||||
onDragStart={startNodeDrag}
|
||||
onDragEnd={() => endDesignerDrag()}
|
||||
>
|
||||
<FaGripVertical /> {node.type}
|
||||
</span>
|
||||
<button
|
||||
className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={!canMoveUp}
|
||||
title={translate(
|
||||
canMoveUp ? '::App.Platform.MoveUp' : '::App.VisualDesignerCanvas.AlreadyAtTop',
|
||||
)}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onMove?.(node.id, -1)
|
||||
}}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 enabled:hover:bg-sky-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={!canMoveDown}
|
||||
title={translate(
|
||||
canMoveDown
|
||||
? '::App.Platform.MoveDown'
|
||||
: '::App.VisualDesignerCanvas.AlreadyAtBottom',
|
||||
)}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onMove?.(node.id, 1)
|
||||
}}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 hover:bg-sky-700"
|
||||
title={translate('::App.Platform.Duplicate')}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onDuplicate?.(node.id)
|
||||
}}
|
||||
>
|
||||
<FaClone />
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 hover:bg-red-600"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onDelete?.(node.id)
|
||||
}}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
<FaGripVertical />
|
||||
</span>
|
||||
)}
|
||||
{interactive && refHidden && (
|
||||
<span className="absolute left-1 top-1 z-10 rounded bg-slate-700 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-white">
|
||||
|
|
@ -2163,7 +2178,7 @@ const NodeView = ({
|
|||
resetKey={JSON.stringify([node.props, node.bindings, node.events])}
|
||||
>
|
||||
{renderElement(
|
||||
node,
|
||||
renderedNode,
|
||||
contentChildren,
|
||||
dataValues,
|
||||
currentItem,
|
||||
|
|
@ -2178,6 +2193,10 @@ const NodeView = ({
|
|||
</PreviewBoundary>
|
||||
{/* Tabs has a drop zone inside every tab, so it needs no outer placeholder. */}
|
||||
{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 &&
|
||||
acceptsDroppedChildren &&
|
||||
node.type !== 'Card' &&
|
||||
|
|
@ -2191,7 +2210,6 @@ const NodeView = ({
|
|||
{translate('::App.VisualDesignerCanvas.DropComponentHere')}
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden">{index}</span>
|
||||
</div>
|
||||
</DesignerDraggedSubtreeContext.Provider>
|
||||
)
|
||||
|
|
@ -2205,10 +2223,7 @@ const VisualCanvas = ({
|
|||
onDropComponent,
|
||||
onDropComponentBeside,
|
||||
onMoveIntoContainer,
|
||||
onMove,
|
||||
onReorder,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onNodePropChange,
|
||||
renderCustomComponent,
|
||||
dataValues = {},
|
||||
|
|
@ -2277,9 +2292,11 @@ const VisualCanvas = ({
|
|||
<CanvasWidthContext.Provider value={canvasWidth}>
|
||||
<DesignerRefContext.Provider value={refStore}>
|
||||
<div
|
||||
className={`min-h-full p-8 text-slate-900 transition dark:text-slate-100 ${
|
||||
drag && interactive ? 'bg-sky-50/40 dark:bg-sky-950/20' : ''
|
||||
}`}
|
||||
className={`min-h-full text-slate-900 transition dark:text-slate-100 ${
|
||||
// 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?.('')}
|
||||
onDragOver={(event) => interactive && event.preventDefault()}
|
||||
onDrop={(event) => {
|
||||
|
|
@ -2307,15 +2324,22 @@ const VisualCanvas = ({
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/*
|
||||
Transform sayfa kutusunu `position: fixed` icin kapsayan blok yapar, boylece
|
||||
sabitlenen bir dugum tarayici penceresine degil sayfa genisligine yayilir.
|
||||
Dugumun kendi `fixed` sinifi akisa cevriliyor (`canvasPositionClass`) ama bir
|
||||
bilesen bunu baska bir proptan getirebilir — PublicHeaderBar kaydirilinca
|
||||
`scrolledClassName` ile `fixed` olur ve serit ozellik panelinin ustune binerdi.
|
||||
*/}
|
||||
{nodes.length ? (
|
||||
<div className="space-y-4">
|
||||
{nodes.map((node, index) => (
|
||||
<div
|
||||
className={canvasWidth === 'responsive' ? 'space-y-4' : ''}
|
||||
style={{ transform: 'translateZ(0)' }}
|
||||
>
|
||||
{nodes.map((node) => (
|
||||
<NodeView
|
||||
key={node.id}
|
||||
isRoot
|
||||
node={node}
|
||||
index={index}
|
||||
siblingCount={nodes.length}
|
||||
selectedId={selectedId}
|
||||
interactive={interactive}
|
||||
renderCustomComponent={renderCustomComponent}
|
||||
|
|
@ -2324,10 +2348,7 @@ const VisualCanvas = ({
|
|||
onDropComponent={onDropComponent}
|
||||
onDropComponentBeside={onDropComponentBeside}
|
||||
onMoveIntoContainer={onMoveIntoContainer}
|
||||
onMove={onMove}
|
||||
onReorder={onReorder}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onNodePropChange={onNodePropChange}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ interface 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'] =>
|
||||
name === 'Table' ? 'layout' : DESIGNER_DATA_COMPONENT_NAMES.has(name) ? 'data' : 'ui'
|
||||
/** Components intentionally kept out of the toolbox. */
|
||||
|
|
@ -875,6 +875,122 @@ const platformDefinition = (
|
|||
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[] = [
|
||||
platformDefinition('ListView', 'List', 'App.CatalogListView.ListViewDescription'),
|
||||
platformDefinition('DataGridView', 'Grid', 'App.CatalogDataGridView.DataGridViewDescription'),
|
||||
|
|
@ -1085,6 +1201,22 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
|||
],
|
||||
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',
|
||||
icon: 'Space',
|
||||
|
|
@ -1146,6 +1278,7 @@ export const getDesignerCatalog = (customNames: string[] = []): DesignerComponen
|
|||
...DESIGNER_EXTRAS,
|
||||
FORM_DEFINITION,
|
||||
...PLATFORM_COMPONENTS,
|
||||
...PUBLIC_SITE_COMPONENTS,
|
||||
...html,
|
||||
...ui,
|
||||
...generatedOnlyUi,
|
||||
|
|
|
|||
|
|
@ -4,18 +4,6 @@ export const AI_ASSISTANT = 'App.Definitions.AiBot.Asistant'
|
|||
/** Veritabanı migration + seed tetikleme yetkisi (host tarafı). */
|
||||
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
|
||||
* yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol
|
||||
|
|
|
|||
|
|
@ -222,11 +222,20 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
try {
|
||||
// Babel and the UI kit are only needed when an active runtime component
|
||||
// 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('../components/ui'),
|
||||
import('axios'),
|
||||
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
|
||||
|
||||
|
|
@ -235,6 +244,7 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
...ui,
|
||||
UiKit: ui,
|
||||
PlatformViewHost,
|
||||
...RUNTIME_NODE_COMPONENTS,
|
||||
apiService,
|
||||
DOMPurify,
|
||||
axios,
|
||||
|
|
|
|||
|
|
@ -8,16 +8,10 @@ const DemoContext = createContext<DemoContextValue | undefined>(undefined)
|
|||
|
||||
export const DemoProvider = DemoContext.Provider
|
||||
|
||||
export const useDemo = () => {
|
||||
const context = useContext(DemoContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useDemo must be used within a DemoProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo penceresini acan cagri. Saglayici disinda cizilen bileşen (tasarimci kanvasi)
|
||||
* de ayni dugumu kullanabilsin diye eksik context bir hataya degil, bos cagriya doner.
|
||||
*/
|
||||
export const useOptionalDemo = () => {
|
||||
const context = useContext(DemoContext)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
// ScrollContext.tsx
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export const ScrollContext = createContext<boolean>(false)
|
||||
|
||||
export const useScroll = () => useContext(ScrollContext)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -25,6 +25,15 @@ const runtimeComponentCache = new Map<string, React.ComponentType<any>>()
|
|||
// time instead.
|
||||
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)
|
||||
function loadPhysicalComponent(componentPath: string) {
|
||||
const cleanedPath = componentPath.replace(/^@\//, '')
|
||||
|
|
@ -124,22 +133,24 @@ export interface DynamicReactRoute {
|
|||
|
||||
// API'den gelen route objesini, React Router için uygun hale getirir
|
||||
export function mapDynamicRoutes(routes: RouteDto[]): DynamicReactRoute[] {
|
||||
return routes.map((route) => ({
|
||||
key: route.path,
|
||||
path: route.path,
|
||||
getComponent: (registeredComponents, renderComponent, isComponentRegistered) =>
|
||||
loadComponent(
|
||||
route.componentType,
|
||||
route.componentPath,
|
||||
registeredComponents,
|
||||
renderComponent,
|
||||
isComponentRegistered,
|
||||
),
|
||||
routeType: route.routeType,
|
||||
authority: route.authority,
|
||||
componentType: route.componentType,
|
||||
componentPath: route.componentPath,
|
||||
}))
|
||||
return routes
|
||||
.filter((route) => route.componentType === 'dynamic' || hasPhysicalComponent(route.componentPath))
|
||||
.map((route) => ({
|
||||
key: route.path,
|
||||
path: route.path,
|
||||
getComponent: (registeredComponents, renderComponent, isComponentRegistered) =>
|
||||
loadComponent(
|
||||
route.componentType,
|
||||
route.componentPath,
|
||||
registeredComponents,
|
||||
renderComponent,
|
||||
isComponentRegistered,
|
||||
),
|
||||
routeType: route.routeType,
|
||||
authority: route.authority,
|
||||
componentType: route.componentType,
|
||||
componentPath: route.componentPath,
|
||||
}))
|
||||
}
|
||||
|
||||
// Custom components are the single source of truth for runtime routes.
|
||||
|
|
|
|||
|
|
@ -62,10 +62,18 @@ export const DynamicRouter: React.FC = () => {
|
|||
useComponents()
|
||||
const location = useLocation()
|
||||
|
||||
const dynamicRoutes = React.useMemo(
|
||||
() => [...mapDynamicRoutes(routes), ...mapCustomComponentRoutes(components)],
|
||||
[routes, components],
|
||||
)
|
||||
const dynamicRoutes = React.useMemo(() => {
|
||||
const customComponentRoutes = mapCustomComponentRoutes(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ı.
|
||||
// bootstrapFailed durumu Layouts'ta, layout seçilmeden önce ele alınır.
|
||||
|
|
|
|||
|
|
@ -2,18 +2,11 @@
|
|||
setup: '/setup',
|
||||
public: {
|
||||
home: '/home',
|
||||
about: '/about',
|
||||
aboutDesigner: '/about/designer',
|
||||
products: '/products',
|
||||
checkout: '/checkout',
|
||||
payment: '/payment',
|
||||
success: '/success',
|
||||
services: '/services',
|
||||
servicesDesigner: '/services/designer',
|
||||
blog: '/blog',
|
||||
blogDetail: '/blog/:id',
|
||||
demo: '/demo',
|
||||
contact: '/contact',
|
||||
accessDenied: '/access-denied',
|
||||
},
|
||||
authenticated: {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
)
|
||||
}
|
||||
|
|
@ -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' },
|
||||
)
|
||||
}
|
||||
30
ui/src/services/country.service.ts
Normal file
30
ui/src/services/country.service.ts
Normal 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' },
|
||||
)
|
||||
}
|
||||
|
|
@ -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' },
|
||||
)
|
||||
}
|
||||
|
|
@ -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' },
|
||||
)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import appConfig from '../proxy/configs/app.config'
|
|||
import { NavigationTree } from '../proxy/menus/navigation'
|
||||
import { MenuDto } from '../proxy/menus/models'
|
||||
import getChildren from '../utils/navigation'
|
||||
import { PUBLIC_MENU_MODULE_ID } from '../utils/hooks/usePublicMenu'
|
||||
import {
|
||||
ApplicationConfigurationDto,
|
||||
ApplicationLocalizationRequestDto,
|
||||
|
|
@ -108,12 +109,17 @@ export const abpConfigModel: AbpConfigModel = {
|
|||
const texts = getState().texts
|
||||
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
|
||||
menu.displayName = getLocalization(texts || {}, defaultResourceName, displayName)
|
||||
})
|
||||
|
||||
const menu = getChildren(result.data.items ?? [], null)
|
||||
const menu = getChildren(items, null)
|
||||
actions.setMenu(menu)
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
65
ui/src/utils/hooks/usePublicMenu.ts
Normal file
65
ui/src/utils/hooks/usePublicMenu.ts
Normal 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])
|
||||
}
|
||||
20
ui/src/utils/hooks/useScrolled.ts
Normal file
20
ui/src/utils/hooks/useScrolled.ts
Normal 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
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
AuthorizationTypeEnum,
|
||||
ColumnCascadeFilterOperatorEnum,
|
||||
ColumnRowTypeListOptions,
|
||||
ColumnSortDirectionEnum,
|
||||
|
|
@ -29,19 +28,6 @@ import { enumToList } from '../../../../utils/enumUtils'
|
|||
import { PREFIX } from '@/constants/theme.constant'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
|
||||
export enum WidgetColorEnum {
|
||||
'Blue' = 'blue',
|
||||
'Green' = 'green',
|
||||
'Purple' = 'purple',
|
||||
'Gray' = 'gray',
|
||||
'Red' = 'red',
|
||||
'Yellow' = 'yellow',
|
||||
'Pink' = 'pink',
|
||||
'Indigo' = 'indigo',
|
||||
'Teal' = 'teal',
|
||||
'Orange' = 'orange',
|
||||
}
|
||||
|
||||
export const overflowWrapOptions = [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'break-word', label: 'Break Word' },
|
||||
|
|
@ -150,21 +136,6 @@ export const customValueTypeOptions = [
|
|||
{ value: 5, label: 'Sequence' },
|
||||
]
|
||||
|
||||
export const gridColumnCopyFieldTypeOptions = [
|
||||
{ value: 0, label: 'All' },
|
||||
{ value: 1, label: 'General' },
|
||||
{ value: 2, label: 'Filter' },
|
||||
{ value: 3, label: 'Header' },
|
||||
{ value: 4, label: 'Group' },
|
||||
{ value: 5, label: 'Customization' },
|
||||
{ value: 6, label: 'GroupSummary' },
|
||||
{ value: 7, label: 'TotalSummary' },
|
||||
{ value: 8, label: 'Editing' },
|
||||
{ value: 9, label: 'Lookup' },
|
||||
{ value: 10, label: 'Validation' },
|
||||
{ value: 11, label: 'Styling' },
|
||||
]
|
||||
|
||||
export const itemTypeOptions = [
|
||||
{ value: 'button', label: 'Button' },
|
||||
{ value: 'empty', label: 'Empty' },
|
||||
|
|
@ -180,18 +151,6 @@ export const urlOptions = [
|
|||
{ value: '_top', label: 'Top' },
|
||||
]
|
||||
|
||||
export const joinConditionOptions = [
|
||||
{ value: 'AND', label: 'AND' },
|
||||
{ value: 'OR', label: 'OR' },
|
||||
]
|
||||
|
||||
export type listFormCustomizationType = 'customization' | 'detail' | ''
|
||||
|
||||
export const columnLookupTagBoxApplyValueModeListOptions = [
|
||||
{ value: 'instantly', label: 'Instantly' },
|
||||
{ value: 'useButtons', label: 'Use Buttons' },
|
||||
]
|
||||
|
||||
export const columnEditorTypeListOptions = [
|
||||
{ value: 'dxAutocomplete', label: 'dxAutocomplete' },
|
||||
{ value: 'dxCalendar', label: 'dxCalendar' },
|
||||
|
|
@ -527,7 +486,6 @@ export const dbSourceTypeOptions = enumToList(DbTypeEnum)
|
|||
export const dataSourceTypeOptions = enumToList(DataSourceTypeEnum)
|
||||
export const selectCommandTypeOptions = enumToList(SelectCommandTypeEnum)
|
||||
export const listFormCustomizationOptions = enumToList(ListFormCustomizationTypeEnum)
|
||||
export const authorizationTypeOptions = enumToList(AuthorizationTypeEnum)
|
||||
export const sortDirectionOptions = enumToList<string>(SortDirectionEnum)
|
||||
export const fixedPositionOptions = enumToList<string>(FixedPositionEnum)
|
||||
export const columnSummaryTypeListOptions = enumToList<string>(SummaryTypeEnum)
|
||||
|
|
@ -546,7 +504,6 @@ export const cascadeFilterOperator = enumToList<string>(ColumnCascadeFilterOpera
|
|||
export const pivotSettingsAreaOptions = enumToList<string>(PivotSettingsAreaEnum)
|
||||
export const pivotSettingsGroupIntervalOptions = enumToList<string>(PivotSettingsGroupIntervalEnum)
|
||||
export const pivotSortDirectionOptions = enumToList<string>(ColumnSortDirectionEnum)
|
||||
export const widgetColorOptions = enumToList<string>(WidgetColorEnum)
|
||||
// Tema renk haritası (border ve fill)
|
||||
const styleColorMap: Record<
|
||||
string,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
export type ChartOperation = '' | 'select' | 'insert' | 'update' | 'delete'
|
||||
export type ChartDialogType = '' | 'pane' | 'serie' | 'annotation' | 'axis'
|
||||
export type ListViewLayoutType =
|
||||
| 'grid'
|
||||
| 'card'
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import {
|
|||
import * as Yup from 'yup'
|
||||
import { ProfileDto, UpdateProfileDto } from '@/proxy/account/models'
|
||||
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 { useSetting } from '@/utils/hooks/useSetting'
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
putUserLookout,
|
||||
putUserPermission,
|
||||
} 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 dayjs from 'dayjs'
|
||||
import { Field, FieldArray, FieldProps, Form, Formik, FormikHelpers } from 'formik'
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
FaArrowLeft,
|
||||
FaBolt,
|
||||
FaCheckCircle,
|
||||
FaClone,
|
||||
FaArrowDown,
|
||||
FaArrowUp,
|
||||
FaChevronDown,
|
||||
|
|
@ -41,6 +42,8 @@ import { getListForms } from '@/services/admin/list-form.service'
|
|||
import { developerKitService } from '@/services/developerKit.service'
|
||||
import { Button, Notification, Select, toast } from '@/components/ui'
|
||||
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 {
|
||||
beginDesignerDragSoon,
|
||||
|
|
@ -843,6 +846,20 @@ const parseGeneratedJsxProps = (
|
|||
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 = (
|
||||
code: string | undefined,
|
||||
definitions: Map<string, DesignerComponentDefinition>,
|
||||
|
|
@ -1475,7 +1492,7 @@ const VisualComponentDesigner = () => {
|
|||
? null
|
||||
: parseDesignerDocumentFromCode(component.code)
|
||||
const migratedCodeDocument =
|
||||
hasStoredDesignerNodes || codeBackupDocument
|
||||
hasStoredDesignerNodes || codeBackupDocument || !isDesignerGeneratedCode(component.code)
|
||||
? null
|
||||
: migrateGeneratedCodeToDesignerDocument(component.code, catalogByName)
|
||||
const visualDocument = hasStoredDesignerNodes
|
||||
|
|
@ -1488,7 +1505,8 @@ const VisualComponentDesigner = () => {
|
|||
setManualCode(component.code || '')
|
||||
setSavedCode(component.code || '')
|
||||
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)
|
||||
undoStack.current = []
|
||||
redoStack.current = []
|
||||
|
|
@ -1587,6 +1605,22 @@ const VisualComponentDesigner = () => {
|
|||
[document.nodes, selectedId],
|
||||
)
|
||||
const selectedDefinition = selectedNode ? catalogByName.get(selectedNode.type) : undefined
|
||||
/**
|
||||
* Ok tuslari kardesler arasinda yer degistirir; sinira gelindiginde dugum bir ust
|
||||
* seviyeye tasinir. Yalnizca kok seviyedeki ilk/son dugumun gidecegi yer yoktur.
|
||||
*/
|
||||
const selectedMoveRange = useMemo(() => {
|
||||
if (!selectedNode) return { up: false, down: false }
|
||||
const ancestors = findDesignerAncestors(document.nodes, selectedNode.id)
|
||||
const parent = ancestors?.[ancestors.length - 1]
|
||||
const siblings = parent ? parent.children : document.nodes
|
||||
const index = siblings.findIndex((node) => node.id === selectedNode.id)
|
||||
|
||||
return {
|
||||
up: Boolean(parent) || index > 0,
|
||||
down: Boolean(parent) || index < siblings.length - 1,
|
||||
}
|
||||
}, [document.nodes, selectedNode])
|
||||
/** Every addressable component of the page, keyed by ref. */
|
||||
const documentRefs = useMemo(() => collectDesignerRefs(document.nodes), [document.nodes])
|
||||
|
||||
|
|
@ -2804,10 +2838,14 @@ const VisualComponentDesigner = () => {
|
|||
[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 names = new Set<string>()
|
||||
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]
|
||||
}, [document.nodes])
|
||||
|
|
@ -5269,10 +5307,7 @@ const VisualComponentDesigner = () => {
|
|||
onDropComponent={addComponent}
|
||||
onDropComponentBeside={addComponentBeside}
|
||||
onMoveIntoContainer={moveNodeIntoContainerNode}
|
||||
onMove={moveNode}
|
||||
onReorder={reorderNode}
|
||||
onDuplicate={duplicateNode}
|
||||
onDelete={deleteNode}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
@ -5372,7 +5407,7 @@ const VisualComponentDesigner = () => {
|
|||
)}
|
||||
</div>
|
||||
<p className="truncate text-[11px] text-slate-500">
|
||||
Visual Component Designer · {component.routePath}
|
||||
Visual Component Designer{component.routePath ? ` · ${component.routePath}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5406,21 +5441,25 @@ const VisualComponentDesigner = () => {
|
|||
>
|
||||
{translate('::App.Platform.Reset')}
|
||||
</Button>
|
||||
<Button
|
||||
color="green-600"
|
||||
icon={<FaEye />}
|
||||
size="sm"
|
||||
title={translate('::App.DeveloperKitComponentDesigner.OpenInNewTab')}
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
const routePath = component.routePath.startsWith('/')
|
||||
? component.routePath
|
||||
: `/${component.routePath}`
|
||||
window.open(routePath, '_blank')
|
||||
}}
|
||||
>
|
||||
{translate('::App.Listform.ListformField.Preview')}
|
||||
</Button>
|
||||
{/* Rotasi olmayan bilesen (baska bir ekranin icinde cizilen alt bilgi gibi)
|
||||
tek basina acilamaz; onizleme dugmesi yalnizca rotasi olanda cikar. */}
|
||||
{component.routePath?.trim() ? (
|
||||
<Button
|
||||
color="green-600"
|
||||
icon={<FaEye />}
|
||||
size="sm"
|
||||
title={translate('::App.DeveloperKitComponentDesigner.OpenInNewTab')}
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
const routePath = component.routePath.startsWith('/')
|
||||
? component.routePath
|
||||
: `/${component.routePath}`
|
||||
window.open(routePath, '_blank')
|
||||
}}
|
||||
>
|
||||
{translate('::App.Listform.ListformField.Preview')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
loading={isSaving}
|
||||
icon={<FaRegSave />}
|
||||
|
|
@ -5602,7 +5641,13 @@ const VisualComponentDesigner = () => {
|
|||
className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-gray-900 dark:text-gray-100"
|
||||
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>
|
||||
) : workspaceTab === 'data' ? (
|
||||
<div className="w-full space-y-4">
|
||||
|
|
@ -5932,18 +5977,59 @@ const VisualComponentDesigner = () => {
|
|||
<div className="text-[10px] uppercase tracking-wider text-slate-400">
|
||||
Selected component
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-between">
|
||||
<strong className="text-sm text-slate-800 dark:text-white">
|
||||
{/*
|
||||
Tasi/kopyala/sil kanvastaki secim seridinden buraya tasindi: serit
|
||||
kucuk bir ikonun ya da ayirici cizginin ustunu tamamen ortuyordu.
|
||||
Kanvasta yalnizca kose tutamagi kaldi.
|
||||
*/}
|
||||
<div className="mt-1 flex items-center justify-between gap-2">
|
||||
<strong className="truncate text-sm text-slate-800 dark:text-white">
|
||||
{selectedNode.type}
|
||||
</strong>
|
||||
<button
|
||||
className="rounded p-1.5 text-red-500 hover:bg-red-50"
|
||||
title={translate('::App.ComponentAction.Delete')}
|
||||
type="button"
|
||||
onClick={() => deleteNode(selectedNode.id)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<button
|
||||
className="rounded p-1.5 text-slate-500 enabled:hover:bg-slate-200 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-300 dark:enabled:hover:bg-gray-800"
|
||||
disabled={!selectedMoveRange.up}
|
||||
title={translate(
|
||||
selectedMoveRange.up
|
||||
? '::App.Platform.MoveUp'
|
||||
: '::App.VisualDesignerCanvas.AlreadyAtTop',
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => moveNode(selectedNode.id, -1)}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-1.5 text-slate-500 enabled:hover:bg-slate-200 disabled:cursor-not-allowed disabled:opacity-40 dark:text-gray-300 dark:enabled:hover:bg-gray-800"
|
||||
disabled={!selectedMoveRange.down}
|
||||
title={translate(
|
||||
selectedMoveRange.down
|
||||
? '::App.Platform.MoveDown'
|
||||
: '::App.VisualDesignerCanvas.AlreadyAtBottom',
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => moveNode(selectedNode.id, 1)}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-1.5 text-slate-500 hover:bg-slate-200 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
title={translate('::App.Platform.Duplicate')}
|
||||
type="button"
|
||||
onClick={() => duplicateNode(selectedNode.id)}
|
||||
>
|
||||
<FaClone />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-1.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
|
||||
title={translate('::App.ComponentAction.Delete')}
|
||||
type="button"
|
||||
onClick={() => deleteNode(selectedNode.id)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<code className="text-[10px] text-slate-400">{selectedNode.id}</code>
|
||||
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -7,7 +7,7 @@ import { BlogCategory, BlogPost } from '@/proxy/blog/blog'
|
|||
import { blogService } from '@/services/blog.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Button } from '@/components/ui'
|
||||
import { translateLabel } from './designer'
|
||||
import { translateLabel } from './localization'
|
||||
import { PublicPageLoader } from './shared'
|
||||
import Input from '@/components/ui/Input'
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { blogService } from '@/services/blog.service'
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { translateLabel } from './designer'
|
||||
import { translateLabel } from './localization'
|
||||
import { PublicPageLoader } from './shared'
|
||||
|
||||
const CenteredMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -14,7 +14,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import { createDemoAsync } from '@/services/demo.service'
|
||||
import { DemoDto } from '@/proxy/demo/models'
|
||||
import { Button, Notification, toast } from '@/components/ui'
|
||||
import { translateLabel } from './designer'
|
||||
import { translateLabel } from './localization'
|
||||
import Input from '@/components/ui/Input'
|
||||
|
||||
interface DemoModalProps {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}))
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
13
ui/src/views/public/localization.ts
Normal file
13
ui/src/views/public/localization.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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]' : ''
|
||||
}`
|
||||
}
|
||||
|
|
@ -6,7 +6,18 @@ const safeListFile = 'safelist.txt'
|
|||
const SAFELIST_COLORS = 'colors'
|
||||
|
||||
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: [
|
||||
{
|
||||
// Covers most utility groups used by designer-entered class strings.
|
||||
|
|
|
|||
|
|
@ -1,17 +1,112 @@
|
|||
const fs = require('fs')
|
||||
const nodePath = require('path')
|
||||
const plugin = require('tailwindcss/plugin')
|
||||
const generator = require('./generator')
|
||||
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')
|
||||
const currentSafeList = fs.readFileSync(path).toString()
|
||||
/** Seed kokunun `ui/` klasorune gore konumu; calisma dizininden bagimsiz cozulur. */
|
||||
const SEEDS_ROOT = nodePath.resolve(__dirname, '../../configs/seeds')
|
||||
|
||||
const hash = crypto.createHash('md5').update(JSON.stringify(safeList)).digest('hex')
|
||||
const prevHash = crypto.createHash('md5').update(JSON.stringify(currentSafeList)).digest('hex')
|
||||
const addTokens = (value, classes) =>
|
||||
String(value)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.forEach((token) => classes.add(token))
|
||||
|
||||
if (hash !== prevHash) {
|
||||
return fs.writeFileSync(path, safeList)
|
||||
const collectFromNodes = (nodes, classes) => {
|
||||
if (!Array.isArray(nodes)) return
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const [name, value] of Object.entries((node && node.props) || {})) {
|
||||
if (typeof value === 'string' && /class(Name)?$/i.test(name)) addTokens(value, classes)
|
||||
}
|
||||
collectFromNodes(node && node.children, classes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const collectFromRow = (row, classes) => {
|
||||
try {
|
||||
const props = row && row.Props ? JSON.parse(row.Props) : null
|
||||
collectFromNodes(props && props.visualDesigner && props.visualDesigner.nodes, classes)
|
||||
} catch {
|
||||
// Tasarimci belgesi olmayan (elle yazilmis) bilesen; siniflari asagida kodundan gelir.
|
||||
}
|
||||
|
||||
const code = String((row && row.Code) || '')
|
||||
const pattern = /className=\{?["'`]([^"'`]+)["'`]/g
|
||||
let match = pattern.exec(code)
|
||||
while (match) {
|
||||
addTokens(match[1], classes)
|
||||
match = pattern.exec(code)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Component'lerin kullandigi siniflar. Bilesen kodu veritabaninda yasar; derlemede
|
||||
* taranabilen tek kaynak seed dosyasidir ve o dosya `ui/` disindadir. Derleme baglami depo kokunu
|
||||
* gormedigin de (docker, ayri checkout) seed'e ozgu siniflar CSS'e hic girmez ve uygulama sunucuda
|
||||
* stilsiz kalir. Siniflar safelist'e yazilir; dosya depoya islendigi icin her ortamda taranir.
|
||||
*/
|
||||
const collectComponentClasses = () => {
|
||||
if (!fs.existsSync(SEEDS_ROOT)) return []
|
||||
|
||||
const files = fs
|
||||
.readdirSync(SEEDS_ROOT, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) =>
|
||||
nodePath.join(SEEDS_ROOT, entry.name, 'data/App.DeveloperKit.CustomComponents.json'),
|
||||
)
|
||||
.filter((file) => fs.existsSync(file))
|
||||
|
||||
const classes = new Set()
|
||||
for (const file of files) {
|
||||
try {
|
||||
const seed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
for (const row of seed.Rows || []) collectFromRow(row, classes)
|
||||
} catch {
|
||||
// Bozuk bir seed dosyasi derlemeyi durdurmaz; diger kapsamlar islenir.
|
||||
}
|
||||
}
|
||||
|
||||
return [...classes].sort()
|
||||
}
|
||||
|
||||
module.exports = plugin.withOptions(
|
||||
({ path = 'safelist.txt', patterns = [] }) =>
|
||||
({ theme }) => {
|
||||
// Yol calisma dizinine gore cozulurse depo kokunden calistirilan her arac ikinci bir
|
||||
// safelist dosyasi yaratir; eklentinin konumuna sabitlenir, tek dosya kalir.
|
||||
const safeListPath = nodePath.isAbsolute(path)
|
||||
? path
|
||||
: nodePath.resolve(__dirname, '..', path)
|
||||
const currentSafeList = fs.existsSync(safeListPath)
|
||||
? fs.readFileSync(safeListPath).toString()
|
||||
: ''
|
||||
const previousComponents = currentSafeList.split(COMPONENT_MARKER)[1] || ''
|
||||
|
||||
const seedClasses = collectComponentClasses()
|
||||
const components = seedClasses.length
|
||||
? seedClasses
|
||||
: previousComponents.split('\n').filter(Boolean)
|
||||
|
||||
const safeList = [...generator(theme)(patterns), COMPONENT_MARKER, ...components].join(
|
||||
'\n',
|
||||
)
|
||||
|
||||
const hash = crypto.createHash('md5').update(JSON.stringify(safeList)).digest('hex')
|
||||
const prevHash = crypto
|
||||
.createHash('md5')
|
||||
.update(JSON.stringify(currentSafeList))
|
||||
.digest('hex')
|
||||
|
||||
if (hash !== prevHash) {
|
||||
return fs.writeFileSync(safeListPath, safeList)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue