diff --git a/App_GlobalResources/Resource.Designer.cs b/App_GlobalResources/Resource.Designer.cs index 10b21dc..4e53424 100644 --- a/App_GlobalResources/Resource.Designer.cs +++ b/App_GlobalResources/Resource.Designer.cs @@ -105,6 +105,33 @@ namespace atakanozbancom.App_GlobalResources { } } + /// + /// Looks up a localized string similar to Affiliate links will be listed here soon.. + /// + public static string AffiliateLinksComingSoon { + get { + return ResourceManager.GetString("AffiliateLinksComingSoon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Support my work 💙. + /// + public static string AffiliateLinksTitle { + get { + return ResourceManager.GetString("AffiliateLinksTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If you'd like to support my work, you can use the links below. It costs money for me to run and maintain these services, and using them may earn me a small benefit at no extra cost to you.. + /// + public static string AffiliateLinksIntro { + get { + return ResourceManager.GetString("AffiliateLinksIntro", resourceCulture); + } + } + /// /// Looks up a localized string similar to I m working hard and on these projects, and your support really motivates me. If you d like to support you can support by these links. /// @@ -133,11 +160,11 @@ namespace atakanozbancom.App_GlobalResources { } /// - /// Looks up a localized string similar to GitHub. + /// Looks up a localized string similar to Gitea. /// - public static string GitHub { + public static string Gitea { get { - return ResourceManager.GetString("GitHub", resourceCulture); + return ResourceManager.GetString("Gitea", resourceCulture); } } @@ -222,6 +249,15 @@ namespace atakanozbancom.App_GlobalResources { } } + /// + /// Looks up a localized string similar to Support my work 💙. + /// + public static string NavAffiliateLinks { + get { + return ResourceManager.GetString("NavAffiliateLinks", resourceCulture); + } + } + /// /// Looks up a localized string similar to Home. /// @@ -241,7 +277,7 @@ namespace atakanozbancom.App_GlobalResources { } /// - /// Looks up a localized string similar to Server Status. + /// Looks up a localized string similar to Service Status. /// public static string Server { get { diff --git a/App_GlobalResources/Resource.resx b/App_GlobalResources/Resource.resx index c55a70e..e54de30 100644 --- a/App_GlobalResources/Resource.resx +++ b/App_GlobalResources/Resource.resx @@ -150,6 +150,18 @@ My Projects + + Support my work 💙 + + + Support my work 💙 + + + If you'd like to support my work, you can use the links below. It costs money for me to run and maintain these services, and using them may earn me a small benefit at no extra cost to you. + + + Affiliate links will be listed here soon. + Thank you @@ -171,13 +183,13 @@ Photography’s another passion of mine. I love traveling and capturing 360° photos to upload on Google Street View, making hidden places visible to the world. It’s like my own version of a vlog channel, just in pictures. - - GitHub + + Gitea page. - Server Status + Service Status \ No newline at end of file diff --git a/App_GlobalResources/Resource.tr-TR.resx b/App_GlobalResources/Resource.tr-TR.resx index 34c79bf..0412264 100644 --- a/App_GlobalResources/Resource.tr-TR.resx +++ b/App_GlobalResources/Resource.tr-TR.resx @@ -150,6 +150,18 @@ Projelerim + + Çalışmamı Destekleyin + + + Çalışmamı Destekleyin + + + Çalışmalarımı desteklemek isterseniz aşağıdaki linkleri kullanabilirsiniz. Bu servisleri çalıştırmak ve sürdürmek benim için maliyetli, bu linkleri kullanmanız size ekstra bir maliyet çıkarmadan bana küçük bir fayda sağlayabilir. + + + Affiliate linkleri yakında burada listelenecek. + Teşekkür ederim @@ -171,13 +183,13 @@ Fotoğrafçılık ise bir başka tutkum. Seyahat etmeyi ve 360° fotoğraflar çekmeyi seviyorum. Bu fotoğrafları Google Street View’a yükleyerek dünyanın görülmeyen yerlerini görünür kılmayı hedefliyorum. Benim için bu, görsel bir vlog kanalı gibi. - - GitHub + + Gitea sayfamı ziyaret edebilirsiniz. - Sunucu Durumu + Servis Durumu \ No newline at end of file diff --git a/App_Start/RouteConfig.cs b/App_Start/RouteConfig.cs index 6606132..6f7e4cd 100644 --- a/App_Start/RouteConfig.cs +++ b/App_Start/RouteConfig.cs @@ -13,13 +13,21 @@ namespace atakanozbancom { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); + // Keep old /affiliate URLs working + routes.MapRoute( + name: "AffiliateRedirect", + url: "affiliate/{action}/{id}", + defaults: new { controller = "support", action = "Index", id = UrlParameter.Optional }, + new[] { "atakanozbancom.Controllers" } + ); + routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", - defaults: new { controller = "Main", action = "Index", id = UrlParameter.Optional }, + defaults: new { controller = "main", action = "Index", id = UrlParameter.Optional }, new[] { "atakanozbancom.Controllers" } ); } } -} +} \ No newline at end of file diff --git a/Content/uploads/affiliate-logos/d02c597beea14b2b89e1fffdae8e32f7.png b/Content/uploads/affiliate-logos/d02c597beea14b2b89e1fffdae8e32f7.png new file mode 100644 index 0000000..20d8e5b Binary files /dev/null and b/Content/uploads/affiliate-logos/d02c597beea14b2b89e1fffdae8e32f7.png differ diff --git a/Content/uploads/affiliate-logos/e22ca27172c044d2ae017569d01ad1f4.png b/Content/uploads/affiliate-logos/e22ca27172c044d2ae017569d01ad1f4.png new file mode 100644 index 0000000..1d63088 Binary files /dev/null and b/Content/uploads/affiliate-logos/e22ca27172c044d2ae017569d01ad1f4.png differ diff --git a/Content/uploads/projects-media/2a158b008da04f09819d65f0dee7bbb4.png b/Content/uploads/projects-media/2a158b008da04f09819d65f0dee7bbb4.png new file mode 100644 index 0000000..f9458a7 Binary files /dev/null and b/Content/uploads/projects-media/2a158b008da04f09819d65f0dee7bbb4.png differ diff --git a/Content/uploads/projects-media/37fab4cfb93441cbbc04878bac60c1ec.png b/Content/uploads/projects-media/37fab4cfb93441cbbc04878bac60c1ec.png new file mode 100644 index 0000000..30050ae Binary files /dev/null and b/Content/uploads/projects-media/37fab4cfb93441cbbc04878bac60c1ec.png differ diff --git a/Content/uploads/projects-media/41853c667bb34ebf9e90ed7f1d2334ba.png b/Content/uploads/projects-media/41853c667bb34ebf9e90ed7f1d2334ba.png new file mode 100644 index 0000000..b2e105c Binary files /dev/null and b/Content/uploads/projects-media/41853c667bb34ebf9e90ed7f1d2334ba.png differ diff --git a/Content/uploads/projects-media/54e27917b1f345aab3eb29bc9b8fac53.png b/Content/uploads/projects-media/54e27917b1f345aab3eb29bc9b8fac53.png new file mode 100644 index 0000000..af0822e Binary files /dev/null and b/Content/uploads/projects-media/54e27917b1f345aab3eb29bc9b8fac53.png differ diff --git a/Content/uploads/projects-media/63ba179cd4b64fecbbd4bd9927b40723.png b/Content/uploads/projects-media/63ba179cd4b64fecbbd4bd9927b40723.png new file mode 100644 index 0000000..31a67de Binary files /dev/null and b/Content/uploads/projects-media/63ba179cd4b64fecbbd4bd9927b40723.png differ diff --git a/Content/uploads/projects-media/83b70aafc2ac4ea886eb84096cad0fac.png b/Content/uploads/projects-media/83b70aafc2ac4ea886eb84096cad0fac.png new file mode 100644 index 0000000..7705a44 Binary files /dev/null and b/Content/uploads/projects-media/83b70aafc2ac4ea886eb84096cad0fac.png differ diff --git a/Content/uploads/projects-media/856476623d23456481b0f7732f7ce331.png b/Content/uploads/projects-media/856476623d23456481b0f7732f7ce331.png new file mode 100644 index 0000000..90b6ce2 Binary files /dev/null and b/Content/uploads/projects-media/856476623d23456481b0f7732f7ce331.png differ diff --git a/Content/uploads/projects-media/85e3cd4752c148008efd7d4fa1aa0308.png b/Content/uploads/projects-media/85e3cd4752c148008efd7d4fa1aa0308.png new file mode 100644 index 0000000..5af2edb Binary files /dev/null and b/Content/uploads/projects-media/85e3cd4752c148008efd7d4fa1aa0308.png differ diff --git a/Content/uploads/projects-media/c6c4adeafa7f45e98167bd78d0cd55b5.jpg b/Content/uploads/projects-media/c6c4adeafa7f45e98167bd78d0cd55b5.jpg new file mode 100644 index 0000000..36fc385 Binary files /dev/null and b/Content/uploads/projects-media/c6c4adeafa7f45e98167bd78d0cd55b5.jpg differ diff --git a/Content/uploads/projects-media/ccedc4a2f5ac44288338e583d82513cf.png b/Content/uploads/projects-media/ccedc4a2f5ac44288338e583d82513cf.png new file mode 100644 index 0000000..e639f68 Binary files /dev/null and b/Content/uploads/projects-media/ccedc4a2f5ac44288338e583d82513cf.png differ diff --git a/Content/uploads/projects-media/e0add95125c34c38b562625d7886ea9b.png b/Content/uploads/projects-media/e0add95125c34c38b562625d7886ea9b.png new file mode 100644 index 0000000..535140b Binary files /dev/null and b/Content/uploads/projects-media/e0add95125c34c38b562625d7886ea9b.png differ diff --git a/Content/uploads/projects-media/ee14a5bd1142452b98545a079c25f100.png b/Content/uploads/projects-media/ee14a5bd1142452b98545a079c25f100.png new file mode 100644 index 0000000..db9a9c1 Binary files /dev/null and b/Content/uploads/projects-media/ee14a5bd1142452b98545a079c25f100.png differ diff --git a/Content/uploads/projects/1a0180574e3448ceaac4d4f3c5c7fbcb.jpeg b/Content/uploads/projects/1a0180574e3448ceaac4d4f3c5c7fbcb.jpeg new file mode 100644 index 0000000..cf14583 Binary files /dev/null and b/Content/uploads/projects/1a0180574e3448ceaac4d4f3c5c7fbcb.jpeg differ diff --git a/Content/uploads/wallpaper/e94637564de340a598769ac78bb18218.jpg b/Content/uploads/wallpaper/e94637564de340a598769ac78bb18218.jpg new file mode 100644 index 0000000..2a3fe50 Binary files /dev/null and b/Content/uploads/wallpaper/e94637564de340a598769ac78bb18218.jpg differ diff --git a/Controllers/adminController.cs b/Controllers/adminController.cs index e5e3931..d175d7f 100644 --- a/Controllers/adminController.cs +++ b/Controllers/adminController.cs @@ -13,12 +13,22 @@ namespace atakanozbancom.Controllers private readonly Context db = new Context(); [Authorize] - public ActionResult Index() // shows what we have + public ActionResult Index() // admin dashboard landing page + { + ViewBag.MyProjectsCount = db.MyProjects.Count(); + ViewBag.AffiliateLinksCount = db.AffiliateLinks.Count(); + ViewBag.SocialIconsCount = db.SocialIcons.Count(); + return View(); + } + + [Authorize] + public ActionResult MyProjects() // shows what we have { var value = db.MyProjects.ToList(); return View(value); } + [Authorize] [HttpGet] public ActionResult myprojectsget(int id) { @@ -35,8 +45,6 @@ namespace atakanozbancom.Controllers var vm = new AdminMyProjectsEditVM { id = p.id, - image = p.image, - iframe = p.iframe, TitleEN = p.title, DescEN = p.description, @@ -45,63 +53,39 @@ namespace atakanozbancom.Controllers DescTR = tr != null ? tr.description : "" }; + // NEW: + vm.medias = db.projectmedia + .Where(m => m.project_id == p.id) + .OrderBy(m => m.sort_order) + .ThenBy(m => m.id) + .ToList(); + return View("myprojectsget", vm); } + + [Authorize] [HttpPost] [ValidateAntiForgeryToken] - public ActionResult myprojectsget(AdminMyProjectsEditVM vm, HttpPostedFileBase file) + public ActionResult myprojectsget(AdminMyProjectsEditVM vm) { if (!ModelState.IsValid) + { + vm.medias = db.projectmedia + .Where(m => m.project_id == vm.id) + .OrderBy(m => m.sort_order) + .ThenBy(m => m.id) + .ToList(); + return View("myprojectsget", vm); + } var p = db.MyProjects.FirstOrDefault(x => x.id == vm.id); if (p == null) return HttpNotFound(); - var imageUrl = p.image; - - if (file != null && file.ContentLength > 0) - { - try - { - var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); - var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".webp" }; - - if (!allowedExtensions.Contains(ext)) - { - ModelState.AddModelError("", "You can just upload these formats: '.jpg', '.jpeg', '.png', '.webp'"); - return View("myprojectsget", vm); - } - - var maxFileSize = 5 * 1024 * 1024; - if (file.ContentLength > maxFileSize) - { - ModelState.AddModelError("", "The size of file exceeds 5MB limit."); - return View("myprojectsget", vm); - } - - var uploadsRoot = Server.MapPath("~/Content/uploads/projects"); - if (!Directory.Exists(uploadsRoot)) - Directory.CreateDirectory(uploadsRoot); - - var fileName = Guid.NewGuid().ToString("N") + ext; - var filePath = Path.Combine(uploadsRoot, fileName); - file.SaveAs(filePath); - - imageUrl = "/Content/uploads/projects/" + fileName; - } - catch (Exception ex) - { - ModelState.AddModelError("", "An error occured while upload: " + ex.Message); - return View("myprojectsget", vm); - } - } - // EN -> base p.title = vm.TitleEN; p.description = vm.DescEN; - p.image = imageUrl; - p.iframe = vm.iframe; // TR -> translations var tr = db.myprojectstranslations @@ -121,76 +105,28 @@ namespace atakanozbancom.Controllers return RedirectToAction("myprojectsget", new { id = p.id }); } - public ActionResult myprojectsupdate(myprojects x) - { - var ag = db.MyProjects.Find(x.id); - ag.description = x.description; - ag.title = x.title; - ag.image = x.image; - ag.iframe = x.iframe; - db.SaveChanges(); - return RedirectToAction("Index"); - } + [Authorize] [HttpGet] public ActionResult newmppost() // newmppost = New My Projects Post { return View(new AdminMyProjectsEditVM()); } + [Authorize] [HttpPost] [ValidateAntiForgeryToken] - public ActionResult newmppost(AdminMyProjectsEditVM vm, HttpPostedFileBase file) + public ActionResult newmppost(AdminMyProjectsEditVM vm) { if (!ModelState.IsValid) return View(vm); - string imageUrl = vm.image; - - try - { - if (file != null && file.ContentLength > 0) - { - var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); - var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".webp" }; - - if (!allowedExtensions.Contains(ext)) - { - ModelState.AddModelError("", "You can just upload these formats: '.jpg', '.jpeg', '.png', '.webp'"); - return View(vm); - } - - var maxFileSize = 5 * 1024 * 1024; - if (file.ContentLength > maxFileSize) - { - ModelState.AddModelError("", "The size of file exceeds 5MB limit."); - return View(vm); - } - - var uploadsRoot = Server.MapPath("~/Content/uploads/projects"); - if (!Directory.Exists(uploadsRoot)) - Directory.CreateDirectory(uploadsRoot); - - var fileName = Guid.NewGuid().ToString("N") + ext; - var filePath = Path.Combine(uploadsRoot, fileName); - file.SaveAs(filePath); - - imageUrl = "/Content/uploads/projects/" + fileName; - } - } - catch (Exception ex) - { - ModelState.AddModelError("", "An errror occured while upload: " + ex.Message); - return View(vm); - } - // EN -> base table var newProject = new myprojects { title = vm.TitleEN, - description = vm.DescEN, - image = imageUrl, - iframe = vm.iframe + description = vm.DescEN + // image/iframe artık yok }; db.MyProjects.Add(newProject); @@ -201,17 +137,21 @@ namespace atakanozbancom.Controllers { project_id = newProject.id, culture = "tr", - title = vm.TitleTR, - description = vm.DescTR + title = vm.TitleTR ?? "", + description = vm.DescTR ?? "" }; db.myprojectstranslations.Add(tr); db.SaveChanges(); TempData["ok"] = "Project added successfully!"; - return RedirectToAction("Index"); + return RedirectToAction("myprojectsget", new { id = newProject.id }); // direkt edit sayfasına at, media eklesin } + + [Authorize] + [HttpPost] + [ValidateAntiForgeryToken] public ActionResult myprojectsdelete(int id) { var mpd = db.MyProjects.Find(id); // mpd = my projects delete @@ -220,42 +160,713 @@ namespace atakanozbancom.Controllers db.MyProjects.Remove(mpd); db.SaveChanges(); } - return RedirectToAction("Index"); + return RedirectToAction("MyProjects"); } - //[HttpPost] - //[ValidateAntiForgeryToken] - //public ActionResult EditProject(AdminMyProjectsEditVM vm) - //{ - // if (!ModelState.IsValid) - // { - // return View("myprojectsget", vm); - // } + [HttpPost] + [Authorize] + [ValidateAntiForgeryToken] + public ActionResult addmedia( + int projectId, + int mediaType, + int sortOrder = 0, + string iframeUrl = null, + HttpPostedFileBase mediaFile = null) + { + var type = (MediaType)mediaType; - // var p = db.MyProjects.FirstOrDefault(x => x.id == vm.id); - // if (p == null) return HttpNotFound(); + // 1) IFRAME -> URL + if (type == MediaType.Iframe) + { + if (string.IsNullOrWhiteSpace(iframeUrl)) + { + TempData["ok"] = "Iframe URL boş olamaz."; + return RedirectToAction("myprojectsget", new { id = projectId }); + } - // p.image = vm.image; - // p.iframe = vm.iframe; - // p.title = vm.TitleEN; - // p.description = vm.DescEN; + var trimmed = iframeUrl.Trim(); + if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var embedUri) + || (embedUri.Scheme != Uri.UriSchemeHttps && embedUri.Scheme != Uri.UriSchemeHttp)) + { + TempData["ok"] = "Iframe URL must be a valid http(s) address."; + return RedirectToAction("myprojectsget", new { id = projectId }); + } - // var tr = db.myprojectstranslations - // .FirstOrDefault(x => x.project_id == p.id && (x.culture == "tr" || x.culture == "tr-TR")); + var row = new projectmedia + { + project_id = projectId, + media_type = MediaType.Iframe, + url = trimmed, + sort_order = sortOrder + }; - // if (tr == null) - // { - // tr = new myprojectstranslation { project_id = p.id, culture = "tr" }; - // db.myprojectstranslations.Add(tr); - // } + db.projectmedia.Add(row); + db.SaveChanges(); - // tr.title = vm.TitleTR; - // tr.description = vm.DescTR; + TempData["ok"] = "Media eklendi!"; + return RedirectToAction("myprojectsget", new { id = projectId }); + } - // db.SaveChanges(); + // 2) IMAGE/VIDEO/DOCUMENT -> FILE + if (mediaFile == null || mediaFile.ContentLength <= 0) + { + TempData["ok"] = "Dosya seçmelisin."; + return RedirectToAction("myprojectsget", new { id = projectId }); + } + + var ext = Path.GetExtension(mediaFile.FileName)?.ToLowerInvariant() ?? ""; + + // Tip bazlı uzantı whitelist + string[] allowed; + switch (type) + { + case MediaType.Image: + allowed = new[] { ".jpg", ".jpeg", ".png", ".webp", ".gif" }; + break; + //case MediaType.Video: + // allowed = new[] { ".mp4", ".webm", ".mov" }; + // break; + //case MediaType.Document: + // allowed = new[] { ".pdf" }; + // break; + default: + TempData["ok"] = "Geçersiz media type."; + return RedirectToAction("myprojectsget", new { id = projectId }); + } + + if (!allowed.Contains(ext)) + { + TempData["ok"] = "Bu dosya tipi kabul edilmiyor: " + ext; + return RedirectToAction("myprojectsget", new { id = projectId }); + } + + // Upload path + var uploadsRoot = Server.MapPath("~/Content/uploads/projects-media"); + if (!Directory.Exists(uploadsRoot)) + Directory.CreateDirectory(uploadsRoot); + + var fileName = Guid.NewGuid().ToString("N") + ext; + var filePath = Path.Combine(uploadsRoot, fileName); + mediaFile.SaveAs(filePath); + + var publicUrl = "/Content/uploads/projects-media/" + fileName; + + var m = new projectmedia + { + project_id = projectId, + media_type = type, + url = publicUrl, + sort_order = sortOrder + }; + + db.projectmedia.Add(m); + db.SaveChanges(); + + TempData["ok"] = "Media eklendi!"; + return RedirectToAction("myprojectsget", new { id = projectId }); + } + + [Authorize] + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult deletemedia(int id, int projectId) + { + var m = db.projectmedia.FirstOrDefault(x => x.id == id && x.project_id == projectId); + if (m == null) + return RedirectToAction("myprojectsget", new { id = projectId }); + + // Dosya silme: sadece Iframe dışındakiler + URL bizim uploads klasörümüzse + if (m.media_type != MediaType.Iframe && !string.IsNullOrWhiteSpace(m.url)) + { + try + { + // Güvenlik: sadece bizim klasördeki dosyaları sil + var prefix = "/Content/uploads/projects-media/"; + if (m.url.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var relative = "~" + m.url; // ~/Content/uploads/... + var physicalPath = Server.MapPath(relative); + + if (System.IO.File.Exists(physicalPath)) + System.IO.File.Delete(physicalPath); + } + } + catch + { + // istersen log yazdırırız, şimdilik sessiz geçiyoruz + } + } + + db.projectmedia.Remove(m); + db.SaveChanges(); + + TempData["ok"] = "Media silindi."; + return RedirectToAction("myprojectsget", new { id = projectId }); + } + + // ================= AFFILIATE LINKS ================= + + // .svg intentionally excluded: SVGs can embed + diff --git a/Views/Shared/_PortfolioLayout.cshtml b/Views/Shared/_PortfolioLayout.cshtml index 0654816..808921b 100644 --- a/Views/Shared/_PortfolioLayout.cshtml +++ b/Views/Shared/_PortfolioLayout.cshtml @@ -1,6 +1,4 @@ -@using Resources - -@{ +@{ Layout = null; } @@ -178,6 +176,8 @@ } + @Html.Action("wallpaper", "main") + @@ -196,9 +196,10 @@

