From e543abb49ff13476bcf99ab8d6155668fc1276ef Mon Sep 17 00:00:00 2001 From: Manpreet Singh <166604315+Code-311@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:51:28 +0530 Subject: [PATCH] Polish editorial prose styling and normalize seeded markdown --- Areas/Admin/Controllers/EssaysController.cs | 23 + .../Controllers/FrameworkModelsController.cs | 23 + Areas/Admin/Controllers/HomeController.cs | 11 + .../Admin/Controllers/MediaItemsController.cs | 23 + .../Controllers/StaticPagesController.cs | 23 + .../Controllers/ToolQuestionsController.cs | 24 + Areas/Admin/Controllers/ToolsController.cs | 23 + .../Controllers/UploadedAssetsController.cs | 23 + Areas/Admin/ViewModels/EssayEditViewModel.cs | 17 + .../ViewModels/FrameworkModelEditViewModel.cs | 15 + Areas/Admin/ViewModels/LoginViewModel.cs | 16 + .../ViewModels/MediaItemEditViewModel.cs | 16 + .../ViewModels/StaticPageEditViewModel.cs | 12 + Areas/Admin/ViewModels/ToolEditViewModel.cs | 16 + .../ViewModels/ToolQuestionEditViewModel.cs | 12 + Areas/Admin/Views/Essays/Edit.cshtml | 2 + Areas/Admin/Views/Essays/Index.cshtml | 2 + Areas/Admin/Views/FrameworkModels/Edit.cshtml | 2 + .../Admin/Views/FrameworkModels/Index.cshtml | 2 + Areas/Admin/Views/Home/Index.cshtml | 1 + Areas/Admin/Views/MediaItems/Edit.cshtml | 2 + Areas/Admin/Views/MediaItems/Index.cshtml | 2 + Areas/Admin/Views/Shared/_Layout.cshtml | 1 + Areas/Admin/Views/StaticPages/Edit.cshtml | 2 + Areas/Admin/Views/StaticPages/Index.cshtml | 2 + Areas/Admin/Views/ToolQuestions/Edit.cshtml | 2 + Areas/Admin/Views/ToolQuestions/Index.cshtml | 2 + Areas/Admin/Views/Tools/Edit.cshtml | 2 + Areas/Admin/Views/Tools/Index.cshtml | 2 + .../Admin/Views/UploadedAssets/Create.cshtml | 1 + Areas/Admin/Views/UploadedAssets/Index.cshtml | 2 + Areas/Admin/Views/_ViewImports.cshtml | 4 + Areas/Admin/Views/_ViewStart.cshtml | 1 + Controllers/AccountController.cs | 42 ++ Controllers/AssetsController.cs | 18 + Controllers/HomeController.cs | 179 +------ Controllers/MediaController.cs | 21 + Controllers/ModelsController.cs | 28 ++ Controllers/PagesController.cs | 26 ++ Controllers/ToolsController.cs | 28 ++ Controllers/WritingsController.cs | 28 ++ Data/ApplicationDbContext.cs | 28 ++ Data/Configurations/EssayConfiguration.cs | 18 + .../FrameworkModelConfiguration.cs | 17 + Data/Configurations/MediaItemConfiguration.cs | 17 + .../SiteSettingConfiguration.cs | 14 + .../Configurations/StaticPageConfiguration.cs | 15 + Data/Configurations/ToolConfiguration.cs | 18 + .../ToolQuestionConfiguration.cs | 15 + .../UploadedAssetConfiguration.cs | 15 + Data/Seed/DatabaseSeeder.cs | 246 ++++++++++ Models/ArchiveModule.cs | 10 - Models/Domain/EntityBase.cs | 8 + Models/Domain/Essay.cs | 16 + Models/Domain/FrameworkModel.cs | 14 + Models/Domain/MediaItem.cs | 16 + Models/Domain/PublishableEntityBase.cs | 8 + Models/Domain/SiteSetting.cs | 7 + Models/Domain/StaticPage.cs | 13 + Models/Domain/Tool.cs | 16 + Models/Domain/ToolQuestion.cs | 12 + Models/Domain/UploadedAsset.cs | 12 + Models/EssayItem.cs | 9 - Models/FeaturedWorkItem.cs | 11 - Models/Identity/ApplicationUser.cs | 8 + Models/MediaItem.cs | 9 - Models/ModelItem.cs | 9 - Models/ToolItem.cs | 8 - Models/WritingItem.cs | 10 - Program.cs | 54 ++- Services/Assets/AssetService.cs | 37 ++ Services/Assets/IAssetService.cs | 8 + Services/Markdown/IMarkdownRenderer.cs | 6 + Services/Markdown/MarkdownRenderer.cs | 15 + ViewModels/ContentDetailViewModel.cs | 9 - ViewModels/DetailPageViewModel.cs | 6 + ViewModels/HomePageViewModel.cs | 16 +- ViewModels/ListPageViewModel.cs | 7 + ViewModels/MediaPageViewModel.cs | 10 - ViewModels/ModelsPageViewModel.cs | 10 - ViewModels/SimplePageViewModel.cs | 8 - ViewModels/StaticPageViewModel.cs | 8 + ViewModels/ToolsPageViewModel.cs | 10 - ViewModels/WritingsPageViewModel.cs | 10 - Views/Account/Login.cshtml | 10 + Views/Home/About.cshtml | 17 - Views/Home/Contact.cshtml | 17 - Views/Home/ContentDetail.cshtml | 17 - Views/Home/Index.cshtml | 76 ++- Views/Home/Media.cshtml | 24 - Views/Home/Models.cshtml | 21 - Views/Home/Tools.cshtml | 20 - Views/Home/Writings.cshtml | 24 - Views/Media/Index.cshtml | 17 + Views/Models/Detail.cshtml | 11 + Views/Models/Index.cshtml | 13 + Views/Pages/Page.cshtml | 6 + Views/Shared/_AboutPreview.cshtml | 9 - Views/Shared/_AboutSection.cshtml | 16 - Views/Shared/_ArchiveIndex.cshtml | 26 -- Views/Shared/_ConceptSection.cshtml | 9 - Views/Shared/_ContactPreview.cshtml | 9 - Views/Shared/_ContactSection.cshtml | 12 - Views/Shared/_FeaturedModels.cshtml | 17 - Views/Shared/_FeaturedWorks.cshtml | 25 - Views/Shared/_Footer.cshtml | 3 - Views/Shared/_Header.cshtml | 24 - Views/Shared/_Hero.cshtml | 11 - Views/Shared/_Layout.cshtml | 29 +- Views/Shared/_Manifesto.cshtml | 13 - Views/Shared/_MediaPreview.cshtml | 19 - Views/Shared/_SelectedWritings.cshtml | 20 - Views/Shared/_ThoughtsSection.cshtml | 20 - Views/Shared/_ToolsPreview.cshtml | 16 - Views/Shared/_WhyGapIntro.cshtml | 14 - Views/Tools/Detail.cshtml | 14 + Views/Tools/Index.cshtml | 14 + Views/Writings/Detail.cshtml | 11 + Views/Writings/Index.cshtml | 14 + Views/_ViewImports.cshtml | 2 +- appsettings.Development.json | 8 + appsettings.json | 15 + manpreetsingh.pro.csproj | 11 + wwwroot/css/site.css | 442 +++++++----------- wwwroot/img/Logo-Out.svg | 4 + wwwroot/js/site.js | 57 +-- 126 files changed, 1622 insertions(+), 1029 deletions(-) create mode 100644 Areas/Admin/Controllers/EssaysController.cs create mode 100644 Areas/Admin/Controllers/FrameworkModelsController.cs create mode 100644 Areas/Admin/Controllers/HomeController.cs create mode 100644 Areas/Admin/Controllers/MediaItemsController.cs create mode 100644 Areas/Admin/Controllers/StaticPagesController.cs create mode 100644 Areas/Admin/Controllers/ToolQuestionsController.cs create mode 100644 Areas/Admin/Controllers/ToolsController.cs create mode 100644 Areas/Admin/Controllers/UploadedAssetsController.cs create mode 100644 Areas/Admin/ViewModels/EssayEditViewModel.cs create mode 100644 Areas/Admin/ViewModels/FrameworkModelEditViewModel.cs create mode 100644 Areas/Admin/ViewModels/LoginViewModel.cs create mode 100644 Areas/Admin/ViewModels/MediaItemEditViewModel.cs create mode 100644 Areas/Admin/ViewModels/StaticPageEditViewModel.cs create mode 100644 Areas/Admin/ViewModels/ToolEditViewModel.cs create mode 100644 Areas/Admin/ViewModels/ToolQuestionEditViewModel.cs create mode 100644 Areas/Admin/Views/Essays/Edit.cshtml create mode 100644 Areas/Admin/Views/Essays/Index.cshtml create mode 100644 Areas/Admin/Views/FrameworkModels/Edit.cshtml create mode 100644 Areas/Admin/Views/FrameworkModels/Index.cshtml create mode 100644 Areas/Admin/Views/Home/Index.cshtml create mode 100644 Areas/Admin/Views/MediaItems/Edit.cshtml create mode 100644 Areas/Admin/Views/MediaItems/Index.cshtml create mode 100644 Areas/Admin/Views/Shared/_Layout.cshtml create mode 100644 Areas/Admin/Views/StaticPages/Edit.cshtml create mode 100644 Areas/Admin/Views/StaticPages/Index.cshtml create mode 100644 Areas/Admin/Views/ToolQuestions/Edit.cshtml create mode 100644 Areas/Admin/Views/ToolQuestions/Index.cshtml create mode 100644 Areas/Admin/Views/Tools/Edit.cshtml create mode 100644 Areas/Admin/Views/Tools/Index.cshtml create mode 100644 Areas/Admin/Views/UploadedAssets/Create.cshtml create mode 100644 Areas/Admin/Views/UploadedAssets/Index.cshtml create mode 100644 Areas/Admin/Views/_ViewImports.cshtml create mode 100644 Areas/Admin/Views/_ViewStart.cshtml create mode 100644 Controllers/AccountController.cs create mode 100644 Controllers/AssetsController.cs create mode 100644 Controllers/MediaController.cs create mode 100644 Controllers/ModelsController.cs create mode 100644 Controllers/PagesController.cs create mode 100644 Controllers/ToolsController.cs create mode 100644 Controllers/WritingsController.cs create mode 100644 Data/ApplicationDbContext.cs create mode 100644 Data/Configurations/EssayConfiguration.cs create mode 100644 Data/Configurations/FrameworkModelConfiguration.cs create mode 100644 Data/Configurations/MediaItemConfiguration.cs create mode 100644 Data/Configurations/SiteSettingConfiguration.cs create mode 100644 Data/Configurations/StaticPageConfiguration.cs create mode 100644 Data/Configurations/ToolConfiguration.cs create mode 100644 Data/Configurations/ToolQuestionConfiguration.cs create mode 100644 Data/Configurations/UploadedAssetConfiguration.cs create mode 100644 Data/Seed/DatabaseSeeder.cs delete mode 100644 Models/ArchiveModule.cs create mode 100644 Models/Domain/EntityBase.cs create mode 100644 Models/Domain/Essay.cs create mode 100644 Models/Domain/FrameworkModel.cs create mode 100644 Models/Domain/MediaItem.cs create mode 100644 Models/Domain/PublishableEntityBase.cs create mode 100644 Models/Domain/SiteSetting.cs create mode 100644 Models/Domain/StaticPage.cs create mode 100644 Models/Domain/Tool.cs create mode 100644 Models/Domain/ToolQuestion.cs create mode 100644 Models/Domain/UploadedAsset.cs delete mode 100644 Models/EssayItem.cs delete mode 100644 Models/FeaturedWorkItem.cs create mode 100644 Models/Identity/ApplicationUser.cs delete mode 100644 Models/MediaItem.cs delete mode 100644 Models/ModelItem.cs delete mode 100644 Models/ToolItem.cs delete mode 100644 Models/WritingItem.cs create mode 100644 Services/Assets/AssetService.cs create mode 100644 Services/Assets/IAssetService.cs create mode 100644 Services/Markdown/IMarkdownRenderer.cs create mode 100644 Services/Markdown/MarkdownRenderer.cs delete mode 100644 ViewModels/ContentDetailViewModel.cs create mode 100644 ViewModels/DetailPageViewModel.cs create mode 100644 ViewModels/ListPageViewModel.cs delete mode 100644 ViewModels/MediaPageViewModel.cs delete mode 100644 ViewModels/ModelsPageViewModel.cs delete mode 100644 ViewModels/SimplePageViewModel.cs create mode 100644 ViewModels/StaticPageViewModel.cs delete mode 100644 ViewModels/ToolsPageViewModel.cs delete mode 100644 ViewModels/WritingsPageViewModel.cs create mode 100644 Views/Account/Login.cshtml delete mode 100644 Views/Home/About.cshtml delete mode 100644 Views/Home/Contact.cshtml delete mode 100644 Views/Home/ContentDetail.cshtml delete mode 100644 Views/Home/Media.cshtml delete mode 100644 Views/Home/Models.cshtml delete mode 100644 Views/Home/Tools.cshtml delete mode 100644 Views/Home/Writings.cshtml create mode 100644 Views/Media/Index.cshtml create mode 100644 Views/Models/Detail.cshtml create mode 100644 Views/Models/Index.cshtml create mode 100644 Views/Pages/Page.cshtml delete mode 100644 Views/Shared/_AboutPreview.cshtml delete mode 100644 Views/Shared/_AboutSection.cshtml delete mode 100644 Views/Shared/_ArchiveIndex.cshtml delete mode 100644 Views/Shared/_ConceptSection.cshtml delete mode 100644 Views/Shared/_ContactPreview.cshtml delete mode 100644 Views/Shared/_ContactSection.cshtml delete mode 100644 Views/Shared/_FeaturedModels.cshtml delete mode 100644 Views/Shared/_FeaturedWorks.cshtml delete mode 100644 Views/Shared/_Footer.cshtml delete mode 100644 Views/Shared/_Header.cshtml delete mode 100644 Views/Shared/_Hero.cshtml delete mode 100644 Views/Shared/_Manifesto.cshtml delete mode 100644 Views/Shared/_MediaPreview.cshtml delete mode 100644 Views/Shared/_SelectedWritings.cshtml delete mode 100644 Views/Shared/_ThoughtsSection.cshtml delete mode 100644 Views/Shared/_ToolsPreview.cshtml delete mode 100644 Views/Shared/_WhyGapIntro.cshtml create mode 100644 Views/Tools/Detail.cshtml create mode 100644 Views/Tools/Index.cshtml create mode 100644 Views/Writings/Detail.cshtml create mode 100644 Views/Writings/Index.cshtml create mode 100644 appsettings.Development.json create mode 100644 appsettings.json create mode 100644 wwwroot/img/Logo-Out.svg diff --git a/Areas/Admin/Controllers/EssaysController.cs b/Areas/Admin/Controllers/EssaysController.cs new file mode 100644 index 0000000..6ae3d8f --- /dev/null +++ b/Areas/Admin/Controllers/EssaysController.cs @@ -0,0 +1,23 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class EssaysController : Controller +{ + private readonly ApplicationDbContext _db; private readonly IMarkdownRenderer _markdown; + public EssaysController(ApplicationDbContext db, IMarkdownRenderer markdown) { _db = db; _markdown = markdown; } + public async Task Index() => View(await _db.Essays.OrderBy(x => x.SortOrder).ToListAsync()); + public IActionResult Create() => View("Edit", new EssayEditViewModel()); + [HttpPost, ValidateAntiForgeryToken] public async Task Create(EssayEditViewModel vm){ if(!ModelState.IsValid) return View("Edit",vm); var e=new Essay(); Map(vm,e); _db.Add(e); await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } + public async Task Edit(int id){ var e=await _db.Essays.FindAsync(id); if(e==null) return NotFound(); return View(new EssayEditViewModel{Id=e.Id,Slug=e.Slug,Title=e.Title,Summary=e.Summary,MarkdownBody=e.MarkdownBody,ReadTimeMinutes=e.ReadTimeMinutes,Category=e.Category,SeoTitle=e.SeoTitle,SeoDescription=e.SeoDescription,HeroAssetId=e.HeroAssetId,SortOrder=e.SortOrder,IsPublished=e.IsPublished}); } + [HttpPost, ValidateAntiForgeryToken] public async Task Edit(int id, EssayEditViewModel vm){ var e=await _db.Essays.FindAsync(id); if(e==null) return NotFound(); Map(vm,e); await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } + [HttpPost, ValidateAntiForgeryToken] public async Task Delete(int id){ var e=await _db.Essays.FindAsync(id); if(e!=null){_db.Remove(e); await _db.SaveChangesAsync();} return RedirectToAction(nameof(Index)); } + private void Map(EssayEditViewModel vm, Essay e){ e.Slug=vm.Slug; e.Title=vm.Title; e.Summary=vm.Summary; e.MarkdownBody=vm.MarkdownBody; e.RenderedHtml=_markdown.Render(vm.MarkdownBody); e.ReadTimeMinutes=vm.ReadTimeMinutes; e.Category=vm.Category; e.SeoTitle=vm.SeoTitle; e.SeoDescription=vm.SeoDescription; e.HeroAssetId=vm.HeroAssetId; e.SortOrder=vm.SortOrder; e.IsPublished=vm.IsPublished; e.PublishedOnUtc=vm.IsPublished?DateTime.UtcNow:null; e.UpdatedUtc=DateTime.UtcNow; } +} diff --git a/Areas/Admin/Controllers/FrameworkModelsController.cs b/Areas/Admin/Controllers/FrameworkModelsController.cs new file mode 100644 index 0000000..3e33e3d --- /dev/null +++ b/Areas/Admin/Controllers/FrameworkModelsController.cs @@ -0,0 +1,23 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class FrameworkModelsController : Controller +{ + private readonly ApplicationDbContext _db; private readonly IMarkdownRenderer _md; + public FrameworkModelsController(ApplicationDbContext db, IMarkdownRenderer md){_db=db;_md=md;} + public async Task Index()=>View(await _db.FrameworkModels.OrderBy(x=>x.SortOrder).ToListAsync()); + public IActionResult Create()=>View("Edit",new FrameworkModelEditViewModel()); + [HttpPost,ValidateAntiForgeryToken] public async Task Create(FrameworkModelEditViewModel vm){var m=new FrameworkModel();Map(vm,m);_db.Add(m);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + public async Task Edit(int id){var m=await _db.FrameworkModels.FindAsync(id);if(m==null) return NotFound(); return View(new FrameworkModelEditViewModel{Id=m.Id,Slug=m.Slug,Title=m.Title,Summary=m.Summary,MarkdownBody=m.MarkdownBody,SeoTitle=m.SeoTitle,SeoDescription=m.SeoDescription,DiagramAssetId=m.DiagramAssetId,SortOrder=m.SortOrder,IsPublished=m.IsPublished});} + [HttpPost,ValidateAntiForgeryToken] public async Task Edit(int id, FrameworkModelEditViewModel vm){var m=await _db.FrameworkModels.FindAsync(id); if(m==null)return NotFound(); Map(vm,m); await _db.SaveChangesAsync(); return RedirectToAction(nameof(Index));} + [HttpPost,ValidateAntiForgeryToken] public async Task Delete(int id){var m=await _db.FrameworkModels.FindAsync(id); if(m!=null){_db.Remove(m); await _db.SaveChangesAsync();} return RedirectToAction(nameof(Index));} + void Map(FrameworkModelEditViewModel vm, FrameworkModel m){m.Slug=vm.Slug;m.Title=vm.Title;m.Summary=vm.Summary;m.MarkdownBody=vm.MarkdownBody;m.RenderedHtml=_md.Render(vm.MarkdownBody);m.SeoTitle=vm.SeoTitle;m.SeoDescription=vm.SeoDescription;m.DiagramAssetId=vm.DiagramAssetId;m.SortOrder=vm.SortOrder;m.IsPublished=vm.IsPublished;m.PublishedOnUtc=vm.IsPublished?DateTime.UtcNow:null;m.UpdatedUtc=DateTime.UtcNow;} +} diff --git a/Areas/Admin/Controllers/HomeController.cs b/Areas/Admin/Controllers/HomeController.cs new file mode 100644 index 0000000..62cb604 --- /dev/null +++ b/Areas/Admin/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin")] +[Authorize(Roles = "Admin")] +public class HomeController : Controller +{ + public IActionResult Index() => View(); +} diff --git a/Areas/Admin/Controllers/MediaItemsController.cs b/Areas/Admin/Controllers/MediaItemsController.cs new file mode 100644 index 0000000..0388350 --- /dev/null +++ b/Areas/Admin/Controllers/MediaItemsController.cs @@ -0,0 +1,23 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class MediaItemsController : Controller +{ + private readonly ApplicationDbContext _db; private readonly IMarkdownRenderer _md; + public MediaItemsController(ApplicationDbContext db, IMarkdownRenderer md){_db=db;_md=md;} + public async Task Index()=>View(await _db.MediaItems.OrderBy(x=>x.SortOrder).ToListAsync()); + public IActionResult Create()=>View("Edit",new MediaItemEditViewModel()); + [HttpPost,ValidateAntiForgeryToken] public async Task Create(MediaItemEditViewModel vm){var m=new MediaItem();Map(vm,m);_db.Add(m);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + public async Task Edit(int id){var m=await _db.MediaItems.FindAsync(id);if(m==null)return NotFound();return View(new MediaItemEditViewModel{Id=m.Id,Slug=m.Slug,Title=m.Title,Summary=m.Summary,MarkdownBody=m.MarkdownBody,MediaType=m.MediaType,Duration=m.Duration,VideoAssetId=m.VideoAssetId,ThumbnailAssetId=m.ThumbnailAssetId,SortOrder=m.SortOrder,IsPublished=m.IsPublished});} + [HttpPost,ValidateAntiForgeryToken] public async Task Edit(int id, MediaItemEditViewModel vm){var m=await _db.MediaItems.FindAsync(id);if(m==null)return NotFound();Map(vm,m);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + [HttpPost,ValidateAntiForgeryToken] public async Task Delete(int id){var m=await _db.MediaItems.FindAsync(id);if(m!=null){_db.Remove(m);await _db.SaveChangesAsync();}return RedirectToAction(nameof(Index));} + void Map(MediaItemEditViewModel vm, MediaItem m){m.Slug=vm.Slug;m.Title=vm.Title;m.Summary=vm.Summary;m.MarkdownBody=vm.MarkdownBody;m.RenderedHtml=_md.Render(vm.MarkdownBody);m.MediaType=vm.MediaType;m.Duration=vm.Duration;m.VideoAssetId=vm.VideoAssetId;m.ThumbnailAssetId=vm.ThumbnailAssetId;m.SortOrder=vm.SortOrder;m.IsPublished=vm.IsPublished;m.PublishedOnUtc=vm.IsPublished?DateTime.UtcNow:null;m.UpdatedUtc=DateTime.UtcNow;} +} diff --git a/Areas/Admin/Controllers/StaticPagesController.cs b/Areas/Admin/Controllers/StaticPagesController.cs new file mode 100644 index 0000000..7a7d803 --- /dev/null +++ b/Areas/Admin/Controllers/StaticPagesController.cs @@ -0,0 +1,23 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class StaticPagesController : Controller +{ + private readonly ApplicationDbContext _db; private readonly IMarkdownRenderer _md; + public StaticPagesController(ApplicationDbContext db, IMarkdownRenderer md){_db=db;_md=md;} + public async Task Index()=>View(await _db.StaticPages.OrderBy(x=>x.Slug).ToListAsync()); + public IActionResult Create()=>View("Edit",new StaticPageEditViewModel()); + [HttpPost,ValidateAntiForgeryToken] public async Task Create(StaticPageEditViewModel vm){var p=new StaticPage();Map(vm,p);_db.Add(p);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + public async Task Edit(int id){var p=await _db.StaticPages.FindAsync(id);if(p==null)return NotFound();return View(new StaticPageEditViewModel{Id=p.Id,Slug=p.Slug,Title=p.Title,MarkdownBody=p.MarkdownBody,SeoTitle=p.SeoTitle,SeoDescription=p.SeoDescription,IsPublished=p.IsPublished});} + [HttpPost,ValidateAntiForgeryToken] public async Task Edit(int id, StaticPageEditViewModel vm){var p=await _db.StaticPages.FindAsync(id);if(p==null)return NotFound();Map(vm,p);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + [HttpPost,ValidateAntiForgeryToken] public async Task Delete(int id){var p=await _db.StaticPages.FindAsync(id);if(p!=null){_db.Remove(p);await _db.SaveChangesAsync();}return RedirectToAction(nameof(Index));} + void Map(StaticPageEditViewModel vm, StaticPage p){p.Slug=vm.Slug;p.Title=vm.Title;p.MarkdownBody=vm.MarkdownBody;p.RenderedHtml=_md.Render(vm.MarkdownBody);p.SeoTitle=vm.SeoTitle;p.SeoDescription=vm.SeoDescription;p.IsPublished=vm.IsPublished;p.PublishedOnUtc=vm.IsPublished?DateTime.UtcNow:null;p.UpdatedUtc=DateTime.UtcNow;} +} diff --git a/Areas/Admin/Controllers/ToolQuestionsController.cs b/Areas/Admin/Controllers/ToolQuestionsController.cs new file mode 100644 index 0000000..bdde13d --- /dev/null +++ b/Areas/Admin/Controllers/ToolQuestionsController.cs @@ -0,0 +1,24 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class ToolQuestionsController : Controller +{ + private readonly ApplicationDbContext _db; + public ToolQuestionsController(ApplicationDbContext db){_db=db;} + public async Task Index()=>View(await _db.ToolQuestions.Include(x=>x.Tool).OrderBy(x=>x.ToolId).ThenBy(x=>x.SortOrder).ToListAsync()); + public async Task Create(){await LoadTools();return View("Edit",new ToolQuestionEditViewModel());} + [HttpPost,ValidateAntiForgeryToken] public async Task Create(ToolQuestionEditViewModel vm){if(!ModelState.IsValid){await LoadTools();return View("Edit",vm);}var q=new ToolQuestion();Map(vm,q);_db.Add(q);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + public async Task Edit(int id){var q=await _db.ToolQuestions.FindAsync(id);if(q==null)return NotFound();await LoadTools();return View(new ToolQuestionEditViewModel{Id=q.Id,ToolId=q.ToolId,Prompt=q.Prompt,HelpText=q.HelpText,SortOrder=q.SortOrder,MinScore=q.MinScore,MaxScore=q.MaxScore});} + [HttpPost,ValidateAntiForgeryToken] public async Task Edit(int id, ToolQuestionEditViewModel vm){var q=await _db.ToolQuestions.FindAsync(id);if(q==null)return NotFound();Map(vm,q);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + [HttpPost,ValidateAntiForgeryToken] public async Task Delete(int id){var q=await _db.ToolQuestions.FindAsync(id);if(q!=null){_db.Remove(q);await _db.SaveChangesAsync();}return RedirectToAction(nameof(Index));} + async Task LoadTools()=>ViewBag.Tools=new SelectList(await _db.Tools.OrderBy(x=>x.Title).ToListAsync(),"Id","Title"); + static void Map(ToolQuestionEditViewModel vm, ToolQuestion q){q.ToolId=vm.ToolId;q.Prompt=vm.Prompt;q.HelpText=vm.HelpText;q.SortOrder=vm.SortOrder;q.MinScore=vm.MinScore;q.MaxScore=vm.MaxScore;q.UpdatedUtc=DateTime.UtcNow;} +} diff --git a/Areas/Admin/Controllers/ToolsController.cs b/Areas/Admin/Controllers/ToolsController.cs new file mode 100644 index 0000000..61a13db --- /dev/null +++ b/Areas/Admin/Controllers/ToolsController.cs @@ -0,0 +1,23 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class ToolsController : Controller +{ + private readonly ApplicationDbContext _db; private readonly IMarkdownRenderer _md; + public ToolsController(ApplicationDbContext db, IMarkdownRenderer md){_db=db;_md=md;} + public async Task Index()=>View(await _db.Tools.OrderBy(x=>x.SortOrder).ToListAsync()); + public IActionResult Create()=>View("Edit",new ToolEditViewModel()); + [HttpPost,ValidateAntiForgeryToken] public async Task Create(ToolEditViewModel vm){var t=new Tool();Map(vm,t);_db.Add(t);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + public async Task Edit(int id){var t=await _db.Tools.FindAsync(id);if(t==null)return NotFound();return View(new ToolEditViewModel{Id=t.Id,Slug=t.Slug,Title=t.Title,Summary=t.Summary,MarkdownBody=t.MarkdownBody,ToolType=t.ToolType,EstimatedDuration=t.EstimatedDuration,SeoTitle=t.SeoTitle,SeoDescription=t.SeoDescription,SortOrder=t.SortOrder,IsPublished=t.IsPublished});} + [HttpPost,ValidateAntiForgeryToken] public async Task Edit(int id, ToolEditViewModel vm){var t=await _db.Tools.FindAsync(id);if(t==null)return NotFound();Map(vm,t);await _db.SaveChangesAsync();return RedirectToAction(nameof(Index));} + [HttpPost,ValidateAntiForgeryToken] public async Task Delete(int id){var t=await _db.Tools.FindAsync(id);if(t!=null){_db.Remove(t);await _db.SaveChangesAsync();}return RedirectToAction(nameof(Index));} + void Map(ToolEditViewModel vm, Tool t){t.Slug=vm.Slug;t.Title=vm.Title;t.Summary=vm.Summary;t.MarkdownBody=vm.MarkdownBody;t.RenderedHtml=_md.Render(vm.MarkdownBody);t.ToolType=vm.ToolType;t.EstimatedDuration=vm.EstimatedDuration;t.SeoTitle=vm.SeoTitle;t.SeoDescription=vm.SeoDescription;t.SortOrder=vm.SortOrder;t.IsPublished=vm.IsPublished;t.PublishedOnUtc=vm.IsPublished?DateTime.UtcNow:null;t.UpdatedUtc=DateTime.UtcNow;} +} diff --git a/Areas/Admin/Controllers/UploadedAssetsController.cs b/Areas/Admin/Controllers/UploadedAssetsController.cs new file mode 100644 index 0000000..281d733 --- /dev/null +++ b/Areas/Admin/Controllers/UploadedAssetsController.cs @@ -0,0 +1,23 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Services.Assets; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Areas.Admin.Controllers; + +[Area("Admin"), Authorize(Roles = "Admin")] +public class UploadedAssetsController : Controller +{ + private readonly ApplicationDbContext _db; private readonly IAssetService _assets; + public UploadedAssetsController(ApplicationDbContext db, IAssetService assets){_db=db;_assets=assets;} + public async Task Index()=>View(await _db.UploadedAssets.OrderByDescending(x=>x.CreatedUtc).ToListAsync()); + [HttpGet] public IActionResult Create()=>View(); + [HttpPost,ValidateAntiForgeryToken] public async Task Create(IFormFile file, string? altText, string? caption, string assetKind="image") + { + if (file == null || file.Length == 0){ModelState.AddModelError("file","File is required."); return View();} + await _assets.SaveAsync(file, altText, caption, assetKind); + return RedirectToAction(nameof(Index)); + } + [HttpPost,ValidateAntiForgeryToken] public async Task Delete(int id){var a=await _db.UploadedAssets.FindAsync(id);if(a!=null){_db.Remove(a);await _db.SaveChangesAsync();}return RedirectToAction(nameof(Index));} +} diff --git a/Areas/Admin/ViewModels/EssayEditViewModel.cs b/Areas/Admin/ViewModels/EssayEditViewModel.cs new file mode 100644 index 0000000..6553ec6 --- /dev/null +++ b/Areas/Admin/ViewModels/EssayEditViewModel.cs @@ -0,0 +1,17 @@ +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class EssayEditViewModel +{ + public int? Id { get; set; } + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public int ReadTimeMinutes { get; set; } + public string Category { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + public int? HeroAssetId { get; set; } + public int SortOrder { get; set; } + public bool IsPublished { get; set; } +} diff --git a/Areas/Admin/ViewModels/FrameworkModelEditViewModel.cs b/Areas/Admin/ViewModels/FrameworkModelEditViewModel.cs new file mode 100644 index 0000000..00d25da --- /dev/null +++ b/Areas/Admin/ViewModels/FrameworkModelEditViewModel.cs @@ -0,0 +1,15 @@ +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class FrameworkModelEditViewModel +{ + public int? Id { get; set; } + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + public int? DiagramAssetId { get; set; } + public int SortOrder { get; set; } + public bool IsPublished { get; set; } +} diff --git a/Areas/Admin/ViewModels/LoginViewModel.cs b/Areas/Admin/ViewModels/LoginViewModel.cs new file mode 100644 index 0000000..75c49b8 --- /dev/null +++ b/Areas/Admin/ViewModels/LoginViewModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class LoginViewModel +{ + [Required] + public string Username { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = string.Empty; + + public bool RememberMe { get; set; } + public string? ReturnUrl { get; set; } +} diff --git a/Areas/Admin/ViewModels/MediaItemEditViewModel.cs b/Areas/Admin/ViewModels/MediaItemEditViewModel.cs new file mode 100644 index 0000000..ebb1577 --- /dev/null +++ b/Areas/Admin/ViewModels/MediaItemEditViewModel.cs @@ -0,0 +1,16 @@ +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class MediaItemEditViewModel +{ + public int? Id { get; set; } + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string MediaType { get; set; } = string.Empty; + public string? Duration { get; set; } + public int? VideoAssetId { get; set; } + public int? ThumbnailAssetId { get; set; } + public int SortOrder { get; set; } + public bool IsPublished { get; set; } +} diff --git a/Areas/Admin/ViewModels/StaticPageEditViewModel.cs b/Areas/Admin/ViewModels/StaticPageEditViewModel.cs new file mode 100644 index 0000000..8d12d0a --- /dev/null +++ b/Areas/Admin/ViewModels/StaticPageEditViewModel.cs @@ -0,0 +1,12 @@ +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class StaticPageEditViewModel +{ + public int? Id { get; set; } + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + public bool IsPublished { get; set; } +} diff --git a/Areas/Admin/ViewModels/ToolEditViewModel.cs b/Areas/Admin/ViewModels/ToolEditViewModel.cs new file mode 100644 index 0000000..ddd41f0 --- /dev/null +++ b/Areas/Admin/ViewModels/ToolEditViewModel.cs @@ -0,0 +1,16 @@ +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class ToolEditViewModel +{ + public int? Id { get; set; } + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string ToolType { get; set; } = string.Empty; + public string EstimatedDuration { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + public int SortOrder { get; set; } + public bool IsPublished { get; set; } +} diff --git a/Areas/Admin/ViewModels/ToolQuestionEditViewModel.cs b/Areas/Admin/ViewModels/ToolQuestionEditViewModel.cs new file mode 100644 index 0000000..57a32af --- /dev/null +++ b/Areas/Admin/ViewModels/ToolQuestionEditViewModel.cs @@ -0,0 +1,12 @@ +namespace manpreetsingh.pro.Areas.Admin.ViewModels; + +public class ToolQuestionEditViewModel +{ + public int? Id { get; set; } + public int ToolId { get; set; } + public string Prompt { get; set; } = string.Empty; + public string? HelpText { get; set; } + public int SortOrder { get; set; } + public int MinScore { get; set; } + public int MaxScore { get; set; } +} diff --git a/Areas/Admin/Views/Essays/Edit.cshtml b/Areas/Admin/Views/Essays/Edit.cshtml new file mode 100644 index 0000000..a8b3112 --- /dev/null +++ b/Areas/Admin/Views/Essays/Edit.cshtml @@ -0,0 +1,2 @@ +@model EssayEditViewModel +

Essay

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/Essays/Index.cshtml b/Areas/Admin/Views/Essays/Index.cshtml new file mode 100644 index 0000000..1caed60 --- /dev/null +++ b/Areas/Admin/Views/Essays/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Essays

Create@foreach(var x in Model){}
@x.Title@x.IsPublishedEdit
@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/FrameworkModels/Edit.cshtml b/Areas/Admin/Views/FrameworkModels/Edit.cshtml new file mode 100644 index 0000000..5704ec8 --- /dev/null +++ b/Areas/Admin/Views/FrameworkModels/Edit.cshtml @@ -0,0 +1,2 @@ +@model FrameworkModelEditViewModel +

Framework Model

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/FrameworkModels/Index.cshtml b/Areas/Admin/Views/FrameworkModels/Index.cshtml new file mode 100644 index 0000000..1c9f0bf --- /dev/null +++ b/Areas/Admin/Views/FrameworkModels/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Framework Models

Create@foreach(var x in Model){
@x.Title Edit
@Html.AntiForgeryToken()
} diff --git a/Areas/Admin/Views/Home/Index.cshtml b/Areas/Admin/Views/Home/Index.cshtml new file mode 100644 index 0000000..3fedc02 --- /dev/null +++ b/Areas/Admin/Views/Home/Index.cshtml @@ -0,0 +1 @@ +

Content Management

Manage essays, models, tools, media, assets, and static pages.

diff --git a/Areas/Admin/Views/MediaItems/Edit.cshtml b/Areas/Admin/Views/MediaItems/Edit.cshtml new file mode 100644 index 0000000..d4d3974 --- /dev/null +++ b/Areas/Admin/Views/MediaItems/Edit.cshtml @@ -0,0 +1,2 @@ +@model MediaItemEditViewModel +

Media Item

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/MediaItems/Index.cshtml b/Areas/Admin/Views/MediaItems/Index.cshtml new file mode 100644 index 0000000..4494773 --- /dev/null +++ b/Areas/Admin/Views/MediaItems/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Media Items

Create@foreach(var x in Model){
@x.Title Edit
@Html.AntiForgeryToken()
} diff --git a/Areas/Admin/Views/Shared/_Layout.cshtml b/Areas/Admin/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..86dde7a --- /dev/null +++ b/Areas/Admin/Views/Shared/_Layout.cshtml @@ -0,0 +1 @@ +

Admin

@RenderBody()
diff --git a/Areas/Admin/Views/StaticPages/Edit.cshtml b/Areas/Admin/Views/StaticPages/Edit.cshtml new file mode 100644 index 0000000..4aed9bc --- /dev/null +++ b/Areas/Admin/Views/StaticPages/Edit.cshtml @@ -0,0 +1,2 @@ +@model StaticPageEditViewModel +

Static Page

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/StaticPages/Index.cshtml b/Areas/Admin/Views/StaticPages/Index.cshtml new file mode 100644 index 0000000..10e2206 --- /dev/null +++ b/Areas/Admin/Views/StaticPages/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Static Pages

Create@foreach(var x in Model){
@x.Slug Edit
@Html.AntiForgeryToken()
} diff --git a/Areas/Admin/Views/ToolQuestions/Edit.cshtml b/Areas/Admin/Views/ToolQuestions/Edit.cshtml new file mode 100644 index 0000000..e473935 --- /dev/null +++ b/Areas/Admin/Views/ToolQuestions/Edit.cshtml @@ -0,0 +1,2 @@ +@model ToolQuestionEditViewModel +

Tool Question

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/ToolQuestions/Index.cshtml b/Areas/Admin/Views/ToolQuestions/Index.cshtml new file mode 100644 index 0000000..acc70f8 --- /dev/null +++ b/Areas/Admin/Views/ToolQuestions/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Tool Questions

Create@foreach(var x in Model){
@x.Tool?.Title - @x.Prompt Edit
@Html.AntiForgeryToken()
} diff --git a/Areas/Admin/Views/Tools/Edit.cshtml b/Areas/Admin/Views/Tools/Edit.cshtml new file mode 100644 index 0000000..e0627e5 --- /dev/null +++ b/Areas/Admin/Views/Tools/Edit.cshtml @@ -0,0 +1,2 @@ +@model ToolEditViewModel +

Tool

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/Tools/Index.cshtml b/Areas/Admin/Views/Tools/Index.cshtml new file mode 100644 index 0000000..2b1b622 --- /dev/null +++ b/Areas/Admin/Views/Tools/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Tools

Create@foreach(var x in Model){
@x.Title Edit
@Html.AntiForgeryToken()
} diff --git a/Areas/Admin/Views/UploadedAssets/Create.cshtml b/Areas/Admin/Views/UploadedAssets/Create.cshtml new file mode 100644 index 0000000..7946a0f --- /dev/null +++ b/Areas/Admin/Views/UploadedAssets/Create.cshtml @@ -0,0 +1 @@ +

Upload Asset

@Html.AntiForgeryToken()
diff --git a/Areas/Admin/Views/UploadedAssets/Index.cshtml b/Areas/Admin/Views/UploadedAssets/Index.cshtml new file mode 100644 index 0000000..6d14858 --- /dev/null +++ b/Areas/Admin/Views/UploadedAssets/Index.cshtml @@ -0,0 +1,2 @@ +@model IReadOnlyList +

Uploaded Assets

Upload@foreach(var x in Model){
@x.FileName (@x.ContentType) id:@x.Id
@Html.AntiForgeryToken()
} diff --git a/Areas/Admin/Views/_ViewImports.cshtml b/Areas/Admin/Views/_ViewImports.cshtml new file mode 100644 index 0000000..01ed183 --- /dev/null +++ b/Areas/Admin/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using manpreetsingh.pro +@using manpreetsingh.pro.Models.Domain +@using manpreetsingh.pro.Areas.Admin.ViewModels +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Areas/Admin/Views/_ViewStart.cshtml b/Areas/Admin/Views/_ViewStart.cshtml new file mode 100644 index 0000000..7c052b4 --- /dev/null +++ b/Areas/Admin/Views/_ViewStart.cshtml @@ -0,0 +1 @@ +@{ Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml"; } diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs new file mode 100644 index 0000000..368acf8 --- /dev/null +++ b/Controllers/AccountController.cs @@ -0,0 +1,42 @@ +using manpreetsingh.pro.Areas.Admin.ViewModels; +using manpreetsingh.pro.Models.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace manpreetsingh.pro.Controllers; + +[Route("account")] +public class AccountController : Controller +{ + private readonly SignInManager _signInManager; + + public AccountController(SignInManager signInManager) + { + _signInManager = signInManager; + } + + [HttpGet("login")] + public IActionResult Login(string? returnUrl = null) => View(new LoginViewModel { ReturnUrl = returnUrl }); + + [HttpPost("login")] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model) + { + if (!ModelState.IsValid) return View(model); + var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false); + if (!result.Succeeded) + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } + return LocalRedirect(model.ReturnUrl ?? "/admin"); + } + + [HttpPost("logout")] + [ValidateAntiForgeryToken] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + return RedirectToAction("Index", "Home"); + } +} diff --git a/Controllers/AssetsController.cs b/Controllers/AssetsController.cs new file mode 100644 index 0000000..b9b0123 --- /dev/null +++ b/Controllers/AssetsController.cs @@ -0,0 +1,18 @@ +using manpreetsingh.pro.Data; +using Microsoft.AspNetCore.Mvc; + +namespace manpreetsingh.pro.Controllers; + +[Route("assets")] +public class AssetsController : Controller +{ + private readonly ApplicationDbContext _db; + public AssetsController(ApplicationDbContext db) => _db = db; + + [HttpGet("{id:int}")] + public async Task Get(int id) + { + var asset = await _db.UploadedAssets.FindAsync(id); + return asset is null ? NotFound() : File(asset.Data, asset.ContentType, enableRangeProcessing: true); + } +} diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 07af4de..1f38f1f 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -1,179 +1,32 @@ -using manpreetsingh.pro.Models; +using manpreetsingh.pro.Data; using manpreetsingh.pro.ViewModels; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace manpreetsingh.pro.Controllers; public class HomeController : Controller { - [HttpGet("/")] - public IActionResult Index() - { - var model = new HomePageViewModel - { - HeroTitle = "HOW ORGANIZATIONS REALLY WORK", - HeroSubtext = "Understanding the forces behind decisions, hierarchy, delay, and execution.", - WhyGapIntro = "Most organizational behavior looks personal from the outside but structural from the inside.", - WhyGapPoints = - [ - "Leaders often act inside constraints they rarely state out loud.", - "Managers absorb the consequences without visibility into those constraints.", - "That distance between decision logic and lived reality is the Why Gap." - ], - FeaturedModels = GetModels(), - SelectedWritings = GetWritings(), - Tools = GetTools(), - MediaItems = GetMedia(), - AboutSummary = "Manpreet Singh writes and teaches about how real systems shape real behavior inside complex organizations.", - ContactSummary = "For speaking, advisory work, and private workshops focused on operating reality, reach out directly." - }; + private readonly ApplicationDbContext _db; - return View(model); - } - - [HttpGet("/writings")] - public IActionResult Writings() => View(new WritingsPageViewModel - { - Intro = "Essays for middle and senior managers trying to make sense of pressure, delay, and institutional behavior.", - Items = GetWritings() - }); - - [HttpGet("/writings/{slug}")] - public IActionResult Writing(string slug) + public HomeController(ApplicationDbContext db) { - var item = GetWritings().FirstOrDefault(x => x.Slug == slug); - if (item is null) - { - return NotFound(); - } - - return View("ContentDetail", new ContentDetailViewModel - { - SectionLabel = "Writing", - Title = item.Title, - Summary = item.Summary, - Points = - [ - "Where decision rights actually sit is usually different from the org chart.", - "Most slowdowns are produced by risk routing, not individual resistance.", - "Repair starts with naming the system pressure before naming the people." - ] - }); - } - - [HttpGet("/models")] - public IActionResult Models() => View(new ModelsPageViewModel - { - Intro = "Simple explanatory models for diagnosing recurring organizational patterns.", - Items = GetModels() - }); - - [HttpGet("/models/{slug}")] - public IActionResult Model(string slug) - { - var item = GetModels().FirstOrDefault(x => x.Slug == slug); - if (item is null) - { - return NotFound(); - } - - return View("ContentDetail", new ContentDetailViewModel - { - SectionLabel = "Model", - Title = item.Name, - Summary = item.Summary, - Points = [item.CoreQuestion, "Use this model before prescribing behavior changes."] - }); + _db = db; } - [HttpGet("/tools")] - public IActionResult Tools() => View(new ToolsPageViewModel - { - Intro = "Practical checks to apply the models in real operating contexts.", - Items = GetTools() - }); - - [HttpGet("/tools/{slug}")] - public IActionResult Tool(string slug) + [HttpGet("/")] + public async Task Index() { - var item = GetTools().FirstOrDefault(x => x.Slug == slug); - if (item is null) + var vm = new HomePageViewModel { - return NotFound(); - } + SelectedWritings = await _db.Essays.Where(x => x.IsPublished).OrderBy(x => x.SortOrder).ThenByDescending(x => x.PublishedOnUtc).Take(3).ToListAsync(), + FeaturedModels = await _db.FrameworkModels.Where(x => x.IsPublished).OrderBy(x => x.SortOrder).Take(3).ToListAsync(), + Tools = await _db.Tools.Where(x => x.IsPublished).OrderBy(x => x.SortOrder).Take(2).ToListAsync(), + MediaItems = await _db.MediaItems.Where(x => x.IsPublished).OrderBy(x => x.SortOrder).Take(2).ToListAsync(), + AboutPage = await _db.StaticPages.FirstOrDefaultAsync(x => x.Slug == "about" && x.IsPublished), + ContactPage = await _db.StaticPages.FirstOrDefaultAsync(x => x.Slug == "contact" && x.IsPublished) + }; - return View("ContentDetail", new ContentDetailViewModel - { - SectionLabel = "Tool", - Title = item.Name, - Summary = item.Summary, - Points = - [ - "Use in team reviews, operating retros, or planning sessions.", - "Focus on constraints, sequencing, and decision visibility." - ] - }); + return View(vm); } - - [HttpGet("/media")] - public IActionResult Media() => View(new MediaPageViewModel - { - Intro = "Short talks and video explanations for teams that need shared language quickly.", - Items = GetMedia() - }); - - [HttpGet("/about")] - public IActionResult About() => View(new SimplePageViewModel - { - Title = "About", - Intro = "This platform exists to explain why complex organizations behave the way they do.", - Details = - [ - "Audience: middle and senior managers.", - "Method: plain language, structural analysis, practical models.", - "Tone: calm, direct, non-jargon." - ] - }); - - [HttpGet("/contact")] - public IActionResult Contact() => View(new SimplePageViewModel - { - Title = "Contact", - Intro = "For speaking, advisory work, and workshops.", - Details = - [ - "Email: hello@manpreetsingh.pro", - "Location: Global / Remote", - "Response window: 3–5 business days" - ] - }); - - private static List GetModels() => - [ - new() { Name = "The Why Gap", Slug = "the-why-gap", Summary = "Explains the distance between visible decisions and invisible constraints.", CoreQuestion = "What constraint is driving this decision that frontline managers cannot see?" }, - new() { Name = "The Friction Map", Slug = "the-friction-map", Summary = "Surfaces where work slows, who absorbs cost, and where accountability breaks.", CoreQuestion = "Where does work stall repeatedly, and who carries the hidden load?" }, - new() { Name = "The Visibility Trap", Slug = "the-visibility-trap", Summary = "Shows how reporting systems reward legibility over reality.", CoreQuestion = "What appears healthy in dashboards but fails in daily execution?" }, - new() { Name = "Quiet Erosion", Slug = "quiet-erosion", Summary = "Describes how standards decay gradually under sustained pressure.", CoreQuestion = "Which standard is being quietly traded away to keep throughput?" } - ]; - - private static List GetWritings() => - [ - new() { Title = "The Why Gap Inside Organizations", Slug = "the-why-gap-inside-organizations", Summary = "Why decision logic becomes opaque as hierarchy grows.", PublishedOn = "2026-02-10", ReadTime = "8 min" }, - new() { Title = "Why Large Organizations Move Slowly", Slug = "why-large-organizations-move-slowly", Summary = "Delay is usually a design property, not a motivation problem.", PublishedOn = "2026-01-21", ReadTime = "7 min" }, - new() { Title = "Why Leaders Centralize Decisions", Slug = "why-leaders-centralize-decisions", Summary = "Centralization often signals risk concentration, not ego.", PublishedOn = "2025-12-18", ReadTime = "6 min" }, - new() { Title = "Why Good Managers Get Trapped in Bad Systems", Slug = "why-good-managers-get-trapped-in-bad-systems", Summary = "Competent people still fail in misaligned structures.", PublishedOn = "2025-11-09", ReadTime = "9 min" }, - new() { Title = "How Standards Erode Quietly", Slug = "how-standards-erode-quietly", Summary = "How small exceptions normalize under pressure.", PublishedOn = "2025-10-03", ReadTime = "5 min" } - ]; - - private static List GetTools() => - [ - new() { Name = "Why Gap Check", Slug = "why-gap-check", Summary = "A guided prompt set to expose hidden decision constraints." }, - new() { Name = "Friction Map Check", Slug = "friction-map-check", Summary = "A simple mapping exercise for recurring delays and handoff failures." } - ]; - - private static List GetMedia() => - [ - new() { Title = "Why organizations stall even when everyone is busy", Format = "Talk", Duration = "22 min", Summary = "A practical explanation of delay mechanics across layers." }, - new() { Title = "The Why Gap in executive communication", Format = "Video", Duration = "14 min", Summary = "How language hides constraints and distorts alignment." } - ]; } diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs new file mode 100644 index 0000000..c733a91 --- /dev/null +++ b/Controllers/MediaController.cs @@ -0,0 +1,21 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Controllers; + +[Route("media")] +public class MediaController : Controller +{ + private readonly ApplicationDbContext _db; + public MediaController(ApplicationDbContext db) => _db = db; + + [HttpGet("")] + public async Task Index() => View(new ListPageViewModel + { + Title = "Media", + Items = await _db.MediaItems.Where(x => x.IsPublished).Include(x => x.ThumbnailAsset).OrderBy(x => x.SortOrder).ToListAsync() + }); +} diff --git a/Controllers/ModelsController.cs b/Controllers/ModelsController.cs new file mode 100644 index 0000000..668256a --- /dev/null +++ b/Controllers/ModelsController.cs @@ -0,0 +1,28 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Controllers; + +[Route("models")] +public class ModelsController : Controller +{ + private readonly ApplicationDbContext _db; + public ModelsController(ApplicationDbContext db) => _db = db; + + [HttpGet("")] + public async Task Index() => View(new ListPageViewModel + { + Title = "Models", + Items = await _db.FrameworkModels.Where(x => x.IsPublished).OrderBy(x => x.SortOrder).ToListAsync() + }); + + [HttpGet("{slug}")] + public async Task Detail(string slug) + { + var model = await _db.FrameworkModels.Include(x => x.DiagramAsset).FirstOrDefaultAsync(x => x.Slug == slug && x.IsPublished); + return model is null ? NotFound() : View(new DetailPageViewModel { Item = model }); + } +} diff --git a/Controllers/PagesController.cs b/Controllers/PagesController.cs new file mode 100644 index 0000000..640e4d4 --- /dev/null +++ b/Controllers/PagesController.cs @@ -0,0 +1,26 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Controllers; + +public class PagesController : Controller +{ + private readonly ApplicationDbContext _db; + public PagesController(ApplicationDbContext db) => _db = db; + + [HttpGet("about")] + public async Task About() + { + var page = await _db.StaticPages.FirstOrDefaultAsync(x => x.Slug == "about" && x.IsPublished); + return page is null ? NotFound() : View("Page", new StaticPageViewModel { Page = page }); + } + + [HttpGet("contact")] + public async Task Contact() + { + var page = await _db.StaticPages.FirstOrDefaultAsync(x => x.Slug == "contact" && x.IsPublished); + return page is null ? NotFound() : View("Page", new StaticPageViewModel { Page = page }); + } +} diff --git a/Controllers/ToolsController.cs b/Controllers/ToolsController.cs new file mode 100644 index 0000000..d95ae19 --- /dev/null +++ b/Controllers/ToolsController.cs @@ -0,0 +1,28 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Controllers; + +[Route("tools")] +public class ToolsController : Controller +{ + private readonly ApplicationDbContext _db; + public ToolsController(ApplicationDbContext db) => _db = db; + + [HttpGet("")] + public async Task Index() => View(new ListPageViewModel + { + Title = "Tools", + Items = await _db.Tools.Where(x => x.IsPublished).Include(x => x.Questions.OrderBy(q => q.SortOrder)).OrderBy(x => x.SortOrder).ToListAsync() + }); + + [HttpGet("{slug}")] + public async Task Detail(string slug) + { + var tool = await _db.Tools.Include(x => x.Questions.OrderBy(q => q.SortOrder)).FirstOrDefaultAsync(x => x.Slug == slug && x.IsPublished); + return tool is null ? NotFound() : View(new DetailPageViewModel { Item = tool }); + } +} diff --git a/Controllers/WritingsController.cs b/Controllers/WritingsController.cs new file mode 100644 index 0000000..acab878 --- /dev/null +++ b/Controllers/WritingsController.cs @@ -0,0 +1,28 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Controllers; + +[Route("writings")] +public class WritingsController : Controller +{ + private readonly ApplicationDbContext _db; + public WritingsController(ApplicationDbContext db) => _db = db; + + [HttpGet("")] + public async Task Index() => View(new ListPageViewModel + { + Title = "Writings", + Items = await _db.Essays.Where(x => x.IsPublished).OrderBy(x => x.SortOrder).ThenByDescending(x => x.PublishedOnUtc).ToListAsync() + }); + + [HttpGet("{slug}")] + public async Task Detail(string slug) + { + var essay = await _db.Essays.Include(x => x.HeroAsset).FirstOrDefaultAsync(x => x.Slug == slug && x.IsPublished); + return essay is null ? NotFound() : View(new DetailPageViewModel { Item = essay }); + } +} diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..953217a --- /dev/null +++ b/Data/ApplicationDbContext.cs @@ -0,0 +1,28 @@ +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Models.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Data; + +public class ApplicationDbContext : IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Essays => Set(); + public DbSet FrameworkModels => Set(); + public DbSet Tools => Set(); + public DbSet ToolQuestions => Set(); + public DbSet MediaItems => Set(); + public DbSet UploadedAssets => Set(); + public DbSet StaticPages => Set(); + public DbSet SiteSettings => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); + } +} diff --git a/Data/Configurations/EssayConfiguration.cs b/Data/Configurations/EssayConfiguration.cs new file mode 100644 index 0000000..b71ae49 --- /dev/null +++ b/Data/Configurations/EssayConfiguration.cs @@ -0,0 +1,18 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class EssayConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Title).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Slug).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Summary).HasMaxLength(400).IsRequired(); + builder.Property(x => x.Category).HasMaxLength(120); + builder.HasIndex(x => x.Slug).IsUnique(); + builder.HasIndex(x => new { x.IsPublished, x.PublishedOnUtc }); + } +} diff --git a/Data/Configurations/FrameworkModelConfiguration.cs b/Data/Configurations/FrameworkModelConfiguration.cs new file mode 100644 index 0000000..d1bb8b5 --- /dev/null +++ b/Data/Configurations/FrameworkModelConfiguration.cs @@ -0,0 +1,17 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class FrameworkModelConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Title).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Slug).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Summary).HasMaxLength(400).IsRequired(); + builder.HasIndex(x => x.Slug).IsUnique(); + builder.HasIndex(x => new { x.IsPublished, x.PublishedOnUtc }); + } +} diff --git a/Data/Configurations/MediaItemConfiguration.cs b/Data/Configurations/MediaItemConfiguration.cs new file mode 100644 index 0000000..4d8f46a --- /dev/null +++ b/Data/Configurations/MediaItemConfiguration.cs @@ -0,0 +1,17 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class MediaItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Title).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Slug).HasMaxLength(200).IsRequired(); + builder.Property(x => x.MediaType).HasMaxLength(80).IsRequired(); + builder.HasIndex(x => x.Slug).IsUnique(); + builder.HasIndex(x => new { x.IsPublished, x.PublishedOnUtc }); + } +} diff --git a/Data/Configurations/SiteSettingConfiguration.cs b/Data/Configurations/SiteSettingConfiguration.cs new file mode 100644 index 0000000..7b5a910 --- /dev/null +++ b/Data/Configurations/SiteSettingConfiguration.cs @@ -0,0 +1,14 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class SiteSettingConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Key).HasMaxLength(120).IsRequired(); + builder.HasIndex(x => x.Key).IsUnique(); + } +} diff --git a/Data/Configurations/StaticPageConfiguration.cs b/Data/Configurations/StaticPageConfiguration.cs new file mode 100644 index 0000000..8fb9591 --- /dev/null +++ b/Data/Configurations/StaticPageConfiguration.cs @@ -0,0 +1,15 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class StaticPageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Title).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Slug).HasMaxLength(120).IsRequired(); + builder.HasIndex(x => x.Slug).IsUnique(); + } +} diff --git a/Data/Configurations/ToolConfiguration.cs b/Data/Configurations/ToolConfiguration.cs new file mode 100644 index 0000000..9179b66 --- /dev/null +++ b/Data/Configurations/ToolConfiguration.cs @@ -0,0 +1,18 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class ToolConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Title).HasMaxLength(200).IsRequired(); + builder.Property(x => x.Slug).HasMaxLength(200).IsRequired(); + builder.Property(x => x.ToolType).HasMaxLength(100); + builder.Property(x => x.EstimatedDuration).HasMaxLength(60); + builder.HasIndex(x => x.Slug).IsUnique(); + builder.HasIndex(x => new { x.IsPublished, x.PublishedOnUtc }); + } +} diff --git a/Data/Configurations/ToolQuestionConfiguration.cs b/Data/Configurations/ToolQuestionConfiguration.cs new file mode 100644 index 0000000..46d7fd6 --- /dev/null +++ b/Data/Configurations/ToolQuestionConfiguration.cs @@ -0,0 +1,15 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class ToolQuestionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Prompt).HasMaxLength(500).IsRequired(); + builder.HasOne(x => x.Tool).WithMany(x => x.Questions).HasForeignKey(x => x.ToolId).OnDelete(DeleteBehavior.Cascade); + builder.HasIndex(x => new { x.ToolId, x.SortOrder }); + } +} diff --git a/Data/Configurations/UploadedAssetConfiguration.cs b/Data/Configurations/UploadedAssetConfiguration.cs new file mode 100644 index 0000000..94692e9 --- /dev/null +++ b/Data/Configurations/UploadedAssetConfiguration.cs @@ -0,0 +1,15 @@ +using manpreetsingh.pro.Models.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace manpreetsingh.pro.Data.Configurations; + +public class UploadedAssetConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.FileName).HasMaxLength(255).IsRequired(); + builder.Property(x => x.ContentType).HasMaxLength(120).IsRequired(); + builder.Property(x => x.AssetKind).HasMaxLength(40).IsRequired(); + } +} diff --git a/Data/Seed/DatabaseSeeder.cs b/Data/Seed/DatabaseSeeder.cs new file mode 100644 index 0000000..b46ddfa --- /dev/null +++ b/Data/Seed/DatabaseSeeder.cs @@ -0,0 +1,246 @@ +using manpreetsingh.pro.Models.Domain; +using manpreetsingh.pro.Models.Identity; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace manpreetsingh.pro.Data.Seed; + +public class DatabaseSeeder +{ + public const string AdminRole = "Admin"; + private readonly ApplicationDbContext _db; + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly IConfiguration _configuration; + private readonly IMarkdownRenderer _markdown; + + public DatabaseSeeder( + ApplicationDbContext db, + UserManager userManager, + RoleManager roleManager, + IConfiguration configuration, + IMarkdownRenderer markdown) + { + _db = db; + _userManager = userManager; + _roleManager = roleManager; + _configuration = configuration; + _markdown = markdown; + } + + public async Task SeedAsync() + { + await _db.Database.EnsureCreatedAsync(); + + if (!await _roleManager.RoleExistsAsync(AdminRole)) + { + await _roleManager.CreateAsync(new IdentityRole(AdminRole)); + } + + var admin = await _userManager.FindByNameAsync("admin"); + if (admin is null) + { + var password = _configuration["Seed:AdminPassword"] + ?? throw new InvalidOperationException("Seed:AdminPassword not configured"); + + admin = new ApplicationUser + { + UserName = "admin", + Email = "admin@manpreetsingh.pro", + EmailConfirmed = true + }; + + var result = await _userManager.CreateAsync(admin, password); + if (!result.Succeeded) + { + throw new InvalidOperationException(string.Join(";", result.Errors.Select(e => e.Description))); + } + } + + if (!await _userManager.IsInRoleAsync(admin, AdminRole)) + { + await _userManager.AddToRoleAsync(admin, AdminRole); + } + + if (!await _db.Essays.AnyAsync()) + { + var essays = new[] + { + SeedEssay( + "the-why-gap-inside-organizations", + "The Why Gap Inside Organizations", + "Execution slows when teams cannot explain why a decision was made.", + "Decision Flow", + 8, + "The Why Gap appears when strategic intent is not translated into local meaning. Managers inherit targets but not rationale, so teams follow process while losing purpose."), + SeedEssay( + "why-large-organizations-move-slowly", + "Why Large Organizations Move Slowly", + "Scale introduces layers of review that quietly compound delay.", + "Execution", + 9, + "Large systems optimize for risk control, not speed. Every handoff protects someone, but each protection adds wait time. Delay is often a design outcome, not a staffing problem."), + SeedEssay( + "why-leaders-centralize-decisions", + "Why Leaders Centralize Decisions", + "When uncertainty rises, leaders pull choices upward.", + "Hierarchy", + 7, + "Centralization is rarely about ego alone. It is a response to unclear standards and fragmented accountability. The cost is slower learning at the edge."), + SeedEssay( + "why-good-managers-get-trapped-in-bad-systems", + "Why Good Managers Get Trapped in Bad Systems", + "Strong individual judgment cannot offset weak system design.", + "Management", + 8, + "Good managers patch issues daily, but patches can hide structural faults. Without authority to simplify process, capable people become maintainers of friction."), + SeedEssay( + "how-standards-erode-quietly", + "How Standards Erode Quietly", + "Standards collapse through small exceptions, not dramatic events.", + "Operations", + 6, + "Most declines start as pragmatic shortcuts. Over time, exceptions become norms and quality drifts. Recovery requires visible rules and consistent enforcement.") + }; + + _db.Essays.AddRange(essays); + } + + if (!await _db.FrameworkModels.AnyAsync()) + { + _db.FrameworkModels.AddRange( + SeedModel("the-why-gap", "The Why Gap", "A map of where intent gets diluted between executive decision and frontline action."), + SeedModel("the-friction-map", "The Friction Map", "A simple way to trace delay across approvals, handoffs, and dependencies."), + SeedModel("the-visibility-trap", "The Visibility Trap", "How reporting systems can increase activity data while reducing operational truth."), + SeedModel("quiet-erosion", "Quiet Erosion", "A framework for spotting gradual decline in standards before results collapse.") + ); + } + + if (!await _db.Tools.AnyAsync()) + { + var whyGap = SeedTool("why-gap-check", "Why Gap Check", "Diagnostic", "15 minutes", "Fast prompt set for assessing whether teams understand intent, tradeoffs, and constraints."); + var frictionMap = SeedTool("friction-map-check", "Friction Map Check", "Workshop", "25 minutes", "Structured review to identify where work waits, loops, or dies in review cycles."); + + _db.Tools.AddRange(whyGap, frictionMap); + await _db.SaveChangesAsync(); + + if (!await _db.ToolQuestions.AnyAsync()) + { + _db.ToolQuestions.AddRange( + new ToolQuestion { ToolId = whyGap.Id, Prompt = "Can frontline teams explain why this priority exists?", MinScore = 1, MaxScore = 5, SortOrder = 1 }, + new ToolQuestion { ToolId = whyGap.Id, Prompt = "Are tradeoffs explicit when deadlines conflict?", MinScore = 1, MaxScore = 5, SortOrder = 2 }, + new ToolQuestion { ToolId = frictionMap.Id, Prompt = "Where does work wait more than two business days?", MinScore = 1, MaxScore = 5, SortOrder = 1 }, + new ToolQuestion { ToolId = frictionMap.Id, Prompt = "Which approval step adds risk reduction versus routine delay?", MinScore = 1, MaxScore = 5, SortOrder = 2 } + ); + } + } + + if (!await _db.MediaItems.AnyAsync()) + { + _db.MediaItems.AddRange( + SeedMedia("the-why-gap-explained", "The Why Gap Explained", "Short briefing on why strategy loses meaning as it moves through hierarchy."), + SeedMedia("why-large-organizations-move-slowly", "Why Large Organizations Move Slowly", "A concise walkthrough of review layers, delay loops, and hidden queue time."), + SeedMedia("why-leaders-lose-visibility", "Why Leaders Lose Visibility", "Explains how polished reporting can hide operational reality.") + ); + } + + if (!await _db.StaticPages.AnyAsync()) + { + _db.StaticPages.AddRange( + SeedPage( + "about", + "About", + "This site examines **how organizations really work**.\n\nIt focuses on the gap between formal design and daily execution: what gets delayed, who decides, and why outcomes drift.\n\nThe audience is middle and senior managers responsible for real delivery."), + SeedPage( + "contact", + "Contact", + "For editorial requests, questions, or speaking inquiries, use your preferred channel and reference the topic clearly.\n\nPlease include organization context, decision level, and timeline constraints so responses can stay practical.") + ); + } + + await _db.SaveChangesAsync(); + } + + private Essay SeedEssay(string slug, string title, string summary, string category, int readTimeMinutes, string body) + { + var markdown = $"{summary}\n\n{body}\n\nManagers do not need more slogans. They need clearer mechanisms for decision quality and execution speed."; + return new Essay + { + Slug = slug, + Title = title, + Summary = summary, + Category = category, + ReadTimeMinutes = readTimeMinutes, + MarkdownBody = markdown, + RenderedHtml = _markdown.Render(markdown), + IsPublished = true, + PublishedOnUtc = DateTime.UtcNow, + SortOrder = 0 + }; + } + + private FrameworkModel SeedModel(string slug, string title, string summary) + { + var markdown = $"{summary}\n\nUse this model to support diagnosis first, intervention second."; + return new FrameworkModel + { + Slug = slug, + Title = title, + Summary = summary, + MarkdownBody = markdown, + RenderedHtml = _markdown.Render(markdown), + IsPublished = true, + PublishedOnUtc = DateTime.UtcNow, + SortOrder = 0 + }; + } + + private Tool SeedTool(string slug, string title, string toolType, string duration, string summary) + { + var markdown = $"{summary}\n\nRun this with your team and document disagreements before action planning."; + return new Tool + { + Slug = slug, + Title = title, + Summary = summary, + ToolType = toolType, + EstimatedDuration = duration, + MarkdownBody = markdown, + RenderedHtml = _markdown.Render(markdown), + IsPublished = true, + PublishedOnUtc = DateTime.UtcNow, + SortOrder = 0 + }; + } + + private MediaItem SeedMedia(string slug, string title, string summary) + { + var markdown = $"{summary}\n\nDesigned for leaders who need a quick, direct framing before deeper reading."; + return new MediaItem + { + Slug = slug, + Title = title, + Summary = summary, + MediaType = "video", + MarkdownBody = markdown, + RenderedHtml = _markdown.Render(markdown), + IsPublished = true, + PublishedOnUtc = DateTime.UtcNow, + SortOrder = 0 + }; + } + + private StaticPage SeedPage(string slug, string title, string markdown) + { + return new StaticPage + { + Slug = slug, + Title = title, + MarkdownBody = markdown, + RenderedHtml = _markdown.Render(markdown), + IsPublished = true, + PublishedOnUtc = DateTime.UtcNow + }; + } +} diff --git a/Models/ArchiveModule.cs b/Models/ArchiveModule.cs deleted file mode 100644 index 650747c..0000000 --- a/Models/ArchiveModule.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class ArchiveModule -{ - public string Code { get; set; } = string.Empty; - public string Label { get; set; } = string.Empty; - public string Discipline { get; set; } = string.Empty; - public string Year { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; -} diff --git a/Models/Domain/EntityBase.cs b/Models/Domain/EntityBase.cs new file mode 100644 index 0000000..b703a53 --- /dev/null +++ b/Models/Domain/EntityBase.cs @@ -0,0 +1,8 @@ +namespace manpreetsingh.pro.Models.Domain; + +public abstract class EntityBase +{ + public int Id { get; set; } + public DateTime CreatedUtc { get; set; } = DateTime.UtcNow; + public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow; +} diff --git a/Models/Domain/Essay.cs b/Models/Domain/Essay.cs new file mode 100644 index 0000000..6d725da --- /dev/null +++ b/Models/Domain/Essay.cs @@ -0,0 +1,16 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class Essay : PublishableEntityBase +{ + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string RenderedHtml { get; set; } = string.Empty; + public int ReadTimeMinutes { get; set; } + public string Category { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + public int? HeroAssetId { get; set; } + public UploadedAsset? HeroAsset { get; set; } +} diff --git a/Models/Domain/FrameworkModel.cs b/Models/Domain/FrameworkModel.cs new file mode 100644 index 0000000..8279a97 --- /dev/null +++ b/Models/Domain/FrameworkModel.cs @@ -0,0 +1,14 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class FrameworkModel : PublishableEntityBase +{ + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string RenderedHtml { get; set; } = string.Empty; + public int? DiagramAssetId { get; set; } + public UploadedAsset? DiagramAsset { get; set; } + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } +} diff --git a/Models/Domain/MediaItem.cs b/Models/Domain/MediaItem.cs new file mode 100644 index 0000000..0659e0f --- /dev/null +++ b/Models/Domain/MediaItem.cs @@ -0,0 +1,16 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class MediaItem : PublishableEntityBase +{ + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string RenderedHtml { get; set; } = string.Empty; + public string MediaType { get; set; } = string.Empty; + public string? Duration { get; set; } + public int? VideoAssetId { get; set; } + public UploadedAsset? VideoAsset { get; set; } + public int? ThumbnailAssetId { get; set; } + public UploadedAsset? ThumbnailAsset { get; set; } +} diff --git a/Models/Domain/PublishableEntityBase.cs b/Models/Domain/PublishableEntityBase.cs new file mode 100644 index 0000000..a0d1909 --- /dev/null +++ b/Models/Domain/PublishableEntityBase.cs @@ -0,0 +1,8 @@ +namespace manpreetsingh.pro.Models.Domain; + +public abstract class PublishableEntityBase : EntityBase +{ + public bool IsPublished { get; set; } + public DateTime? PublishedOnUtc { get; set; } + public int SortOrder { get; set; } +} diff --git a/Models/Domain/SiteSetting.cs b/Models/Domain/SiteSetting.cs new file mode 100644 index 0000000..5e8499b --- /dev/null +++ b/Models/Domain/SiteSetting.cs @@ -0,0 +1,7 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class SiteSetting : EntityBase +{ + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; +} diff --git a/Models/Domain/StaticPage.cs b/Models/Domain/StaticPage.cs new file mode 100644 index 0000000..bf05a64 --- /dev/null +++ b/Models/Domain/StaticPage.cs @@ -0,0 +1,13 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class StaticPage : EntityBase +{ + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string RenderedHtml { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + public bool IsPublished { get; set; } + public DateTime? PublishedOnUtc { get; set; } +} diff --git a/Models/Domain/Tool.cs b/Models/Domain/Tool.cs new file mode 100644 index 0000000..31b0807 --- /dev/null +++ b/Models/Domain/Tool.cs @@ -0,0 +1,16 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class Tool : PublishableEntityBase +{ + public string Slug { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + public string MarkdownBody { get; set; } = string.Empty; + public string RenderedHtml { get; set; } = string.Empty; + public string ToolType { get; set; } = string.Empty; + public string EstimatedDuration { get; set; } = string.Empty; + public string? SeoTitle { get; set; } + public string? SeoDescription { get; set; } + + public List Questions { get; set; } = new(); +} diff --git a/Models/Domain/ToolQuestion.cs b/Models/Domain/ToolQuestion.cs new file mode 100644 index 0000000..0c7630b --- /dev/null +++ b/Models/Domain/ToolQuestion.cs @@ -0,0 +1,12 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class ToolQuestion : EntityBase +{ + public int ToolId { get; set; } + public Tool? Tool { get; set; } + public string Prompt { get; set; } = string.Empty; + public string? HelpText { get; set; } + public int SortOrder { get; set; } + public int MinScore { get; set; } + public int MaxScore { get; set; } +} diff --git a/Models/Domain/UploadedAsset.cs b/Models/Domain/UploadedAsset.cs new file mode 100644 index 0000000..f782fb5 --- /dev/null +++ b/Models/Domain/UploadedAsset.cs @@ -0,0 +1,12 @@ +namespace manpreetsingh.pro.Models.Domain; + +public class UploadedAsset : EntityBase +{ + public string FileName { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public byte[] Data { get; set; } = Array.Empty(); + public long Length { get; set; } + public string? AltText { get; set; } + public string? Caption { get; set; } + public string AssetKind { get; set; } = "image"; +} diff --git a/Models/EssayItem.cs b/Models/EssayItem.cs deleted file mode 100644 index e25bb39..0000000 --- a/Models/EssayItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class EssayItem -{ - public string Title { get; set; } = string.Empty; - public string Date { get; set; } = string.Empty; - public string ReadTime { get; set; } = string.Empty; - public string Category { get; set; } = string.Empty; -} diff --git a/Models/FeaturedWorkItem.cs b/Models/FeaturedWorkItem.cs deleted file mode 100644 index 1a24968..0000000 --- a/Models/FeaturedWorkItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class FeaturedWorkItem -{ - public string Title { get; set; } = string.Empty; - public string Category { get; set; } = string.Empty; - public string Year { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string ImageUrl { get; set; } = string.Empty; - public string AltText { get; set; } = string.Empty; -} diff --git a/Models/Identity/ApplicationUser.cs b/Models/Identity/ApplicationUser.cs new file mode 100644 index 0000000..092c3ad --- /dev/null +++ b/Models/Identity/ApplicationUser.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Identity; + +namespace manpreetsingh.pro.Models.Identity; + +public class ApplicationUser : IdentityUser +{ + public DateTime CreatedUtc { get; set; } = DateTime.UtcNow; +} diff --git a/Models/MediaItem.cs b/Models/MediaItem.cs deleted file mode 100644 index 07c5abc..0000000 --- a/Models/MediaItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class MediaItem -{ - public string Title { get; set; } = string.Empty; - public string Format { get; set; } = string.Empty; - public string Duration { get; set; } = string.Empty; - public string Summary { get; set; } = string.Empty; -} diff --git a/Models/ModelItem.cs b/Models/ModelItem.cs deleted file mode 100644 index af9b905..0000000 --- a/Models/ModelItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class ModelItem -{ - public string Name { get; set; } = string.Empty; - public string Slug { get; set; } = string.Empty; - public string Summary { get; set; } = string.Empty; - public string CoreQuestion { get; set; } = string.Empty; -} diff --git a/Models/ToolItem.cs b/Models/ToolItem.cs deleted file mode 100644 index b145728..0000000 --- a/Models/ToolItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class ToolItem -{ - public string Name { get; set; } = string.Empty; - public string Slug { get; set; } = string.Empty; - public string Summary { get; set; } = string.Empty; -} diff --git a/Models/WritingItem.cs b/Models/WritingItem.cs deleted file mode 100644 index 35f44e7..0000000 --- a/Models/WritingItem.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace manpreetsingh.pro.Models; - -public class WritingItem -{ - public string Title { get; set; } = string.Empty; - public string Slug { get; set; } = string.Empty; - public string Summary { get; set; } = string.Empty; - public string PublishedOn { get; set; } = string.Empty; - public string ReadTime { get; set; } = string.Empty; -} diff --git a/Program.cs b/Program.cs index ed90792..98f9050 100644 --- a/Program.cs +++ b/Program.cs @@ -1,7 +1,48 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Data.Seed; +using manpreetsingh.pro.Models.Identity; +using manpreetsingh.pro.Services.Assets; +using manpreetsingh.pro.Services.Markdown; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); +builder.Services.AddDbContext(options => +{ + if (builder.Environment.IsDevelopment()) + { + options.UseInMemoryDatabase("manpreetsingh_dev"); + } + else + { + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("PostgreSQL connection string missing."); + options.UseNpgsql(connectionString); + } +}); + +builder.Services + .AddIdentity(options => + { + options.Password.RequireDigit = true; + options.Password.RequireUppercase = true; + options.Password.RequiredLength = 8; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +builder.Services.ConfigureApplicationCookie(options => +{ + options.LoginPath = "/account/login"; +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); if (!app.Environment.IsDevelopment()) @@ -12,11 +53,20 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseRouting(); - +app.UseAuthentication(); app.UseAuthorization(); +using (var scope = app.Services.CreateScope()) +{ + var seeder = scope.ServiceProvider.GetRequiredService(); + await seeder.SeedAsync(); +} + +app.MapControllerRoute( + name: "areas", + pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); + app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); diff --git a/Services/Assets/AssetService.cs b/Services/Assets/AssetService.cs new file mode 100644 index 0000000..23aaea9 --- /dev/null +++ b/Services/Assets/AssetService.cs @@ -0,0 +1,37 @@ +using manpreetsingh.pro.Data; +using manpreetsingh.pro.Models.Domain; + +namespace manpreetsingh.pro.Services.Assets; + +public class AssetService : IAssetService +{ + private readonly ApplicationDbContext _db; + + public AssetService(ApplicationDbContext db) + { + _db = db; + } + + public async Task SaveAsync(IFormFile file, string? altText, string? caption, string assetKind) + { + await using var ms = new MemoryStream(); + await file.CopyToAsync(ms); + + var asset = new UploadedAsset + { + FileName = Path.GetFileName(file.FileName), + ContentType = file.ContentType, + Data = ms.ToArray(), + Length = file.Length, + AltText = altText, + Caption = caption, + AssetKind = assetKind, + CreatedUtc = DateTime.UtcNow, + UpdatedUtc = DateTime.UtcNow + }; + + _db.UploadedAssets.Add(asset); + await _db.SaveChangesAsync(); + return asset; + } +} diff --git a/Services/Assets/IAssetService.cs b/Services/Assets/IAssetService.cs new file mode 100644 index 0000000..2c24ebc --- /dev/null +++ b/Services/Assets/IAssetService.cs @@ -0,0 +1,8 @@ +using manpreetsingh.pro.Models.Domain; + +namespace manpreetsingh.pro.Services.Assets; + +public interface IAssetService +{ + Task SaveAsync(IFormFile file, string? altText, string? caption, string assetKind); +} diff --git a/Services/Markdown/IMarkdownRenderer.cs b/Services/Markdown/IMarkdownRenderer.cs new file mode 100644 index 0000000..0e3ae1c --- /dev/null +++ b/Services/Markdown/IMarkdownRenderer.cs @@ -0,0 +1,6 @@ +namespace manpreetsingh.pro.Services.Markdown; + +public interface IMarkdownRenderer +{ + string Render(string markdown); +} diff --git a/Services/Markdown/MarkdownRenderer.cs b/Services/Markdown/MarkdownRenderer.cs new file mode 100644 index 0000000..e2a3085 --- /dev/null +++ b/Services/Markdown/MarkdownRenderer.cs @@ -0,0 +1,15 @@ +using Markdig; + +namespace manpreetsingh.pro.Services.Markdown; + +public class MarkdownRenderer : IMarkdownRenderer +{ + private readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder() + .DisableHtml() + .UseAdvancedExtensions() + .Build(); + + public string Render(string markdown) => string.IsNullOrWhiteSpace(markdown) + ? string.Empty + : Markdig.Markdown.ToHtml(markdown, _pipeline); +} diff --git a/ViewModels/ContentDetailViewModel.cs b/ViewModels/ContentDetailViewModel.cs deleted file mode 100644 index 6a53c1e..0000000 --- a/ViewModels/ContentDetailViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace manpreetsingh.pro.ViewModels; - -public class ContentDetailViewModel -{ - public string SectionLabel { get; set; } = string.Empty; - public string Title { get; set; } = string.Empty; - public string Summary { get; set; } = string.Empty; - public IReadOnlyList Points { get; set; } = []; -} diff --git a/ViewModels/DetailPageViewModel.cs b/ViewModels/DetailPageViewModel.cs new file mode 100644 index 0000000..5ba2bb9 --- /dev/null +++ b/ViewModels/DetailPageViewModel.cs @@ -0,0 +1,6 @@ +namespace manpreetsingh.pro.ViewModels; + +public class DetailPageViewModel +{ + public T Item { get; set; } = default!; +} diff --git a/ViewModels/HomePageViewModel.cs b/ViewModels/HomePageViewModel.cs index 0782112..f09412c 100644 --- a/ViewModels/HomePageViewModel.cs +++ b/ViewModels/HomePageViewModel.cs @@ -1,17 +1,13 @@ -using manpreetsingh.pro.Models; +using manpreetsingh.pro.Models.Domain; namespace manpreetsingh.pro.ViewModels; public class HomePageViewModel { - public string HeroTitle { get; set; } = string.Empty; - public string HeroSubtext { get; set; } = string.Empty; - public string WhyGapIntro { get; set; } = string.Empty; - public IReadOnlyList WhyGapPoints { get; set; } = []; - public IReadOnlyList FeaturedModels { get; set; } = []; - public IReadOnlyList SelectedWritings { get; set; } = []; - public IReadOnlyList Tools { get; set; } = []; + public IReadOnlyList SelectedWritings { get; set; } = []; + public IReadOnlyList FeaturedModels { get; set; } = []; + public IReadOnlyList Tools { get; set; } = []; public IReadOnlyList MediaItems { get; set; } = []; - public string AboutSummary { get; set; } = string.Empty; - public string ContactSummary { get; set; } = string.Empty; + public StaticPage? AboutPage { get; set; } + public StaticPage? ContactPage { get; set; } } diff --git a/ViewModels/ListPageViewModel.cs b/ViewModels/ListPageViewModel.cs new file mode 100644 index 0000000..8e69ccb --- /dev/null +++ b/ViewModels/ListPageViewModel.cs @@ -0,0 +1,7 @@ +namespace manpreetsingh.pro.ViewModels; + +public class ListPageViewModel +{ + public string Title { get; set; } = string.Empty; + public IReadOnlyList Items { get; set; } = []; +} diff --git a/ViewModels/MediaPageViewModel.cs b/ViewModels/MediaPageViewModel.cs deleted file mode 100644 index 33df9cf..0000000 --- a/ViewModels/MediaPageViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using manpreetsingh.pro.Models; - -namespace manpreetsingh.pro.ViewModels; - -public class MediaPageViewModel -{ - public string Title { get; set; } = "Media"; - public string Intro { get; set; } = string.Empty; - public IReadOnlyList Items { get; set; } = []; -} diff --git a/ViewModels/ModelsPageViewModel.cs b/ViewModels/ModelsPageViewModel.cs deleted file mode 100644 index 3d39913..0000000 --- a/ViewModels/ModelsPageViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using manpreetsingh.pro.Models; - -namespace manpreetsingh.pro.ViewModels; - -public class ModelsPageViewModel -{ - public string Title { get; set; } = "Models"; - public string Intro { get; set; } = string.Empty; - public IReadOnlyList Items { get; set; } = []; -} diff --git a/ViewModels/SimplePageViewModel.cs b/ViewModels/SimplePageViewModel.cs deleted file mode 100644 index 7aae050..0000000 --- a/ViewModels/SimplePageViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace manpreetsingh.pro.ViewModels; - -public class SimplePageViewModel -{ - public string Title { get; set; } = string.Empty; - public string Intro { get; set; } = string.Empty; - public IReadOnlyList Details { get; set; } = []; -} diff --git a/ViewModels/StaticPageViewModel.cs b/ViewModels/StaticPageViewModel.cs new file mode 100644 index 0000000..de4a7fc --- /dev/null +++ b/ViewModels/StaticPageViewModel.cs @@ -0,0 +1,8 @@ +using manpreetsingh.pro.Models.Domain; + +namespace manpreetsingh.pro.ViewModels; + +public class StaticPageViewModel +{ + public StaticPage Page { get; set; } = default!; +} diff --git a/ViewModels/ToolsPageViewModel.cs b/ViewModels/ToolsPageViewModel.cs deleted file mode 100644 index a733ce3..0000000 --- a/ViewModels/ToolsPageViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using manpreetsingh.pro.Models; - -namespace manpreetsingh.pro.ViewModels; - -public class ToolsPageViewModel -{ - public string Title { get; set; } = "Tools"; - public string Intro { get; set; } = string.Empty; - public IReadOnlyList Items { get; set; } = []; -} diff --git a/ViewModels/WritingsPageViewModel.cs b/ViewModels/WritingsPageViewModel.cs deleted file mode 100644 index f40d2ba..0000000 --- a/ViewModels/WritingsPageViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using manpreetsingh.pro.Models; - -namespace manpreetsingh.pro.ViewModels; - -public class WritingsPageViewModel -{ - public string Title { get; set; } = "Writings"; - public string Intro { get; set; } = string.Empty; - public IReadOnlyList Items { get; set; } = []; -} diff --git a/Views/Account/Login.cshtml b/Views/Account/Login.cshtml new file mode 100644 index 0000000..16831d1 --- /dev/null +++ b/Views/Account/Login.cshtml @@ -0,0 +1,10 @@ +@model manpreetsingh.pro.Areas.Admin.ViewModels.LoginViewModel +

Admin Login

+
+ @Html.AntiForgeryToken() + + + + + +
diff --git a/Views/Home/About.cshtml b/Views/Home/About.cshtml deleted file mode 100644 index 408fa4d..0000000 --- a/Views/Home/About.cshtml +++ /dev/null @@ -1,17 +0,0 @@ -@model SimplePageViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

About

-

@Model.Title

-

@Model.Intro

-
-
-
    - @foreach (var line in Model.Details) - { -
  • @line
  • - } -
-
diff --git a/Views/Home/Contact.cshtml b/Views/Home/Contact.cshtml deleted file mode 100644 index d79ab7e..0000000 --- a/Views/Home/Contact.cshtml +++ /dev/null @@ -1,17 +0,0 @@ -@model SimplePageViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

Contact

-

@Model.Title

-

@Model.Intro

-
-
-
    - @foreach (var line in Model.Details) - { -
  • @line
  • - } -
-
diff --git a/Views/Home/ContentDetail.cshtml b/Views/Home/ContentDetail.cshtml deleted file mode 100644 index 9a7f1fa..0000000 --- a/Views/Home/ContentDetail.cshtml +++ /dev/null @@ -1,17 +0,0 @@ -@model ContentDetailViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

@Model.SectionLabel

-

@Model.Title

-

@Model.Summary

-
-
-
    - @foreach (var point in Model.Points) - { -
  • @point
  • - } -
-
diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 9c60b6a..2ccf8c0 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,13 +1,65 @@ @model HomePageViewModel -@{ - ViewData["Title"] = "Home"; -} - -@await Html.PartialAsync("_Hero", Model) -@await Html.PartialAsync("_WhyGapIntro", Model) -@await Html.PartialAsync("_FeaturedModels", Model) -@await Html.PartialAsync("_SelectedWritings", Model) -@await Html.PartialAsync("_ToolsPreview", Model) -@await Html.PartialAsync("_MediaPreview", Model) -@await Html.PartialAsync("_AboutPreview", Model) -@await Html.PartialAsync("_ContactPreview", Model) +
+ Issue 01 +

How Organizations Really Work

+

Understanding the forces behind decisions, hierarchy, delay, and execution.

+
+
+ Core Thesis +

The Why Gap

+

Most execution failures begin when people cannot explain why work matters. When context thins out, coordination slows and standards drift.

+
+
+ Models +

Featured Models

+ @foreach (var item in Model.FeaturedModels) + { + + } +
+
+ Writings +

Selected Writings

+ @foreach (var item in Model.SelectedWritings) + { + + } +
+
+ Tools +

Practical Checks

+ @foreach (var item in Model.Tools) + { + + } +
+
+ Media +

Briefings

+ @foreach (var item in Model.MediaItems) + { +
+

@item.Title

+

@item.Summary

+
+ } +
+
+ About +

About

+
@Html.Raw(Model.AboutPage?.RenderedHtml)
+
+
+ Contact +

Contact

+
@Html.Raw(Model.ContactPage?.RenderedHtml)
+
diff --git a/Views/Home/Media.cshtml b/Views/Home/Media.cshtml deleted file mode 100644 index da411f8..0000000 --- a/Views/Home/Media.cshtml +++ /dev/null @@ -1,24 +0,0 @@ -@model MediaPageViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

Media

-

@Model.Title

-

@Model.Intro

-
-
-
- @foreach (var item in Model.Items) - { -
-

@item.Title

-

@item.Summary

-
- @item.Format - @item.Duration -
-
- } -
-
diff --git a/Views/Home/Models.cshtml b/Views/Home/Models.cshtml deleted file mode 100644 index 3a4121d..0000000 --- a/Views/Home/Models.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@model ModelsPageViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

Models

-

@Model.Title

-

@Model.Intro

-
-
-
- @foreach (var item in Model.Items) - { -
-

@item.Name

-

@item.Summary

-

@item.CoreQuestion

-
- } -
-
diff --git a/Views/Home/Tools.cshtml b/Views/Home/Tools.cshtml deleted file mode 100644 index 908e1c0..0000000 --- a/Views/Home/Tools.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@model ToolsPageViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

Tools

-

@Model.Title

-

@Model.Intro

-
-
-
- @foreach (var item in Model.Items) - { - - } -
-
diff --git a/Views/Home/Writings.cshtml b/Views/Home/Writings.cshtml deleted file mode 100644 index 37ccbdc..0000000 --- a/Views/Home/Writings.cshtml +++ /dev/null @@ -1,24 +0,0 @@ -@model WritingsPageViewModel -@{ - ViewData["Title"] = Model.Title; -} -
-

Writings

-

@Model.Title

-

@Model.Intro

-
-
-
- @foreach (var item in Model.Items) - { -
-

@item.Title

-

@item.Summary

-
- @item.PublishedOn - @item.ReadTime -
-
- } -
-
diff --git a/Views/Media/Index.cshtml b/Views/Media/Index.cshtml new file mode 100644 index 0000000..e367339 --- /dev/null +++ b/Views/Media/Index.cshtml @@ -0,0 +1,17 @@ +@model ListPageViewModel +
+ Media +

@Model.Title

+

Short editorial briefings for managers under time pressure.

+ @foreach (var item in Model.Items) + { +
+

@item.Title

+

@item.Summary

+ @if (item.ThumbnailAssetId.HasValue) + { + @item.ThumbnailAsset?.AltText + } +
+ } +
diff --git a/Views/Models/Detail.cshtml b/Views/Models/Detail.cshtml new file mode 100644 index 0000000..20182e6 --- /dev/null +++ b/Views/Models/Detail.cshtml @@ -0,0 +1,11 @@ +@model DetailPageViewModel +
+ Model +

@Model.Item.Title

+

@Model.Item.Summary

+ @if (Model.Item.DiagramAssetId.HasValue) + { + @Model.Item.DiagramAsset?.AltText + } +
@Html.Raw(Model.Item.RenderedHtml)
+
diff --git a/Views/Models/Index.cshtml b/Views/Models/Index.cshtml new file mode 100644 index 0000000..e3d2d90 --- /dev/null +++ b/Views/Models/Index.cshtml @@ -0,0 +1,13 @@ +@model ListPageViewModel +
+ Library +

@Model.Title

+

Operational models for diagnosing where execution gets blocked.

+ @foreach (var item in Model.Items) + { + + } +
diff --git a/Views/Pages/Page.cshtml b/Views/Pages/Page.cshtml new file mode 100644 index 0000000..8ba863d --- /dev/null +++ b/Views/Pages/Page.cshtml @@ -0,0 +1,6 @@ +@model StaticPageViewModel +
+ Page +

@Model.Page.Title

+
@Html.Raw(Model.Page.RenderedHtml)
+
diff --git a/Views/Shared/_AboutPreview.cshtml b/Views/Shared/_AboutPreview.cshtml deleted file mode 100644 index b10bc9d..0000000 --- a/Views/Shared/_AboutPreview.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@model HomePageViewModel -
-
-

06

-

About

-
-

@Model.AboutSummary

-

Read full about

-
diff --git a/Views/Shared/_AboutSection.cshtml b/Views/Shared/_AboutSection.cshtml deleted file mode 100644 index 709c378..0000000 --- a/Views/Shared/_AboutSection.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@model HomePageViewModel -
-
-

05

-

@Model.AboutTitle

-
-
-

@Model.AboutBody

-
    - @foreach (var capability in Model.Capabilities) - { -
  • @capability
  • - } -
-
-
diff --git a/Views/Shared/_ArchiveIndex.cshtml b/Views/Shared/_ArchiveIndex.cshtml deleted file mode 100644 index 5219e69..0000000 --- a/Views/Shared/_ArchiveIndex.cshtml +++ /dev/null @@ -1,26 +0,0 @@ -@model HomePageViewModel -
-
-

04

-

Archive Index

-
-
-
- Code - Module - Discipline - Year - Status -
- @foreach (var module in Model.ArchiveModules) - { -
- @module.Code - @module.Label - @module.Discipline - @module.Year - @module.Status -
- } -
-
diff --git a/Views/Shared/_ConceptSection.cshtml b/Views/Shared/_ConceptSection.cshtml deleted file mode 100644 index 931cbd2..0000000 --- a/Views/Shared/_ConceptSection.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -
-
-

03

-

Concept

-
-

- The practice combines editorial restraint with kinetic behavior. Structure first, ornament never; nuance through proportion, timing, and selective contrast. -

-
diff --git a/Views/Shared/_ContactPreview.cshtml b/Views/Shared/_ContactPreview.cshtml deleted file mode 100644 index f8cb512..0000000 --- a/Views/Shared/_ContactPreview.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@model HomePageViewModel -
-
-

07

-

Contact / engagement

-
-

@Model.ContactSummary

-

Contact details

-
diff --git a/Views/Shared/_ContactSection.cshtml b/Views/Shared/_ContactSection.cshtml deleted file mode 100644 index b45ef0f..0000000 --- a/Views/Shared/_ContactSection.cshtml +++ /dev/null @@ -1,12 +0,0 @@ -@model HomePageViewModel -
-
-

07

-

Contact

-
-
- @Model.ContactEmail - @Model.ContactPhone -

@Model.ContactLocation

-
-
diff --git a/Views/Shared/_FeaturedModels.cshtml b/Views/Shared/_FeaturedModels.cshtml deleted file mode 100644 index e0b2ef6..0000000 --- a/Views/Shared/_FeaturedModels.cshtml +++ /dev/null @@ -1,17 +0,0 @@ -@model HomePageViewModel -
-
-

02

- -
-
- @foreach (var model in Model.FeaturedModels) - { -
-

@model.Name

-

@model.Summary

-

@model.CoreQuestion

-
- } -
-
diff --git a/Views/Shared/_FeaturedWorks.cshtml b/Views/Shared/_FeaturedWorks.cshtml deleted file mode 100644 index 7e44d47..0000000 --- a/Views/Shared/_FeaturedWorks.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@model HomePageViewModel - diff --git a/Views/Shared/_Footer.cshtml b/Views/Shared/_Footer.cshtml deleted file mode 100644 index 4973ada..0000000 --- a/Views/Shared/_Footer.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -
-

Manpreet Singh · How organizations really work.

-
diff --git a/Views/Shared/_Header.cshtml b/Views/Shared/_Header.cshtml deleted file mode 100644 index 4b2d12e..0000000 --- a/Views/Shared/_Header.cshtml +++ /dev/null @@ -1,24 +0,0 @@ - diff --git a/Views/Shared/_Hero.cshtml b/Views/Shared/_Hero.cshtml deleted file mode 100644 index 1210314..0000000 --- a/Views/Shared/_Hero.cshtml +++ /dev/null @@ -1,11 +0,0 @@ -@model HomePageViewModel -
-
- -

manpreet singh

-
-
-

@Model.HeroTitle

-

@Model.HeroSubtext

-
-
diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 11349c9..c010d87 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,18 +1,29 @@ - + - @ViewData["Title"] - Manpreet Singh + Manpreet Singh - - @await Html.PartialAsync("_Header") -
- @RenderBody() -
- @await Html.PartialAsync("_Footer") - + +
@RenderBody()
+
Manpreet Singh · Editorial platform for middle and senior managers.
+ diff --git a/Views/Shared/_Manifesto.cshtml b/Views/Shared/_Manifesto.cshtml deleted file mode 100644 index 029782e..0000000 --- a/Views/Shared/_Manifesto.cshtml +++ /dev/null @@ -1,13 +0,0 @@ -@model HomePageViewModel -
-
-

01

-

@Model.ManifestoTitle

-
-
    - @foreach (var line in Model.ManifestoLines) - { -
  1. @line
  2. - } -
-
diff --git a/Views/Shared/_MediaPreview.cshtml b/Views/Shared/_MediaPreview.cshtml deleted file mode 100644 index 8f20a7d..0000000 --- a/Views/Shared/_MediaPreview.cshtml +++ /dev/null @@ -1,19 +0,0 @@ -@model HomePageViewModel -
-
-

05

-

Media preview

-
-
- @foreach (var item in Model.MediaItems) - { - - } -
-
diff --git a/Views/Shared/_SelectedWritings.cshtml b/Views/Shared/_SelectedWritings.cshtml deleted file mode 100644 index fdbd52c..0000000 --- a/Views/Shared/_SelectedWritings.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@model HomePageViewModel -
-
-

03

-

Selected writings

-
-
- @for (var i = 0; i < Math.Min(3, Model.SelectedWritings.Count); i++) - { - var item = Model.SelectedWritings[i]; -
-

@item.Title

-
- @item.PublishedOn - @item.ReadTime -
-
- } -
-
diff --git a/Views/Shared/_ThoughtsSection.cshtml b/Views/Shared/_ThoughtsSection.cshtml deleted file mode 100644 index 7ae117f..0000000 --- a/Views/Shared/_ThoughtsSection.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@model HomePageViewModel -
-
-

06

-

Thoughts & Writings

-
-
- @foreach (var essay in Model.Essays) - { -
-

@essay.Title

-
- @essay.Date - @essay.ReadTime - @essay.Category -
-
- } -
-
diff --git a/Views/Shared/_ToolsPreview.cshtml b/Views/Shared/_ToolsPreview.cshtml deleted file mode 100644 index efe5440..0000000 --- a/Views/Shared/_ToolsPreview.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@model HomePageViewModel -
-
-

04

-

Tools preview

-
-
- @foreach (var item in Model.Tools) - { - - } -
-
diff --git a/Views/Shared/_WhyGapIntro.cshtml b/Views/Shared/_WhyGapIntro.cshtml deleted file mode 100644 index 3fbb4b1..0000000 --- a/Views/Shared/_WhyGapIntro.cshtml +++ /dev/null @@ -1,14 +0,0 @@ -@model HomePageViewModel -
-
-

01

-

Why Gap

-
-

@Model.WhyGapIntro

-
    - @foreach (var point in Model.WhyGapPoints) - { -
  • @point
  • - } -
-
diff --git a/Views/Tools/Detail.cshtml b/Views/Tools/Detail.cshtml new file mode 100644 index 0000000..6e11713 --- /dev/null +++ b/Views/Tools/Detail.cshtml @@ -0,0 +1,14 @@ +@model DetailPageViewModel +
+ Tool +

@Model.Item.Title

+

@Model.Item.Summary

+
@Html.Raw(Model.Item.RenderedHtml)
+

Questions

+
    + @foreach (var q in Model.Item.Questions.OrderBy(x => x.SortOrder)) + { +
  • @q.Prompt
  • + } +
+
diff --git a/Views/Tools/Index.cshtml b/Views/Tools/Index.cshtml new file mode 100644 index 0000000..4a34df2 --- /dev/null +++ b/Views/Tools/Index.cshtml @@ -0,0 +1,14 @@ +@model ListPageViewModel +
+ Practice +

@Model.Title

+

Simple managerial checks designed for quick use in real teams.

+ @foreach (var item in Model.Items) + { +
+

@item.Title

+

@item.Summary

+ @item.ToolType · @item.EstimatedDuration +
+ } +
diff --git a/Views/Writings/Detail.cshtml b/Views/Writings/Detail.cshtml new file mode 100644 index 0000000..a8a99cf --- /dev/null +++ b/Views/Writings/Detail.cshtml @@ -0,0 +1,11 @@ +@model DetailPageViewModel +
+ Writing +

@Model.Item.Title

+

@Model.Item.Summary

+ @if (Model.Item.HeroAssetId.HasValue) + { + @Model.Item.HeroAsset?.AltText + } +
@Html.Raw(Model.Item.RenderedHtml)
+
diff --git a/Views/Writings/Index.cshtml b/Views/Writings/Index.cshtml new file mode 100644 index 0000000..32636fb --- /dev/null +++ b/Views/Writings/Index.cshtml @@ -0,0 +1,14 @@ +@model ListPageViewModel +
+ Index +

@Model.Title

+

Essays on decisions, incentives, execution, and organizational delay.

+ @foreach (var item in Model.Items) + { +
+

@item.Title

+

@item.Summary

+ @item.ReadTimeMinutes min · @item.Category +
+ } +
diff --git a/Views/_ViewImports.cshtml b/Views/_ViewImports.cshtml index e2f0a47..8a92d96 100644 --- a/Views/_ViewImports.cshtml +++ b/Views/_ViewImports.cshtml @@ -1,4 +1,4 @@ @using manpreetsingh.pro -@using manpreetsingh.pro.Models +@using manpreetsingh.pro.Models.Domain @using manpreetsingh.pro.ViewModels @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..e16f43d --- /dev/null +++ b/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=manpreetsingh;Username=postgres;Password=postgres" + }, + "Seed": { + "AdminPassword": "Password123!" + } +} diff --git a/manpreetsingh.pro.csproj b/manpreetsingh.pro.csproj index 24ec0e8..e2e088a 100644 --- a/manpreetsingh.pro.csproj +++ b/manpreetsingh.pro.csproj @@ -4,4 +4,15 @@ enable enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css index b67c11b..2d023f3 100644 --- a/wwwroot/css/site.css +++ b/wwwroot/css/site.css @@ -1,330 +1,250 @@ :root { - --bg: #f5f5f3; - --surface: #efefec; - --text: #111111; - --muted: #5b5b5b; - --border: #16161633; - --accent: #2b2b2b; - --accent-2: #4a4a4a; - --space-1: clamp(0.6rem, 0.55rem + 0.2vw, 0.8rem); - --space-2: clamp(1rem, 0.8rem + 0.5vw, 1.4rem); - --space-3: clamp(1.8rem, 1.3rem + 1vw, 2.8rem); - --space-4: clamp(2.8rem, 2rem + 2vw, 5rem); - --space-5: clamp(4rem, 3rem + 3vw, 8rem); + --bg: #f4f4f2; + --surface: #f8f8f6; + --fg: #141414; + --muted: #666662; + --line: #cbcbc5; + --logo-bg: #f4f4f2; + --max: 1040px; + --display: clamp(2rem, 5vw, 4.2rem); + --h1: clamp(1.6rem, 3vw, 2.3rem); + --h2: clamp(1.3rem, 2vw, 1.65rem); + --body: 1.02rem; +} + +:root[data-theme='dark'] { + --bg: #111110; + --surface: #171716; + --fg: #efefed; + --muted: #a5a59f; + --line: #2d2d2a; + --logo-bg: #111110; } -@media (prefers-color-scheme: dark) { - :root { - --bg: #0f0f0f; - --surface: #171717; - --text: #ececec; - --muted: #a9a9a9; - --border: #ffffff24; - } -} - -html[data-theme='light'] { - --bg: #f5f5f3; - --surface: #efefec; - --text: #111111; - --muted: #5b5b5b; - --border: #16161633; -} - -html[data-theme='dark'] { - --bg: #0f0f0f; - --surface: #171717; - --text: #ececec; - --muted: #a9a9a9; - --border: #ffffff24; -} - -html[data-accent='graphite'] { --accent: #2b2b2b; --accent-2: #4a4a4a; } -html[data-accent='indigo'] { --accent: #3f4fd6; --accent-2: #7a84e2; } -html[data-accent='oxide'] { --accent: #8f4f39; --accent-2: #c47e64; } - * { box-sizing: border-box; } -html, body { margin: 0; } +html { scroll-behavior: smooth; } body { + margin: 0; background: var(--bg); - color: var(--text); - font-family: Inter, "Helvetica Neue", Arial, sans-serif; - line-height: 1.42; - transition: background-color 220ms ease, color 220ms ease; + color: var(--fg); + font: 400 var(--body)/1.75 "Inter", "Avenir Next", "Segoe UI", sans-serif; + letter-spacing: 0.01em; + text-rendering: optimizeLegibility; + transition: background-color .28s ease, color .28s ease; } -.skip-link { - position: absolute; - left: -9999px; - top: var(--space-1); - padding: 0.5rem 0.7rem; - border: 1px solid var(--text); - background: var(--bg); +.container { + max-width: var(--max); + margin: 0 auto; + padding: 2.25rem 1.4rem 4.5rem; } -.skip-link:focus { left: var(--space-2); } .site-header { position: sticky; top: 0; z-index: 20; - border-bottom: 1px solid var(--border); - background: color-mix(in srgb, var(--bg) 94%, transparent); - backdrop-filter: blur(6px); -} - -.header-wrap, -.site-main, -.site-footer { - width: min(1560px, 100% - clamp(1.5rem, 1rem + 3vw, 5rem)); - margin-inline: auto; -} - -.header-wrap { display: grid; grid-template-columns: auto 1fr auto; - gap: var(--space-2); + gap: 1.2rem; align-items: center; - padding: var(--space-2) 0; + padding: .85rem 1.4rem; + background: color-mix(in oklab, var(--bg) 90%, transparent); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(8px); } -.wordmark { - color: var(--text); +.brand { + display: inline-flex; + align-items: center; + gap: .65rem; text-decoration: none; - font-size: 0.72rem; - letter-spacing: 0.15em; - text-transform: uppercase; + color: var(--fg); } -.main-nav { - list-style: none; - display: flex; - gap: clamp(0.65rem, 0.4rem + 0.8vw, 1.3rem); - margin: 0; - padding: 0; +.brand img { + width: 44px; + height: 44px; + display: block; + color: var(--fg); + transition: transform .2s ease; } -.main-nav a, -.inline-link, -.info-card a, -.line-item a { - color: var(--text); - text-decoration: none; - position: relative; -} +.brand:hover img { transform: translateY(-1px); } -.main-nav a, -.inline-link { - font-size: 0.7rem; - letter-spacing: 0.12em; - text-transform: uppercase; +nav { + display: flex; + flex-wrap: wrap; + gap: .85rem 1rem; + justify-content: center; } -.main-nav a::after, -.inline-link::after, -.info-card a::after, -.line-item a::after { - content: ""; - position: absolute; - left: 0; - bottom: -0.2rem; - width: 100%; - height: 1px; - background: var(--accent); - transform: scaleX(0); - transform-origin: right; - transition: transform 180ms ease, background-color 220ms ease; +nav a, +a { + color: var(--fg); + text-decoration: none; } -.main-nav a:hover::after, -.main-nav a:focus-visible::after, -.inline-link:hover::after, -.inline-link:focus-visible::after, -.info-card a:hover::after, -.info-card a:focus-visible::after, -.line-item a:hover::after, -.line-item a:focus-visible::after { - transform: scaleX(1); - transform-origin: left; +.site-header nav a { + font-size: .76rem; + text-transform: uppercase; + letter-spacing: .12em; + color: var(--muted); + border-bottom: 1px solid transparent; + transition: color .2s ease, border-color .2s ease; } -.header-controls { - display: flex; - align-items: center; - gap: 0.7rem; +.site-header nav a:hover, +.site-header nav a:focus-visible { + color: var(--fg); + border-color: var(--fg); } -.toggle-btn { - border: 1px solid var(--border); +#theme-toggle { + width: auto; + margin: 0; + padding: .45rem .75rem; + border: 1px solid var(--line); background: var(--surface); - color: var(--text); - font-size: 0.68rem; - letter-spacing: 0.1em; + color: var(--fg); + font-size: .68rem; + letter-spacing: .1em; text-transform: uppercase; - padding: 0.35rem 0.5rem; -} - -.accent-switch { display: inline-flex; gap: 0.38rem; } -.accent-switch button { - width: 0.95rem; - height: 0.95rem; - border-radius: 999px; - border: 1px solid var(--border); - padding: 0; - background: var(--surface); -} -.accent-switch [data-accent-option='graphite'] { background: #3a3a3a; } -.accent-switch [data-accent-option='indigo'] { background: #5664d8; } -.accent-switch [data-accent-option='oxide'] { background: #a35d43; } - -.site-main { padding: var(--space-4) 0 var(--space-5); } -section { padding: var(--space-4) 0; border-bottom: 1px solid var(--border); } - -.hero { - display: grid; - grid-template-columns: 1fr 2fr; - gap: var(--space-3); - align-items: end; - min-height: 72vh; } -.identity-block { border-top: 1px solid var(--border); padding-top: var(--space-2); } -.ms-mark { +main > section, +main > article { margin: 0; - font-size: clamp(2rem, 1.4rem + 2.4vw, 3.6rem); - letter-spacing: 0.06em; -} -.identity-name { - margin: 0.4rem 0 0; - color: var(--muted); - text-transform: lowercase; - letter-spacing: 0.05em; + padding: 2.8rem 0; + border-top: 1px solid var(--line); + opacity: 0; + transform: translateY(8px); + animation: fadeUp .45s ease forwards; } +main > section:first-child { border-top: 0; padding-top: 1.4rem; } +h1, h2, h3 { margin: 0 0 .9rem; line-height: 1.15; } h1 { - margin: 0; - font-size: clamp(2rem, 1.3rem + 5.8vw, 7.2rem); - line-height: 0.95; - letter-spacing: 0.01em; + font-size: var(--display); + letter-spacing: .055em; text-transform: uppercase; - max-width: 14ch; + font-weight: 800; } -.thesis-block p, -.page-intro p { max-width: 58ch; margin-top: var(--space-2); } - -.section-head { - display: grid; - grid-template-columns: 3rem 1fr; - gap: 1rem; - align-items: baseline; -} -.kicker { - margin: 0; - color: var(--muted); - font-size: 0.65rem; - letter-spacing: 0.14em; +h2 { + font-size: var(--h1); + letter-spacing: .03em; text-transform: uppercase; + font-weight: 700; +} +h3 { + font-size: var(--h2); + font-weight: 650; + letter-spacing: .01em; } -h2, h3 { margin: 0; font-weight: 550; } -h2 { +p { max-width: 72ch; margin: 0 0 1rem; color: color-mix(in oklab, var(--fg) 86%, var(--muted)); } + +.meta-label { + display: inline-block; + margin-bottom: .85rem; + color: var(--muted); + font-size: .68rem; text-transform: uppercase; - letter-spacing: 0.04em; - font-size: clamp(1.2rem, 1rem + 0.8vw, 1.8rem); + letter-spacing: .14em; } -.lead-copy { margin-left: 4rem; max-width: 60ch; font-size: clamp(1rem, .9rem + .5vw, 1.25rem); } -.plain-list { - margin: var(--space-2) 0 0 4rem; - padding: 0; - list-style: none; - display: grid; - gap: 0.8rem; - max-width: 72ch; +.hero-dek { + font-size: clamp(1.05rem, 1.2vw, 1.24rem); + max-width: 58ch; } -.plain-list li { padding-left: 1rem; border-left: 2px solid var(--accent); } -.card-grid { - margin-top: var(--space-2); - display: grid; - grid-template-columns: repeat(12, 1fr); - gap: var(--space-2); +article { + padding: 1.1rem 0; + border-top: 1px solid color-mix(in oklab, var(--line) 75%, transparent); } -.card-grid.compact .info-card { grid-column: span 6; } -.info-card { - grid-column: span 6; - border: 1px solid var(--border); - background: var(--surface); - padding: var(--space-2); - transition: border-color 180ms ease, transform 180ms ease, background-color 220ms ease; +article:first-of-type { border-top: 0; } + +.prose { + margin-top: 1.2rem; } -.info-card:hover, -.info-card:focus-visible { - border-color: var(--accent); - transform: translateY(-2px); + +.prose h1, +.prose h2, +.prose h3 { + text-transform: none; + letter-spacing: .01em; + margin-top: 1.4rem; } -.info-card p { margin: 0.6rem 0 0; max-width: 48ch; } -.micro { color: var(--muted); font-size: 0.8rem; } - -.stack-list { margin-top: var(--space-2); } -.line-item { - border-top: 1px solid var(--border); - padding: 1rem 0; - transition: padding-left 180ms ease, border-color 180ms ease; + +.prose h1 { font-size: clamp(1.35rem, 2.2vw, 1.75rem); } +.prose h2 { font-size: clamp(1.2rem, 1.8vw, 1.45rem); } +.prose h3 { font-size: 1.15rem; } + +.prose p, +.prose li { + color: color-mix(in oklab, var(--fg) 88%, var(--muted)); } -.line-item:hover, -.line-item:focus-visible { - padding-left: 0.7rem; - border-color: var(--accent); + +.prose ul, +.prose ol { + margin: .7rem 0 1rem; } -.meta-row { - margin-top: 0.45rem; - display: flex; - justify-content: space-between; - gap: 1rem; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.12em; - font-size: 0.64rem; + +article a { + border-bottom: 1px solid transparent; + transition: border-color .2s ease; } +article a:hover { border-color: var(--fg); } -.page-intro h1 { max-width: 16ch; } +ul { margin: .4rem 0 0; padding-left: 1.2rem; } +li { margin: .28rem 0; } + +img { + max-width: 100%; + height: auto; + border: 1px solid var(--line); + background: var(--surface); +} .site-footer { - padding: var(--space-3) 0; + border-top: 1px solid var(--line); + padding: 1.4rem; color: var(--muted); - font-size: 0.72rem; - letter-spacing: 0.12em; + font-size: .72rem; text-transform: uppercase; + letter-spacing: .12em; } -.section-reveal { - opacity: 0; - transform: translateY(12px); - transition: opacity 380ms ease, transform 380ms ease; +input, textarea, select, button { + display: block; + margin: .4rem 0 .85rem; + padding: .55rem .65rem; + background: transparent; + color: var(--fg); + border: 1px solid var(--line); + width: 100%; + font: inherit; } -.section-reveal.is-visible { opacity: 1; transform: translateY(0); } -.hero .thesis-block { opacity: 0; transform: translateY(14px); transition: opacity 480ms ease, transform 480ms ease; } -.hero.is-visible .thesis-block { opacity: 1; transform: translateY(0); } - -:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } - -@media (max-width: 1024px) { - .header-wrap { grid-template-columns: 1fr; } - .main-nav { flex-wrap: wrap; } - .hero { grid-template-columns: 1fr; min-height: auto; } - .lead-copy, - .plain-list { margin-left: 0; } - .info-card, - .card-grid.compact .info-card { grid-column: span 12; } +textarea { min-height: 10rem; } +button { width: auto; cursor: pointer; transition: background-color .2s ease; } +button:hover { background: color-mix(in oklab, var(--surface) 75%, var(--fg) 8%); } + +@keyframes fadeUp { + to { opacity: 1; transform: translateY(0); } +} + +main > section:nth-of-type(2), +main > article:nth-of-type(2) { animation-delay: .04s; } +main > section:nth-of-type(3), +main > article:nth-of-type(3) { animation-delay: .08s; } +main > section:nth-of-type(4), +main > article:nth-of-type(4) { animation-delay: .12s; } + +@media (max-width: 900px) { + .site-header { grid-template-columns: 1fr; text-align: left; } + nav { justify-content: flex-start; } } @media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - .section-reveal, - .hero .thesis-block { opacity: 1; transform: none; } + html { scroll-behavior: auto; } + *, *::before, *::after { animation: none !important; transition: none !important; } } diff --git a/wwwroot/img/Logo-Out.svg b/wwwroot/img/Logo-Out.svg new file mode 100644 index 0000000..d8f4580 --- /dev/null +++ b/wwwroot/img/Logo-Out.svg @@ -0,0 +1,4 @@ + + + + diff --git a/wwwroot/js/site.js b/wwwroot/js/site.js index 294dd3d..ecf5b06 100644 --- a/wwwroot/js/site.js +++ b/wwwroot/js/site.js @@ -1,51 +1,18 @@ (() => { + const key = 'theme'; const root = document.documentElement; - const themeKey = 'ms-theme'; - const accentKey = 'ms-accent'; - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const stored = localStorage.getItem(key); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const theme = stored || (prefersDark ? 'dark' : 'light'); + root.setAttribute('data-theme', theme); - const storedTheme = localStorage.getItem(themeKey); - if (storedTheme === 'light' || storedTheme === 'dark') { - root.setAttribute('data-theme', storedTheme); - } else { - root.setAttribute('data-theme', 'system'); - } - - const storedAccent = localStorage.getItem(accentKey); - if (storedAccent) { - root.setAttribute('data-accent', storedAccent); - } - - const themeButton = document.querySelector('[data-theme-toggle]'); - themeButton?.addEventListener('click', () => { - const current = root.getAttribute('data-theme'); - const next = current === 'dark' ? 'light' : 'dark'; + const btn = document.getElementById('theme-toggle'); + if (!btn) return; + btn.textContent = theme === 'dark' ? 'Light' : 'Dark'; + btn.addEventListener('click', () => { + const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; root.setAttribute('data-theme', next); - localStorage.setItem(themeKey, next); - }); - - document.querySelectorAll('[data-accent-option]').forEach((button) => { - button.addEventListener('click', () => { - const accent = button.getAttribute('data-accent-option'); - if (!accent) return; - root.setAttribute('data-accent', accent); - localStorage.setItem(accentKey, accent); - }); + localStorage.setItem(key, next); + btn.textContent = next === 'dark' ? 'Light' : 'Dark'; }); - - if (prefersReducedMotion) { - document.querySelectorAll('.section-reveal').forEach((item) => item.classList.add('is-visible')); - return; - } - - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.classList.add('is-visible'); - observer.unobserve(entry.target); - } - }); - }, { threshold: 0.15, rootMargin: '0px 0px -24px 0px' }); - - document.querySelectorAll('.section-reveal').forEach((item) => observer.observe(item)); })();