Atakan

diff --git a/Views/_ViewStart.cshtml b/Views/_ViewStart.cshtml new file mode 100644 index 0000000..a72e945 --- /dev/null +++ b/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Views/Shared/_PortfolioLayout.cshtml"; +} diff --git a/Views/about/index.cshtml b/Views/about/index.cshtml index bfe8b44..53d4d95 100644 --- a/Views/about/index.cshtml +++ b/Views/about/index.cshtml @@ -1,20 +1,18 @@ -@using Resources - -@{ +@{ ViewBag.Title = "index"; Layout = "~/Views/Shared/_PortfolioLayout.cshtml"; }
-

@Resource.AboutMeTitle

+

@atakanozbancom.App_GlobalResources.Resource.AboutMeTitle

- @Resource.About1 + @atakanozbancom.App_GlobalResources.Resource.About1

- @Resource.About2 + @atakanozbancom.App_GlobalResources.Resource.About2

- @Resource.About3 + @atakanozbancom.App_GlobalResources.Resource.About3

- @Resource.About4 + @atakanozbancom.App_GlobalResources.Resource.About4

diff --git a/Views/admin/affiliatelinkget.cshtml b/Views/admin/affiliatelinkget.cshtml new file mode 100644 index 0000000..af2effb --- /dev/null +++ b/Views/admin/affiliatelinkget.cshtml @@ -0,0 +1,61 @@ +@model atakanozbancom.Models.classes.AdminAffiliateLinkVM +@{ + ViewBag.Title = "affiliatelinkget"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + +

Edit Affiliate Link

+@if (TempData["ok"] != null) +{
@TempData["ok"]
} +
+ +@using (Html.BeginForm("affiliatelinkget", "admin", FormMethod.Post, new { enctype = "multipart/form-data" })) +{ + @Html.AntiForgeryToken() + @Html.HiddenFor(m => m.id) + @Html.ValidationSummary(false, "", new { @class = "alert alert-danger" + (ViewData.ModelState.IsValid ? " d-none" : "") }) + +
+
+ @Model.TitleEN + + + Leave empty to keep the current logo. +
+ +
+ @Html.LabelFor(p => p.url, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.url, new { @class = "form-control bg-dark text-white" }) +
+ +
+ @Html.LabelFor(p => p.TitleEN, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.TitleEN, new { @class = "form-control bg-dark text-white" }) +
+ +
+ @Html.LabelFor(p => p.DescEN, new { @class = "text-white" }) + @Html.TextAreaFor(p => p.DescEN, new { @class = "form-control bg-dark text-white", rows = 2 }) +
+ +

Translation (TR)

+ +
+ @Html.LabelFor(m => m.TitleTR, new { @class = "text-white" }) + @Html.TextBoxFor(m => m.TitleTR, new { @class = "form-control bg-dark text-white" }) +
+ +
+ @Html.LabelFor(m => m.DescTR, new { @class = "text-white" }) + @Html.TextAreaFor(m => m.DescTR, new { @class = "form-control bg-dark text-white", rows = 2 }) +
+ +
+ @Html.LabelFor(p => p.sort_order, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.sort_order, new { @class = "form-control bg-dark text-white", type = "number" }) +
+ + +} diff --git a/Views/admin/affiliatelinks.cshtml b/Views/admin/affiliatelinks.cshtml new file mode 100644 index 0000000..64ec6e5 --- /dev/null +++ b/Views/admin/affiliatelinks.cshtml @@ -0,0 +1,48 @@ +@using atakanozbancom.Models.classes +@model List + +@{ + ViewBag.Title = "affiliatelinks"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + +

Affiliate Links

+@if (TempData["ok"] != null) +{
@TempData["ok"]
} + + + + + + + + + + + + @foreach (var x in Model) + { + + + + + + + + + + } + @if (!Model.Any()) + { + + + + } +
LogoTitleDescriptionURLOrderEditDelete
@x.title@x.title@x.description@x.url@x.sort_orderEdit + @using (Html.BeginForm("affiliatelinkdelete", "admin", new { id = x.id }, FormMethod.Post, new { onsubmit = "return confirm('Delete this affiliate link?');" })) + { + @Html.AntiForgeryToken() + + } +
No affiliate links yet.
+Create New Affiliate Link diff --git a/Views/admin/index.cshtml b/Views/admin/index.cshtml index 5a8b580..e709b94 100644 --- a/Views/admin/index.cshtml +++ b/Views/admin/index.cshtml @@ -1,33 +1,38 @@ -@using atakanozbancom.Models.classes -@model List - - -@{ +@{ ViewBag.Title = "index"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } -

Posts

- - - - - - - - - - @foreach (var x in Model) - { - - - - - - - - } - -
IDTitleDescriptionEditDelete
@x.id@x.title@x.descriptionEditDelete
-Create New Post +

Dashboard

+

Welcome back! Choose a section from the menu on the left to get started.

+
+
+
+

My Projects

+

@ViewBag.MyProjectsCount post(s) published.

+ Manage My Projects +
+
+
+
+

Affiliate Links

+

@ViewBag.AffiliateLinksCount link(s) published.

+ Manage Affiliate Links +
+
+
+
+

Social Icons

+

@ViewBag.SocialIconsCount icon(s) in the footer.

+ Manage Social Icons +
+
+
+
+

Wallpaper

+

Change the background image used across the site.

+ Manage Wallpaper +
+
+
diff --git a/Views/admin/myprojects.cshtml b/Views/admin/myprojects.cshtml new file mode 100644 index 0000000..b1bdc9c --- /dev/null +++ b/Views/admin/myprojects.cshtml @@ -0,0 +1,38 @@ +@using atakanozbancom.Models.classes +@model List + + +@{ + ViewBag.Title = "myprojects"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + +

My Projects

+ + + + + + + + + + @foreach (var x in Model) + { + + + + + + + + } + +
IDTitleDescriptionEditDelete
@x.id@x.title@x.descriptionEdit + @using (Html.BeginForm("myprojectsdelete", "admin", new { id = x.id }, FormMethod.Post, new { onsubmit = "return confirm('Delete this project?');" })) + { + @Html.AntiForgeryToken() + + } +
+Create New Post diff --git a/Views/admin/myprojectsget.cshtml b/Views/admin/myprojectsget.cshtml index eeedd63..bb0dd21 100644 --- a/Views/admin/myprojectsget.cshtml +++ b/Views/admin/myprojectsget.cshtml @@ -6,64 +6,138 @@ }

My Projects Edit Post

- @if (TempData["ok"] != null) -{ -
@TempData["ok"]
-} - +{
@TempData["ok"]
}
-@using (Html.BeginForm("myprojectsget", "admin", FormMethod.Post, new { enctype = "multipart/form-data" })) +@using (Html.BeginForm("myprojectsget", "admin", FormMethod.Post, + new { enctype = "multipart/form-data" })) { @Html.AntiForgeryToken() @Html.HiddenFor(m => m.id) -
- @Html.LabelFor(m => m.id, new { @class = "text-white" }) - @Html.TextBoxFor(m => m.id, new { @class = "form-control bg-dark text-white", @readonly = "readonly" }) -
+ @Html.LabelFor(x => x.id, new { @class = "text-white" }) + @Html.TextBoxFor(x => x.id, new { @class = "form-control bg-dark text-white", @readonly = "readonly" }) +
-
- @Html.LabelFor(m => m.TitleEN, new { @class = "text-white" }) - @Html.TextBoxFor(m => m.TitleEN, new { @class = "form-control bg-dark text-white required" }) -
+ @Html.LabelFor(p => p.TitleEN, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.TitleEN, new { @class = "form-control bg-dark text-white required" }) +
-
- @Html.LabelFor(m => m.DescEN, new { @class = "text-white" }) - @Html.TextAreaFor(m => m.DescEN, new { @class = "form-control bg-dark text-white required", rows = "6" }) -
+ @Html.LabelFor(p => p.DescEN, new { @class = "text-white" }) + @Html.TextAreaFor(p => p.DescEN, new { @class = "form-control bg-dark text-white required" }) + To add a link, use: [Link text](https://example.com) +
- if (!string.IsNullOrEmpty(Model.image)) - { -
- -
- -
- } - -
- - -
- -
- @Html.LabelFor(m => m.iframe, new { @class = "text-white" }) - @Html.TextBoxFor(m => m.iframe, new { @class = "form-control bg-dark text-white" }) -
- -

Translation (TR)

- -
- @Html.LabelFor(m => m.TitleTR, new { @class = "text-white" }) - @Html.TextBoxFor(m => m.TitleTR, new { @class = "form-control bg-dark text-white required" }) -
- -
- @Html.LabelFor(m => m.DescTR, new { @class = "text-white" }) - @Html.TextAreaFor(m => m.DescTR, new { @class = "form-control bg-dark text-white required", rows = "6" }) -
+

Translation

+ @Html.LabelFor(m => m.TitleTR, new { @class = "text-white" }) + @Html.TextBoxFor(m => m.TitleTR, new { @class = "form-control bg-dark text-white required" }) +
+ @Html.LabelFor(m => m.DescTR, new { @class = "text-white" }) + @Html.TextAreaFor(m => m.DescTR, new { @class = "form-control bg-dark text-white required" }) + To add a link, use: [Link text](https://example.com) +
-} \ No newline at end of file +} + +
+

Media Manager

+ +@if (Model.medias != null && Model.medias.Count > 0) +{ +
+ @foreach (var m in Model.medias) + { +
+
+ @m.media_type + (order: @m.sort_order) +
@m.url
+ @*@if (!string.IsNullOrWhiteSpace(m.title)) + { +
@m.title
+ }*@ +
+ + @using (Html.BeginForm("deletemedia", "admin", new { id = m.id, projectId = Model.id }, FormMethod.Post, new { onsubmit = "return confirm('Delete this media?');" })) + { + @Html.AntiForgeryToken() + + } +
+
+ } +
+} +else +{ +
No media for this project.
+} + +@using (Html.BeginForm("addmedia", "admin", FormMethod.Post, new { enctype = "multipart/form-data" })) +{ + @Html.AntiForgeryToken() + + +
+ + +
+ + + + + +
+ + + + Upload image/document files. +
+ +
+ + +
+ + +} + + + diff --git a/Views/admin/newaffiliatepost.cshtml b/Views/admin/newaffiliatepost.cshtml new file mode 100644 index 0000000..bd09881 --- /dev/null +++ b/Views/admin/newaffiliatepost.cshtml @@ -0,0 +1,59 @@ +@model atakanozbancom.Models.classes.AdminAffiliateLinkVM +@{ + ViewBag.Title = "Add New Affiliate Link"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + +

Add New Affiliate Link

+
+ +@using (Html.BeginForm("newaffiliatepost", "admin", FormMethod.Post, new { enctype = "multipart/form-data" })) +{ + @Html.AntiForgeryToken() + @Html.ValidationSummary(false, "", new { @class = "alert alert-danger" + (ViewData.ModelState.IsValid ? " d-none" : "") }) + +
+ + + + + PNG, JPG, WEBP or GIF. +
+ +
+ @Html.LabelFor(p => p.url, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.url, new { @class = "form-control bg-dark text-white", placeholder = "https://..." }) +
+ +
+ @Html.LabelFor(p => p.TitleEN, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.TitleEN, new { @class = "form-control bg-dark text-white", placeholder = "e.g. Hetzner" }) +
+ +
+ @Html.LabelFor(p => p.DescEN, new { @class = "text-white" }) + @Html.TextAreaFor(p => p.DescEN, new { @class = "form-control bg-dark text-white", rows = 2, placeholder = "e.g. Get $20 free credit" }) +
+ +

Translation (TR)

+ +
+ @Html.LabelFor(p => p.TitleTR, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.TitleTR, new { @class = "form-control bg-dark text-white" }) +
+ +
+ @Html.LabelFor(p => p.DescTR, new { @class = "text-white" }) + @Html.TextAreaFor(p => p.DescTR, new { @class = "form-control bg-dark text-white", rows = 2 }) +
+ +
+ @Html.LabelFor(p => p.sort_order, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.sort_order, new { @class = "form-control bg-dark text-white", type = "number" }) + Lower numbers show first. +
+ + +} diff --git a/Views/admin/newmppost.cshtml b/Views/admin/newmppost.cshtml index 99e4ceb..072ccae 100644 --- a/Views/admin/newmppost.cshtml +++ b/Views/admin/newmppost.cshtml @@ -22,16 +22,7 @@
@Html.LabelFor(p => p.DescEN, new { @class = "text-white" }) @Html.TextAreaFor(p => p.DescEN, new { @class = "form-control bg-dark text-white" }) -
- -
- - -
- -
- @Html.LabelFor(p => p.iframe, new { @class = "text-white" }) - @Html.TextBoxFor(p => p.iframe, new { @class = "form-control bg-dark text-white" }) + To add a link, use: [Link text](https://example.com)

Translation (TR)

@@ -44,7 +35,107 @@
@Html.LabelFor(p => p.DescTR, new { @class = "text-white" }) @Html.TextAreaFor(p => p.DescTR, new { @class = "form-control bg-dark text-white" }) + To add a link, use: [Link text](https://example.com)
-} \ No newline at end of file +} + +
+

Media Manager

+ +@if (Model.medias != null && Model.medias.Count > 0) +{ +
+ @foreach (var m in Model.medias) + { +
+
+ @m.media_type + (order: @m.sort_order) +
@m.url
+ @*@if (!string.IsNullOrWhiteSpace(m.title)) + { +
@m.title
+ }*@ +
+ + @using (Html.BeginForm("deletemedia", "admin", new { id = m.id, projectId = Model.id }, FormMethod.Post, new { onsubmit = "return confirm('Delete this media?');" })) + { + @Html.AntiForgeryToken() + + } +
+
+ } +
+} +else +{ +
No media for this project.
+} + +@using (Html.BeginForm("addmedia", "admin", FormMethod.Post, new { enctype = "multipart/form-data" })) +{ + @Html.AntiForgeryToken() + + +
+ + +
+ + + + + +
+ + + + Upload image/document files. +
+ +
+ + +
+ + +} + + \ No newline at end of file diff --git a/Views/admin/newsocialicon.cshtml b/Views/admin/newsocialicon.cshtml new file mode 100644 index 0000000..c84969e --- /dev/null +++ b/Views/admin/newsocialicon.cshtml @@ -0,0 +1,48 @@ +@model atakanozbancom.Models.classes.AdminSocialIconVM +@{ + ViewBag.Title = "Add New Social Icon"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + + + +

Add New Social Icon

+
+ +@using (Html.BeginForm("newsocialicon", "admin", FormMethod.Post)) +{ + @Html.AntiForgeryToken() + @Html.ValidationSummary(false, "", new { @class = "alert alert-danger" + (ViewData.ModelState.IsValid ? " d-none" : "") }) + +
+ @Html.LabelFor(p => p.icon, new { @class = "text-white" }) +
+ + + + @Html.TextBoxFor(p => p.icon, new { @class = "form-control bg-dark text-white", placeholder = "e.g. fa-brands fa-youtube", id = "icon-input" }) +
+ + Any Font Awesome class name, e.g. fa-brands fa-youtube or fa-solid fa-link. + +
+ +
+ @Html.LabelFor(p => p.url, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.url, new { @class = "form-control bg-dark text-white", placeholder = "https://..." }) +
+ +
+ @Html.LabelFor(p => p.sort_order, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.sort_order, new { @class = "form-control bg-dark text-white", type = "number" }) + Lower numbers show first. +
+ + +} + + diff --git a/Views/admin/security.cshtml b/Views/admin/security.cshtml new file mode 100644 index 0000000..7520802 --- /dev/null +++ b/Views/admin/security.cshtml @@ -0,0 +1,5 @@ +@{ + // Kept for old bookmarks; controller redirects to UserSettings. + Layout = null; + Response.Redirect(Url.Action("UserSettings", "admin")); +} diff --git a/Views/admin/socialiconget.cshtml b/Views/admin/socialiconget.cshtml new file mode 100644 index 0000000..6550e32 --- /dev/null +++ b/Views/admin/socialiconget.cshtml @@ -0,0 +1,50 @@ +@model atakanozbancom.Models.classes.AdminSocialIconVM +@{ + ViewBag.Title = "socialiconget"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + + + +

Edit Social Icon

+@if (TempData["ok"] != null) +{
@TempData["ok"]
} +
+ +@using (Html.BeginForm("socialiconget", "admin", FormMethod.Post)) +{ + @Html.AntiForgeryToken() + @Html.HiddenFor(m => m.id) + @Html.ValidationSummary(false, "", new { @class = "alert alert-danger" + (ViewData.ModelState.IsValid ? " d-none" : "") }) + +
+ @Html.LabelFor(p => p.icon, new { @class = "text-white" }) +
+ + + + @Html.TextBoxFor(p => p.icon, new { @class = "form-control bg-dark text-white", id = "icon-input" }) +
+ + Any Font Awesome class name, e.g. fa-brands fa-youtube or fa-solid fa-link. + +
+ +
+ @Html.LabelFor(p => p.url, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.url, new { @class = "form-control bg-dark text-white" }) +
+ +
+ @Html.LabelFor(p => p.sort_order, new { @class = "text-white" }) + @Html.TextBoxFor(p => p.sort_order, new { @class = "form-control bg-dark text-white", type = "number" }) +
+ + +} + + diff --git a/Views/admin/socialicons.cshtml b/Views/admin/socialicons.cshtml new file mode 100644 index 0000000..b91e37b --- /dev/null +++ b/Views/admin/socialicons.cshtml @@ -0,0 +1,49 @@ +@using atakanozbancom.Models.classes +@model List + +@{ + ViewBag.Title = "socialicons"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + + + +

Social Icons

+

These appear as the icon row at the bottom of every public page.

+@if (TempData["ok"] != null) +{
@TempData["ok"]
} + + + + + + + + + + + @foreach (var x in Model) + { + + + + + + + + + } + @if (!Model.Any()) + { + + + + } +
IconClassURLOrderEditDelete
@x.icon@x.url@x.sort_orderEdit + @using (Html.BeginForm("socialicondelete", "admin", new { id = x.id }, FormMethod.Post, new { onsubmit = "return confirm('Delete this icon?');" })) + { + @Html.AntiForgeryToken() + + } +
No social icons yet.
+Create New Icon diff --git a/Views/admin/usersettings.cshtml b/Views/admin/usersettings.cshtml new file mode 100644 index 0000000..b98ecc5 --- /dev/null +++ b/Views/admin/usersettings.cshtml @@ -0,0 +1,116 @@ +@{ + ViewBag.Title = "User Settings"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; + var enabled = ViewBag.TwoFactorEnabled == true; +} + +

User Settings

+

Account: @ViewBag.Username

+ +
+
+
+

Change password

+ + @if (TempData["pwdOk"] != null) + { +
@TempData["pwdOk"]
+ } + @if (TempData["pwdError"] != null) + { +
@TempData["pwdError"]
+ } + + @using (Html.BeginForm("ChangePassword", "admin", FormMethod.Post)) + { + @Html.AntiForgeryToken() +
+ + +
+
+ + + At least 8 characters. +
+
+ + +
+ + } +
+
+ +
+
+

+ Two-factor authentication + @if (enabled) + { + Enabled + } + else + { + Disabled + } +

+ + @if (TempData["tfaOk"] != null) + { +
@TempData["tfaOk"]
+ } + @if (TempData["tfaError"] != null) + { +
@TempData["tfaError"]
+ } + + @if (enabled) + { +

2FA is active. Enter a current authenticator code to disable it.

+ using (Html.BeginForm("DisableTwoFactor", "admin", FormMethod.Post)) + { + @Html.AntiForgeryToken() +
+ + +
+ + } + } + else + { +
    +
  1. Open your authenticator app and scan the QR code below.
  2. +
  3. Or enter this secret manually: @ViewBag.SetupSecret
  4. +
  5. Enter the 6-digit code to confirm and enable 2FA.
  6. +
+ +
+
+
+ + using (Html.BeginForm("EnableTwoFactor", "admin", FormMethod.Post)) + { + @Html.AntiForgeryToken() +
+ + +
+ + } + + + + } +
+
+
diff --git a/Views/admin/wallpaper.cshtml b/Views/admin/wallpaper.cshtml new file mode 100644 index 0000000..7be17d9 --- /dev/null +++ b/Views/admin/wallpaper.cshtml @@ -0,0 +1,37 @@ +@{ + ViewBag.Title = "wallpaper"; + Layout = "~/Views/Shared/_AdminLayout.cshtml"; +} + +

Wallpaper

+

This image is used as the background on every public page.

+@if (TempData["ok"] != null) +{
@TempData["ok"]
} + +
+ + +
+ +@using (Html.BeginForm("Wallpaper", "admin", FormMethod.Post, new { enctype = "multipart/form-data" })) +{ + @Html.AntiForgeryToken() + +
+ + + + + PNG, JPG, WEBP or GIF. +
+ + +} + +@using (Html.BeginForm("WallpaperReset", "admin", FormMethod.Post, new { onsubmit = "return confirm('Reset wallpaper to the default image?');" })) +{ + @Html.AntiForgeryToken() + +} diff --git a/Views/login/index.cshtml b/Views/login/index.cshtml index 5a02e9f..f8518fd 100644 --- a/Views/login/index.cshtml +++ b/Views/login/index.cshtml @@ -1,19 +1,15 @@ - -@{ +@{ Layout = null; } - Sign-in - - -
@using (Html.BeginForm("index", "login", FormMethod.Post)) { + @Html.AntiForgeryToken()

Please sign in

+ if (ViewBag.Error != null) + { +
@ViewBag.Error
+ } +
- +
-
- +
+
}
- - diff --git a/Views/login/verify.cshtml b/Views/login/verify.cshtml new file mode 100644 index 0000000..0d0d67f --- /dev/null +++ b/Views/login/verify.cshtml @@ -0,0 +1,50 @@ +@{ + Layout = null; +} + + + + + + + Two-factor authentication + + + + +
+ @using (Html.BeginForm("Verify", "login", FormMethod.Post)) + { + @Html.AntiForgeryToken() +

Two-factor authentication

+

Enter the 6-digit code from your authenticator app.

+ + if (ViewBag.Error != null) + { +
@ViewBag.Error
+ } + +
+ +
+ + + Back to sign in + } +
+ + diff --git a/Views/main/index.cshtml b/Views/main/index.cshtml index 70d05c9..282a4e9 100644 --- a/Views/main/index.cshtml +++ b/Views/main/index.cshtml @@ -1,13 +1,11 @@ -@using Resources - -@{ +@{ ViewBag.Title = "index"; Layout = "~/Views/Shared/_PortfolioLayout.cshtml"; }
-

@Resource.MainH1

+

@atakanozbancom.App_GlobalResources.Resource.MainH1

- @Resource.MainDiv + @atakanozbancom.App_GlobalResources.Resource.MainDiv
diff --git a/Views/main/socialicons.cshtml b/Views/main/socialicons.cshtml new file mode 100644 index 0000000..2d55f46 --- /dev/null +++ b/Views/main/socialicons.cshtml @@ -0,0 +1,9 @@ +@using atakanozbancom.Models.classes +@model List + +@foreach (var x in Model) +{ + + + +} diff --git a/Views/main/wallpaper.cshtml b/Views/main/wallpaper.cshtml new file mode 100644 index 0000000..a5d69cb --- /dev/null +++ b/Views/main/wallpaper.cshtml @@ -0,0 +1,5 @@ + diff --git a/Views/myprojects/index.cshtml b/Views/myprojects/index.cshtml index 55a95f2..607eead 100644 --- a/Views/myprojects/index.cshtml +++ b/Views/myprojects/index.cshtml @@ -1,11 +1,10 @@ -@using Resources -@{ +@{ ViewBag.Title = "index"; Layout = "~/Views/Shared/_PortfolioLayout.cshtml"; }
-

@Resource.MyProjectsTitle

+

@atakanozbancom.App_GlobalResources.Resource.MyProjectsTitle

@Html.Action("projectposts", "myprojects")
diff --git a/Views/myprojects/projectposts.cshtml b/Views/myprojects/projectposts.cshtml index 46a90b4..806829b 100644 --- a/Views/myprojects/projectposts.cshtml +++ b/Views/myprojects/projectposts.cshtml @@ -1,12 +1,11 @@ -@using Resources -@model IEnumerable +@model IEnumerable

- @(Resource.MyProjectsP1) - - @Resource.GitHub + @(atakanozbancom.App_GlobalResources.Resource.MyProjectsP1) + + @atakanozbancom.App_GlobalResources.Resource.Gitea - @Resource.MyProjectsP1_1 + @atakanozbancom.App_GlobalResources.Resource.MyProjectsP1_1

@foreach (var p in Model) { - var media = new List<(string Type, string Url)>(); - - if (!string.IsNullOrWhiteSpace(p.iframe)) - { - media.Add(("iframe", p.iframe)); - } - - if (!string.IsNullOrWhiteSpace(p.image)) - { - media.Add(("image", p.image)); - } - + var media = p.medias ?? new List(); var carouselId = $"projCarousel_{p.id}"; var hasMultiple = media.Count > 1; @@ -82,6 +160,22 @@ @if (media.Count > 0) {