├── docs ├── 05-Emails.md ├── 06-Newsletters.md ├── 03-Logging.md ├── 04-Localization.md └── 01-Authentication.md ├── deploy └── .gitignore ├── .vscode ├── .gitignore ├── launch.json └── tasks.json ├── src ├── Blogifier.Admin │ ├── assets │ │ ├── scss │ │ │ ├── include │ │ │ │ ├── _icons.scss │ │ │ │ ├── _tooltips.scss │ │ │ │ ├── _buttons.scss │ │ │ │ ├── _section.scss │ │ │ │ ├── _blazor.scss │ │ │ │ └── _highlight.scss │ │ │ ├── helpers │ │ │ │ ├── _reset.scss │ │ │ │ ├── _mixins.scss │ │ │ │ └── _colors.scss │ │ │ ├── layout │ │ │ │ ├── _main.scss │ │ │ │ └── _sidebar.scss │ │ │ ├── pages │ │ │ │ ├── settings │ │ │ │ │ ├── _themes.scss │ │ │ │ │ ├── _about.scss │ │ │ │ │ └── _settings.scss │ │ │ │ └── account │ │ │ │ │ └── _account.scss │ │ │ └── blogifier.scss │ │ └── package.json │ ├── wwwroot │ │ └── admin │ │ │ └── favicons │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── browserconfig.xml │ │ │ ├── site.webmanifest │ │ │ └── safari-pinned-tab.svg │ ├── Dtos │ │ ├── FrontCategoryItemDto.cs │ │ ├── FrontPostItemDto.cs │ │ ├── FrontUserInfoDto.cs │ │ ├── FrontBlobInfo.cs │ │ ├── FrontPostImportDto.cs │ │ └── FrontImportDto.cs │ ├── Components │ │ ├── RedirectComponent.razor │ │ ├── PageTitleComponent.razor │ │ └── CategoriesComponent.razor │ ├── Shared │ │ ├── MainLayout.razor │ │ ├── NewsletterLayout.razor │ │ └── BlogsLayout.razor │ ├── App.razor │ ├── Pages │ │ ├── Settings │ │ │ ├── AdvancedView.razor │ │ │ ├── CommentsView.razor │ │ │ ├── MenusView.razor │ │ │ ├── WidgetsView.razor │ │ │ ├── ThemesView.razor │ │ │ ├── CustomizeView.razor │ │ │ └── ScriptsView.razor │ │ ├── Drive │ │ │ └── DriveView.razor │ │ └── Blogs │ │ │ └── SettingsView.razor │ ├── Services │ │ └── ToasterService.cs │ ├── _Imports.razor │ ├── Properties │ │ └── launchSettings.json │ ├── Interop │ │ ├── CommonJsInterop.cs │ │ └── EditorJsInterop.cs │ ├── Program.cs │ └── BlogAuthStateProvider.cs ├── Blogifier.Themes.Standard │ ├── assets │ │ ├── scss │ │ │ ├── page │ │ │ │ ├── _home.scss │ │ │ │ ├── _profile.scss │ │ │ │ └── _account.scss │ │ │ ├── components │ │ │ │ ├── _search.scss │ │ │ │ ├── _dropdowns.scss │ │ │ │ ├── _pagination.scss │ │ │ │ ├── _newsletter.scss │ │ │ │ └── _highlight.scss │ │ │ ├── helpers │ │ │ │ ├── _base.scss │ │ │ │ ├── _colors.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── _variables.scss │ │ │ │ └── _reset.scss │ │ │ ├── layout │ │ │ │ ├── _footer.scss │ │ │ │ ├── _widgets.scss │ │ │ │ ├── _header.scss │ │ │ │ ├── _nav.scss │ │ │ │ └── _sidebar.scss │ │ │ ├── post │ │ │ │ ├── _nav.scss │ │ │ │ └── _related.scss │ │ │ ├── include │ │ │ │ └── _buttons.scss │ │ │ └── base.scss │ │ ├── js │ │ │ ├── highlight.js │ │ │ ├── profile.js │ │ │ └── blogifier.js │ │ ├── screenshot.png │ │ ├── svg │ │ │ ├── categories.svg │ │ │ ├── chevron-down.svg │ │ │ ├── bi-chevron-down.svg │ │ │ ├── bi-arrow-left.svg │ │ │ ├── bi-arrow-right.svg │ │ │ ├── bi-arrow-right-short.svg │ │ │ ├── search.svg │ │ │ ├── bi-arrow-up-circle.svg │ │ │ ├── bi-envelope-fill.svg │ │ │ ├── bi-x-circle.svg │ │ │ ├── facebook.svg │ │ │ ├── bi-calendar-event.svg │ │ │ ├── bi-share.svg │ │ │ ├── logo.svg │ │ │ ├── bi-pencil.svg │ │ │ ├── box-arrow-in-right.svg │ │ │ ├── twitter.svg │ │ │ ├── github.svg │ │ │ ├── bi-hash.svg │ │ │ ├── youtube.svg │ │ │ └── instagram.svg │ │ ├── package.json │ │ └── settings.json │ ├── Views │ │ ├── Themes │ │ │ └── standard │ │ │ │ ├── post │ │ │ │ ├── comments.cshtml │ │ │ │ ├── author.cshtml │ │ │ │ └── nav.cshtml │ │ │ │ ├── category.cshtml │ │ │ │ ├── index.cshtml │ │ │ │ ├── search.cshtml │ │ │ │ ├── 404.cshtml │ │ │ │ ├── components │ │ │ │ ├── newsletter.cshtml │ │ │ │ ├── header.cshtml │ │ │ │ ├── footer.cshtml │ │ │ │ └── pagination.cshtml │ │ │ │ ├── layouts │ │ │ │ ├── _base.cshtml │ │ │ │ ├── _account.cshtml │ │ │ │ └── _main.cshtml │ │ │ │ ├── password.cshtml │ │ │ │ └── login.cshtml │ │ └── _ViewImports.cshtml │ ├── wwwroot │ │ └── img │ │ │ ├── logo-black.png │ │ │ └── logo-white.png │ └── ThemesStandardConstant.cs ├── Blogifier │ ├── wwwroot │ │ ├── favicon.ico │ │ └── img │ │ │ ├── avatar.jpg │ │ │ └── cover.jpg │ ├── Views │ │ ├── Index.cshtml │ │ └── _ViewImports.cshtml │ ├── Posts │ │ ├── PostSearch.cs │ │ ├── PostCategory.cs │ │ ├── Category.cs │ │ ├── MarkdigProvider.cs │ │ ├── PostManager.cs │ │ ├── ReverseProvider.cs │ │ └── Post.cs │ ├── Caches │ │ ├── CacheKeys.cs │ │ └── CacheExtensions.cs │ ├── Data │ │ ├── AppEntity.cs │ │ ├── ValueGeneration │ │ │ └── UtcDateTimeValueGenerator .cs │ │ └── AppProvider.cs │ ├── Profiles │ │ ├── NewsletterProfile.cs │ │ ├── MailSettingProfile.cs │ │ ├── BlogProfile.cs │ │ ├── SubscriberProfile.cs │ │ ├── StorageProfile.cs │ │ ├── UserProfile.cs │ │ ├── CategoryProfile.cs │ │ └── PostProfile.cs │ ├── Controllers │ │ ├── AdminController.cs │ │ ├── CategoryController.cs │ │ ├── ErrorController.cs │ │ ├── StorageController.cs │ │ ├── SearchController.cs │ │ ├── HomeController.cs │ │ ├── PageController.cs │ │ ├── PostController.cs │ │ └── SitemapController.cs │ ├── Interfaces │ │ ├── TokenController.cs │ │ ├── MailController.cs │ │ ├── ImportController.cs │ │ ├── NewsletterController.cs │ │ ├── AnalyticsController.cs │ │ ├── SubscriberController.cs │ │ ├── StorageController.cs │ │ ├── BlogController.cs │ │ └── CategoryController.cs │ ├── Newsletters │ │ ├── Newsletter.cs │ │ ├── MailSettingData.cs │ │ ├── Subscriber.cs │ │ ├── SubscriberProvider.cs │ │ └── NewsletterProvider.cs │ ├── Options │ │ ├── OptionInfo.cs │ │ └── OptionProvider.cs │ ├── Blogs │ │ ├── BlogNotIitializeException.cs │ │ ├── BlogData.cs │ │ └── AnalyticsProvider.cs │ ├── Extensions │ │ └── PrincipalExtensions.cs │ ├── Storages │ │ ├── StorageReference.cs │ │ ├── IStorageProvider.cs │ │ └── Storage.cs │ ├── Identity │ │ ├── SignInManager.cs │ │ ├── UserInfo.cs │ │ ├── UserManager.cs │ │ ├── UserClaimsPrincipalFactory.cs │ │ ├── UserProvider.cs │ │ └── IdentityExtensions.cs │ ├── Properties │ │ └── launchSettings.json │ └── appsettings.json └── Blogifier.Shared │ ├── Enums │ ├── PostType.cs │ ├── StorageReferenceType.cs │ ├── StorageType.cs │ ├── UserState.cs │ ├── AnalyticsListType.cs │ ├── UserType.cs │ ├── GroupAction.cs │ ├── SaveStatus.cs │ ├── PostListType.cs │ ├── PostState.cs │ ├── UploadType.cs │ ├── PublishedStatus.cs │ ├── AnalyticsPeriod.cs │ └── SendNewsletterState.cs │ ├── Models │ ├── AccountModel.cs │ ├── MainModel.cs │ ├── SearchModel.cs │ ├── OptionItem.cs │ ├── PostVisit.cs │ ├── IndexModel.cs │ ├── PostPagerModel.cs │ ├── AccountProfileModel.cs │ ├── CategoryModel.cs │ ├── ChartModel.cs │ ├── PostModel.cs │ ├── ThemeItem.cs │ ├── AccountInitializeModel.cs │ ├── AccountLoginModel.cs │ ├── AccountProfileEditModel.cs │ ├── AccountProfilePasswordModel.cs │ ├── AccountRegisterModel.cs │ └── ThemeSettings.cs │ ├── Dtos │ ├── CategoryDto.cs │ ├── ImportRssDto.cs │ ├── PostBriefDto.cs │ ├── CategoryEitorDto.cs │ ├── AboutDto.cs │ ├── ImportDto.cs │ ├── SubscriberApplyDto.cs │ ├── CategoryItemDto.cs │ ├── BlogSumDto.cs │ ├── UserDto.cs │ ├── StorageDto.cs │ ├── NewsletterDto.cs │ ├── PostSlugDto.cs │ ├── SubscriberDto.cs │ ├── BlogEitorDto.cs │ ├── UserInfoDto.cs │ ├── AnalyticsDto.cs │ ├── PostItemDto.cs │ ├── PostEditorDto.cs │ ├── PostDto.cs │ ├── MailSettingDto.cs │ ├── UserEditorDto.cs │ ├── MainDto.cs │ ├── PostToHtmlDto.cs │ └── PostPagerDto.cs │ ├── Helper │ ├── DateTimeHelper.cs │ ├── PageHelper.cs │ └── StringHelper.cs │ ├── Identity │ └── BlogifierClaimTypes.cs │ ├── BlogifierSharedConstant.cs │ ├── Blogifier.Shared.csproj │ └── Extensions │ └── PrincipalExtensions.cs ├── global.json ├── docker.sh ├── tests └── Blogifier.Tests │ ├── BaseTests.cs │ ├── TestHelper.cs │ └── Blogifier.Tests.csproj ├── .dockerignore ├── publish.cmd ├── publish.sh ├── .config └── dotnet-tools.json ├── SECURITY.md ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── Dockerfile ├── LICENSE └── docker-compose.yml /docs/05-Emails.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/06-Newsletters.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | -------------------------------------------------------------------------------- /.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/include/_icons.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/page/_home.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.101" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | # docker 2 | docker build -t dorthl/blogifier:latest . 3 | docker push dorthl/blogifier:latest 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/components/_search.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | &-form{} 3 | &-results{} 4 | } 5 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/page/_profile.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .profile { 4 | margin-top: 8rem; 5 | } 6 | -------------------------------------------------------------------------------- /tests/Blogifier.Tests/BaseTests.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Tests; 2 | 3 | public class BaseTests 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/js/highlight.js: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js'; 2 | hljs.initHighlightingOnLoad(); 3 | -------------------------------------------------------------------------------- /src/Blogifier/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Blogifier/wwwroot/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier/wwwroot/img/avatar.jpg -------------------------------------------------------------------------------- /src/Blogifier/wwwroot/img/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier/wwwroot/img/cover.jpg -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/bin 3 | **/obj 4 | **/node_modules 5 | Dockerfile 6 | docker-compose.yml 7 | deploy/ 8 | **/package-lock.json 9 | -------------------------------------------------------------------------------- /publish.cmd: -------------------------------------------------------------------------------- 1 | rmdir dist /s/q 2 | dotnet publish -c Release /p:RuntimeIdentifier=win-x64 ./src/Blogifier/Blogifier.csproj -v minimal --output dist 3 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/helpers/_reset.scss: -------------------------------------------------------------------------------- 1 | strong, 2 | b { 3 | font-weight: 500; 4 | } 5 | 6 | img { 7 | max-width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/PostType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum PostType 4 | { 5 | Post = 1, 6 | Page = 2, 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/StorageReferenceType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum StorageReferenceType 4 | { 5 | Post = 1, 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/StorageType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum StorageType 4 | { 5 | Local = 1, 6 | Minio = 2, 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/UserState.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum UserState 4 | { 5 | Disable = -1, 6 | None = 0, 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/post/comments.cshtml: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | # Local machine 2 | rm -fr dist 3 | dotnet publish -c Release /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist 4 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/AnalyticsListType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum AnalyticsListType 4 | { 5 | List = 1, 6 | Graph = 2, 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/UserType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum UserType 4 | { 5 | Ordinary = 0, 6 | Administrator = 1000, 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Themes.Standard/assets/screenshot.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/favicon.ico -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class AccountModel 4 | { 5 | public string? RedirectUri { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/wwwroot/img/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Themes.Standard/wwwroot/img/logo-black.png -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/wwwroot/img/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Themes.Standard/wwwroot/img/logo-white.png -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/MainModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class MainModel(MainDto main) 4 | { 5 | public MainDto Main { get; set; } = main; 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/GroupAction.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum GroupAction 4 | { 5 | Publish, 6 | Unpublish, 7 | Delete, 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/SaveStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum SaveStatus 4 | { 5 | Saving = 1, 6 | Publishing = 2, 7 | Unpublishing = 3, 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/SearchModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class SearchModel(PostPagerDto pager, MainDto main) : PostPagerModel(pager, main) 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/Blogifier/Views/Index.cshtml: -------------------------------------------------------------------------------- 1 | @* For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 *@ 2 |

Welcome to the blog!

3 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/PostListType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum PostListType 4 | { 5 | Blog, 6 | Category, 7 | Author, 8 | Search, 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontCategoryItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class FrontCategoryItemDto : CategoryItemDto 4 | { 5 | public bool Selected { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/helpers/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin bf-shadow($rad: .25rem, $shad: $bf-shadow, $bg: #fff) { 2 | background: $bg; 3 | box-shadow: $shad; 4 | border-radius: $rad; 5 | } 6 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blogifierdotnet/Blogifier/HEAD/src/Blogifier.Admin/wwwroot/admin/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/PostState.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum PostState 4 | { 5 | Individual = -1, 6 | Draft = 1, 7 | Release = 2, 8 | Featured = 3, 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/CategoryDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class CategoryDto 4 | { 5 | public int Id { get; set; } 6 | public string Content { get; set; } = default!; 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/UploadType.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum UploadType 4 | { 5 | Avatar, 6 | Attachement, 7 | AppLogo, 8 | PostCover, 9 | PostImage 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/OptionItem.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class OptionItem 4 | { 5 | public int Id { get; set; } 6 | public string Title { get; set; } = default!; 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/PostVisit.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class PostVisit 4 | { 5 | public string Name { get; set; } = default!; 6 | public int Value { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/PublishedStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum PublishedStatus 4 | { 5 | All = 1, 6 | Published = 2, 7 | Drafts = 3, 8 | Featured = 4, 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/IndexModel.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | namespace Blogifier.Models; 3 | 4 | public class IndexModel(PostPagerDto pager, MainDto main) : PostPagerModel(pager, main) 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontPostItemDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Admin; 4 | 5 | public class FrontPostItemDto : PostItemDto 6 | { 7 | public bool Selected { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontUserInfoDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Admin; 4 | 5 | public class FrontUserInfoDto : UserInfoDto 6 | { 7 | public bool Selected { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontBlobInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Admin; 2 | 3 | public class FrontBlobInfo 4 | { 5 | public string FileName { get; set; } = default!; 6 | public string Url { get; set; } = default!; 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/PostPagerModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class PostPagerModel(PostPagerDto pager, MainDto main) : MainModel(main) 4 | { 5 | public PostPagerDto Pager { get; } = pager; 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/AnalyticsPeriod.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum AnalyticsPeriod 4 | { 5 | Today = 1, 6 | Yesterday = 2, 7 | Days7 = 3, 8 | Days30 = 4, 9 | Days90 = 5 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/ThemesStandardConstant.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Themes.Standard; 2 | 3 | public static class ThemesStandardConstant 4 | { 5 | public const string AssemblyName = "Blogifier.Themes.Standard"; 6 | } 7 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "7.0.5", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountProfileModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class AccountProfileModel : AccountModel 4 | { 5 | public bool IsProfile { get; set; } 6 | public bool IsPassword { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/ImportRssDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class ImportRssDto 6 | { 7 | [Required][Url] public string FeedUrl { get; set; } = default!; 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostBriefDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class PostBriefDto 4 | { 5 | public int Id { get; set; } 6 | public string Title { get; set; } = default!; 7 | public string Slug { get; set; } = default!; 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/CategoryEitorDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class CategoryEitorDto 4 | { 5 | public int Id { get; set; } 6 | public string Content { get; set; } = default!; 7 | public string? Description { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/CategoryModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class CategoryModel(string category, PostPagerDto pager, MainDto main) : PostPagerModel(pager, main) 4 | { 5 | public string Category { get; set; } = category; 6 | } 7 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/helpers/_base.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 61.5rem; // 2rem container gutters 3 | max-width: 100%; 4 | } 5 | 6 | @media (max-width: 991px) { 7 | :root { 8 | --bs-gutter-x: 1.5rem; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/helpers/_colors.scss: -------------------------------------------------------------------------------- 1 | // included in the _bootstrap.scss 2 | $custom-colors: ( 3 | "blogifier": $blogifier, 4 | "default": $default, 5 | "gold": $gold 6 | ); 7 | 8 | $theme-colors: map-merge($theme-colors, $custom-colors); 9 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/AboutDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class AboutDto 4 | { 5 | public string? Version { get; set; } 6 | public string OperatingSystem { get; set; } = default!; 7 | public string? DatabaseProvider { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontPostImportDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Admin; 4 | 5 | public class FrontPostImportDto : PostEditorDto 6 | { 7 | public bool Selected { get; set; } 8 | public bool? ImportComplete { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/helpers/_colors.scss: -------------------------------------------------------------------------------- 1 | // included in the _bootstrap.scss 2 | $custom-colors: ( 3 | "blogifier": $blogifier, 4 | "default": $default, 5 | "gold": $gold 6 | ); 7 | 8 | $theme-colors: map-merge($theme-colors, $custom-colors); 9 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/PostSearch.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | 3 | namespace Blogifier.Posts; 4 | 5 | public class PostSearch(PostItemDto post, int rank) 6 | { 7 | public PostItemDto Post { get; set; } = post; 8 | public int Rank { get; set; } = rank; 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/ImportDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class ImportDto 6 | { 7 | public string BaseUrl { get; set; } = default!; 8 | public List Posts { get; set; } = default!; 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/SubscriberApplyDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class SubscriberApplyDto 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } = default!; 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Enums/SendNewsletterState.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public enum SendNewsletterState 4 | { 5 | OK = 0, 6 | NotPost = 10, 7 | NotSubscriber = 11, 8 | NotMailEnabled = 12, 9 | NewsletterSuccess = 13, 10 | SentError = 14 11 | } 12 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Dtos/FrontImportDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Admin; 5 | 6 | public class FrontImportDto : ImportDto 7 | { 8 | public new List Posts { get; set; } = default!; 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/ChartModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class BarChartModel 6 | { 7 | public ICollection Labels { get; set; } = default!; 8 | public ICollection Data { get; set; } = default!; 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier/Caches/CacheKeys.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Caches; 2 | 3 | public static class CacheKeys 4 | { 5 | public const string BlogData = "blogifier"; 6 | public const string BlogMailData = $"{BlogData}:mail"; 7 | public const string CategoryItemes = $"{BlogData}:category:itemes"; 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier/Data/AppEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Blogifier.Data; 5 | 6 | public abstract class AppEntity where TKey : IEquatable 7 | { 8 | [Key] 9 | public virtual TKey Id { get; set; } = default!; 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Components/RedirectComponent.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager _navigationManager 2 | 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | _navigationManager.NavigateTo($"account?redirectUri={Uri.EscapeDataString(_navigationManager.Uri)}", true, true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/CategoryItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class CategoryItemDto 4 | { 5 | public int Id { get; set; } 6 | public string Category { get; set; } = default!; 7 | public string? Description { get; set; } 8 | public int PostCount { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/categories.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/PostCategory.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class PostCategory 4 | { 5 | public int PostId { get; set; } 6 | public Post Post { get; set; } = default!; 7 | public int CategoryId { get; set; } 8 | public Category Category { get; set; } = default!; 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/PostModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class PostModel(PostSlugDto postSlug, string categoriesUrl, MainDto main) : MainModel(main) 4 | { 5 | public PostSlugDto PostSlug { get; set; } = postSlug; 6 | public string CategoriesUrl { get; set; } = categoriesUrl; 7 | } 8 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/ThemeItem.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class ThemeItem 4 | { 5 | public string Title { get; set; } = default!; 6 | public string Cover { get; set; } = default!; 7 | public bool IsCurrent { get; set; } 8 | public bool HasSettings { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/NewsletterProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Newsletters; 3 | using Blogifier.Shared; 4 | 5 | namespace Blogifier.Profiles; 6 | 7 | public class NewsletterProfile : Profile 8 | { 9 | public NewsletterProfile() => CreateMap(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | @Body 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Blogifier/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Localization 2 | @using Blogifier.Shared 3 | @using Blogifier.Shared.Resources 4 | @using Blogifier.Extensions; 5 | @using Blogifier.Models; 6 | @using Blogifier.Helper; 7 | @using Blogifier; 8 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 9 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/BlogSumDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class BlogSumDto 4 | { 5 | public string Time { get; set; } = default!; 6 | public int Posts { get; set; } 7 | public int Pages { get; set; } 8 | public int Views { get; set; } 9 | public int Subscribers { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/MailSettingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Newsletters; 3 | using Blogifier.Shared; 4 | 5 | namespace Blogifier.Profiles; 6 | 7 | public class MailSettingProfile : Profile 8 | { 9 | public MailSettingProfile() => CreateMap().ReverseMap(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/UserDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class UserDto 4 | { 5 | public int Id { get; set; } 6 | public string Email { get; set; } = default!; 7 | public string NickName { get; set; } = default!; 8 | public string? Avatar { get; set; } 9 | public string? Bio { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Localization 2 | @using Blogifier.Shared 3 | @using Blogifier.Shared.Resources 4 | @using Blogifier.Models; 5 | @using Blogifier.Helper; 6 | @using Blogifier; 7 | @using Blogifier.Themes.Standard; 8 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 9 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-arrow-right-short.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/include/_tooltips.scss: -------------------------------------------------------------------------------- 1 | .tooltip-inner { 2 | margin-top: 0.25rem; 3 | box-shadow: 0 0 0 1px rgba(#fff, 0.5); 4 | letter-spacing: 0.03125rem; 5 | } 6 | 7 | .bs-tooltip-bottom .tooltip-inner { 8 | margin-top: 0.25rem; 9 | } 10 | 11 | .bs-tooltip-top .tooltip-inner { 12 | margin-bottom: 0.25rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/StorageDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class StorageDto 4 | { 5 | public int Id { get; set; } 6 | public string Slug { get; set; } = default!; 7 | public string Name { get; set; } = default!; 8 | public long Length { get; set; } 9 | public string ContentType { get; set; } = default!; 10 | } 11 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Components/PageTitleComponent.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime _jsRuntime; 2 | @inject CommonJsInterop _commonJsInterop 3 | 4 | @code { 5 | [Parameter] public string Title { get; set; } = default!; 6 | 7 | protected override async Task OnInitializedAsync() 8 | { 9 | await _commonJsInterop.SetTitleAsync(Title); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/BlogProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Blogs; 3 | using Blogifier.Shared; 4 | 5 | namespace Blogifier.Profiles; 6 | 7 | public class BlogProfile : Profile 8 | { 9 | public BlogProfile() 10 | { 11 | CreateMap(); 12 | CreateMap().ReverseMap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/NewsletterDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class NewsletterDto 6 | { 7 | public int Id { get; set; } 8 | public DateTime CreatedAt { get; set; } 9 | public int PostId { get; set; } 10 | public PostBriefDto Post { get; set; } = default!; 11 | public bool Success { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | border-top: 1px solid rgba(#000, 0.05); 3 | margin-top: 4rem; 4 | color: #444; 5 | font-size: 0.75rem; 6 | a { 7 | color: #444; 8 | text-decoration: none; 9 | &:hover { 10 | color: $color; 11 | } 12 | } 13 | p { 14 | margin: 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountInitializeModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class AccountInitializeModel : AccountRegisterModel 6 | { 7 | [Required] 8 | public string Title { get; set; } = default!; 9 | [Required] 10 | public string Description { get; set; } = default!; 11 | } 12 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/SubscriberProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Newsletters; 3 | using Blogifier.Shared; 4 | 5 | namespace Blogifier.Profiles; 6 | 7 | public class SubscriberProfile : Profile 8 | { 9 | public SubscriberProfile() 10 | { 11 | CreateMap(); 12 | CreateMap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostSlugDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class PostSlugDto 6 | { 7 | public PostToHtmlDto Post { get; set; } = default!; 8 | public PostItemDto? Older { get; set; } 9 | public PostItemDto? Newer { get; set; } 10 | public IEnumerable Related { get; set; } = default!; 11 | } 12 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-arrow-up-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/category.cshtml: -------------------------------------------------------------------------------- 1 | @model CategoryModel 2 | 3 | @inject IStringLocalizer _localizer 4 | 5 | @{ 6 | Layout = "layouts/_main.cshtml"; 7 | } 8 | 9 |
10 |

@_localizer["categories"]: @Model.Category

11 | 12 |
13 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/StorageProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Shared; 3 | using Blogifier.Storages; 4 | 5 | namespace Blogifier.Profiles; 6 | 7 | public class StorageProfile : Profile 8 | { 9 | public StorageProfile() => CreateMap().ReverseMap(); 10 | //CreateMap()// .IncludeMembers(m => m.Storage)// .ReverseMap(); 11 | } 12 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/SubscriberDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class SubscriberDto 6 | { 7 | public int Id { get; set; } 8 | public DateTime CreatedAt { get; set; } 9 | public string Email { get; set; } = default!; 10 | public string? Ip { get; set; } 11 | public string? Country { get; set; } 12 | public string? Region { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-envelope-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/UserProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Identity; 3 | using Blogifier.Shared; 4 | 5 | namespace Blogifier.Profiles; 6 | 7 | public class UserProfile : Profile 8 | { 9 | public UserProfile() 10 | { 11 | CreateMap(); 12 | CreateMap().ReverseMap(); 13 | CreateMap(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/AdminController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Threading.Tasks; 4 | 5 | namespace Blogifier.Controllers; 6 | 7 | public class AdminController : Controller 8 | { 9 | [HttpGet("/admin")] 10 | [Authorize] 11 | public Task Admin() => Task.FromResult(File("~/index.html", "text/html")); 12 | } 13 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/CategoryProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Shared; 3 | 4 | namespace Blogifier.Profiles; 5 | 6 | public class CategoryProfile : Profile 7 | { 8 | public CategoryProfile() 9 | { 10 | CreateMap().ReverseMap(); 11 | CreateMap() 12 | .IncludeMembers(m => m.Category) 13 | .ReverseMap(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/BlogEitorDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class BlogEitorDto 4 | { 5 | public string Title { get; set; } = default!; 6 | public string Description { get; set; } = default!; 7 | public string? HeaderScript { get; set; } 8 | public string? FooterScript { get; set; } 9 | public int ItemsPerPage { get; set; } 10 | public bool IncludeFeatured { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security patches will be applied to the most recent version. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.9.x.x | Most Current Version| 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please report (suspected) security vulnerabilities to 14 | **[blogifierdotnet@gmail.com](mailto:blogifierdotnet@gmail.com)**. 15 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/layout/_widgets.scss: -------------------------------------------------------------------------------- 1 | .bft-widget { 2 | &-title { 3 | font-weight: 300; 4 | letter-spacing: 0.1em; 5 | color: #aaa; 6 | font-size: 1rem; 7 | text-transform: uppercase; 8 | display: block; 9 | margin-bottom: 2rem; 10 | } 11 | &-content { 12 | ul { 13 | list-style: none; 14 | padding: 0; 15 | margin: 0; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/UserInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public class UserInfoDto 4 | { 5 | public int Id { get; set; } 6 | public string Email { get; set; } = default!; 7 | public string UserName { get; set; } = default!; 8 | public string NickName { get; set; } = default!; 9 | public string? Avatar { get; set; } 10 | public string? Bio { get; set; } 11 | public UserType Type { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-x-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/TokenController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Identity; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Blogifier.Interfaces; 6 | 7 | 8 | [Route("api/token")] 9 | [ApiController] 10 | public class TokenController : ControllerBase 11 | { 12 | 13 | [HttpGet("userinfo")] 14 | [Authorize] 15 | public BlogifierClaims? Get() => BlogifierClaims.Analysis(User); 16 | } 17 | -------------------------------------------------------------------------------- /src/Blogifier/Newsletters/Newsletter.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using Blogifier.Shared; 3 | using System; 4 | 5 | namespace Blogifier.Newsletters; 6 | 7 | public class Newsletter : AppEntity 8 | { 9 | public DateTime CreatedAt { get; set; } 10 | public DateTime UpdatedAt { get; set; } 11 | public int PostId { get; set; } 12 | public bool Success { get; set; } 13 | public Post Post { get; set; } = default!; 14 | } 15 | -------------------------------------------------------------------------------- /docs/03-Logging.md: -------------------------------------------------------------------------------- 1 | ### Serilog 2 | 3 | Logging done using [Serilog Sink File](https://github.com/serilog/serilog-sinks-file) package. 4 | All logs saved to the `/Logs` folder as configured in the application startup: 5 | 6 | ``` 7 | Log.Logger = new LoggerConfiguration() 8 | .Enrich.FromLogContext() 9 | .WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day) 10 | .CreateLogger(); 11 | 12 | Log.Warning("Test log"); 13 | ``` -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountLoginModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class AccountLoginModel : AccountModel 6 | { 7 | public bool ShowError { get; set; } 8 | [Required] 9 | [EmailAddress] 10 | public string Email { get; set; } = default!; 11 | [Required] 12 | [DataType(DataType.Password)] 13 | public string Password { get; set; } = default!; 14 | } 15 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-calendar-event.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Blogifier/Options/OptionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Blogifier.Options; 5 | 6 | public class OptionInfo 7 | { 8 | [Key] 9 | public int Id { get; set; } 10 | public DateTime CreatedAt { get; set; } 11 | public DateTime UpdatedAt { get; set; } 12 | [StringLength(256)] 13 | public string Key { get; set; } = default!; 14 | public string Value { get; set; } = default!; 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Blogs/BlogNotIitializeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blogifier.Blogs; 4 | 5 | public class BlogNotIitializeException : Exception 6 | { 7 | public BlogNotIitializeException() 8 | { 9 | } 10 | 11 | public BlogNotIitializeException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public BlogNotIitializeException(string message, Exception inner) 17 | : base(message, inner) 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Helper/DateTimeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blogifier.Helper; 4 | 5 | public static class DateTimeHelper 6 | { 7 | public static string ToFriendlyShortDateString(DateTime? date, string defaultString = "") 8 | { 9 | if (!date.HasValue) return defaultString; 10 | return ToFriendlyShortDateString(date.Value); 11 | } 12 | 13 | public static string ToFriendlyShortDateString(DateTime date) => $"{date:MMM dd}, {date.Year}"; 14 | } 15 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Identity/BlogifierClaimTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Identity; 2 | 3 | public static class BlogifierClaimTypes 4 | { 5 | public const string UserId = "sub"; 6 | public const string SecurityStamp = "ses"; 7 | public const string UserName = "name"; 8 | public const string Email = "email"; 9 | public const string NickName = "nck"; 10 | public const string Avatar = "ava"; 11 | public const string Gender = "gen"; 12 | public const string Type = "type"; 13 | } 14 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountProfileEditModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class AccountProfileEditModel : AccountProfileModel 6 | { 7 | public string? Error { get; set; } 8 | [Required] 9 | [EmailAddress] 10 | public string? Email { get; set; } = default!; 11 | [Required] 12 | public string NickName { get; set; } = default!; 13 | public string? Avatar { get; set; } 14 | public string? Bio { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Sorry, there's nothing at this address.

9 |
10 |
11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Helper/PageHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Shared; 2 | 3 | public static class PageHelper 4 | { 5 | public static string CheckGetAvatarUrl(string? avatar) 6 | { 7 | if (!string.IsNullOrEmpty(avatar)) return avatar; 8 | return BlogifierSharedConstant.DefaultAvatar; 9 | } 10 | 11 | public static string CheckGetCoverrUrl(string? avatar) 12 | { 13 | if (!string.IsNullOrEmpty(avatar)) return avatar; 14 | return BlogifierSharedConstant.DefaultCover; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/index.cshtml: -------------------------------------------------------------------------------- 1 | @model IndexModel 2 | @{ 3 | Layout = "layouts/_main.cshtml"; 4 | } 5 | 6 |
7 | 8 | @* 9 | TODO: Custom fields: 10 | - Post list style options: 11 | 1. view-grid (default) 12 | 2. view-list 13 | *@ 14 | 15 | @* *@ 16 | 17 |
18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 14 | 15 | **Screenshots** 16 | 19 | 20 | **Additional context** 21 | 24 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/js/profile.js: -------------------------------------------------------------------------------- 1 | 2 | // copy input 3 | function test(elm) { 4 | var copyText = document.getElementById(elm); 5 | var copyTextStore = copyText.dataset.link; 6 | copyText.select(); 7 | copyText.setSelectionRange(0, 99999); 8 | document.execCommand("copy"); 9 | copyText.value = "Copied!"; 10 | copyText.classList.add("copied"); 11 | setTimeout(function () { 12 | copyText.value = copyTextStore; 13 | copyText.classList.remove("copied"); 14 | }, 500); 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Newsletters/MailSettingData.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Newsletters; 2 | 3 | public class MailSettingData 4 | { 5 | public bool Enabled { get; set; } 6 | public string Host { get; set; } = default!; 7 | public int Port { get; set; } 8 | public string UserEmail { get; set; } = default!; 9 | public string UserPassword { get; set; } = default!; 10 | public string FromName { get; set; } = default!; 11 | public string FromEmail { get; set; } = default!; 12 | public string ToName { get; set; } = default!; 13 | } 14 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/Category.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Blogifier.Shared; 7 | 8 | public class Category : AppEntity 9 | { 10 | public DateTime CreatedAt { get; set; } 11 | [StringLength(120)] 12 | public string Content { get; set; } = default!; 13 | [StringLength(255)] 14 | public string? Description { get; set; } = default!; 15 | public List? PostCategories { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /docs/04-Localization.md: -------------------------------------------------------------------------------- 1 | ### Blazor Internationalization(I18n) Text 2 | 3 | Localization and internationalization is done on admin UI level using 4 | [Blazor Internationalization](https://github.com/jsakamoto/Toolbelt.Blazor.I18nText) package. 5 | It uses JSON files instead of standard XML resources used by MS frameworks and compiles resources on build making them strongly typed. 6 | 7 | Resource language files located under `Blogifier.Admin/i18ntext` folder. 8 | If browser set to use one of the cultures in this folder, admin UI should display accordingly. -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/js/blogifier.js: -------------------------------------------------------------------------------- 1 | import "bootstrap"; 2 | 3 | // copy input 4 | function copyInput(elm) { 5 | var copyText = document.getElementById(elm); 6 | var copyTextStore = copyText.dataset.link; 7 | copyText.select(); 8 | copyText.setSelectionRange(0, 99999); 9 | document.execCommand("copy"); 10 | copyText.value = "Copied!"; 11 | copyText.classList.add("copied"); 12 | setTimeout(function () { 13 | copyText.value = copyTextStore; 14 | copyText.classList.remove("copied"); 15 | }, 500); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/Blogifier/Blogs/BlogData.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Blogs; 2 | 3 | public class BlogData 4 | { 5 | public string Title { get; set; } = default!; 6 | public string Description { get; set; } = default!; 7 | public string? Logo { get; set; } 8 | public string Theme { get; set; } = default!; 9 | public string? HeaderScript { get; set; } 10 | public string? FooterScript { get; set; } 11 | public string Version { get; set; } = default!; 12 | public int ItemsPerPage { get; set; } 13 | public bool IncludeFeatured { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blogifier", 3 | "short_name": "Blogifier", 4 | "icons": [ 5 | { 6 | "src": "admin/favicons/android-chrome-192x192.png?v=3", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "admin/favicons/android-chrome-512x512.png?v=3", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountProfilePasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class AccountProfilePasswordModel : AccountProfileModel 6 | { 7 | public string? Error { get; set; } 8 | [Required] 9 | [DataType(DataType.Password)] 10 | public string Password { get; set; } = default!; 11 | [Required] 12 | [DataType(DataType.Password)] 13 | [Compare("Password", ErrorMessage = "Passwords do not match")] 14 | public string PasswordConfirm { get; set; } = default!; 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/AccountRegisterModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class AccountRegisterModel : AccountLoginModel 6 | { 7 | [Required] 8 | public string UserName { get; set; } = default!; 9 | [Required] 10 | [StringLength(256)] 11 | public string NickName { get; set; } = default!; 12 | [Required] 13 | [DataType(DataType.Password)] 14 | [Compare("Password", ErrorMessage = "Passwords do not match")] 15 | public string PasswordConfirm { get; set; } = default!; 16 | } 17 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/box-arrow-in-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/post/_nav.scss: -------------------------------------------------------------------------------- 1 | .post-nav { 2 | margin-bottom: 4rem; 3 | 4 | // item 5 | &-item { 6 | position: relative; 7 | color: #000; 8 | text-decoration: none; 9 | display: block; 10 | &:hover { 11 | color: $color; 12 | } 13 | } 14 | 15 | // title 16 | &-title { 17 | z-index: 2; 18 | font-size: 0.875rem; 19 | font-weight: 600; 20 | margin: 0; 21 | } 22 | 23 | // text 24 | &-text { 25 | opacity: 0.5; 26 | text-transform: uppercase; 27 | font-size: 0.75rem; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/AnalyticsDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class AnalyticsDto 6 | { 7 | public IEnumerable Blogs { get; set; } = default!; 8 | public int TotalPosts { get; set; } 9 | public int TotalPages { get; set; } 10 | public int TotalViews { get; set; } 11 | public int TotalSubscribers { get; set; } 12 | public AnalyticsListType DisplayType { get; set; } 13 | public AnalyticsPeriod DisplayPeriod { get; set; } 14 | public BarChartModel LatestPostViews { get; set; } = default!; 15 | } 16 | -------------------------------------------------------------------------------- /src/Blogifier/Extensions/PrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Claims; 3 | 4 | namespace Blogifier.Extensions; 5 | 6 | public static class PrincipalExtensions 7 | { 8 | public static string FirstValue(this ClaimsPrincipal principal, string claimType) 9 | { 10 | var value = FirstOrDefault(principal, claimType); 11 | if (value == null) throw new NullReferenceException(nameof(value)); 12 | return value; 13 | } 14 | 15 | public static string? FirstOrDefault(this ClaimsPrincipal principal, string claimType) 16 | => principal.FindFirstValue(claimType); 17 | } 18 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostItemDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Shared; 5 | 6 | public class PostItemDto 7 | { 8 | public int Id { get; set; } 9 | public UserDto User { get; set; } = default!; 10 | public string Title { get; set; } = default!; 11 | public string Slug { get; set; } = default!; 12 | public string Description { get; set; } = default!; 13 | public List? Categories { get; set; } 14 | public string? Cover { get; set; } 15 | public PostState State { get; set; } 16 | public DateTime? PublishedAt { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/helpers/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin background-hover( 2 | $class, 3 | $color, 4 | $color-hover, 5 | $color-text, 6 | $color-text-hover 7 | ) { 8 | #{$class} { 9 | background-color: $color; 10 | color: $color-text; 11 | 12 | &:hover { 13 | background-color: $color-hover; 14 | color: $color-text-hover; 15 | } 16 | } 17 | } 18 | 19 | @mixin text-hover($class, $color, $color-hover) { 20 | #{$class} { 21 | @if $color { 22 | color: $color; 23 | } 24 | 25 | &:hover { 26 | color: $color-hover; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/components/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | // 2 | .dropdown-menu { 3 | border-radius: $radius; 4 | box-shadow: 0 0 3rem rgba(#000, 0.2); 5 | border: none; 6 | } 7 | 8 | .dropdown-item { 9 | font-size: 0.875rem; 10 | font-weight: 500; 11 | text-transform: capitalize; 12 | padding: 0.5rem 1rem; 13 | color: #555; 14 | transition: all ease-in-out 0.2s; 15 | &:active, 16 | &:hover { 17 | color: #000; 18 | background-color: var(--bs-light); 19 | } 20 | } 21 | .dropdown-divider { 22 | background: none; 23 | border-top: 1px solid rgba(#000, 0.3); 24 | margin: 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/Blogifier/Newsletters/Subscriber.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace Blogifier.Newsletters; 6 | 7 | public class Subscriber : AppEntity 8 | { 9 | public DateTime CreatedAt { get; set; } 10 | public DateTime UpdatedAt { get; set; } 11 | [EmailAddress] 12 | [StringLength(160)] 13 | public string Email { get; set; } = default!; 14 | [StringLength(80)] 15 | public string? Ip { get; set; } 16 | [StringLength(120)] 17 | public string? Country { get; set; } 18 | [StringLength(120)] 19 | public string? Region { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/MarkdigProvider.cs: -------------------------------------------------------------------------------- 1 | using Markdig; 2 | 3 | namespace Blogifier.Posts; 4 | 5 | public class MarkdigProvider 6 | { 7 | private readonly MarkdownPipeline _markdownPipeline; 8 | 9 | 10 | public MarkdigProvider() => 11 | _markdownPipeline = new MarkdownPipelineBuilder() 12 | .UsePipeTables() 13 | .UseAdvancedExtensions() 14 | .Build(); 15 | 16 | public string ToHtml(string markdown) 17 | { 18 | var html = Markdown.ToHtml(markdown, _markdownPipeline); 19 | //_logger.LogDebug("ToHtml markdown:{markdown}, html:{html}", markdown, html); 20 | return html; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/post/author.cshtml: -------------------------------------------------------------------------------- 1 | @model PostModel 2 | 3 | 12 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/post/_related.scss: -------------------------------------------------------------------------------- 1 | .related { 2 | margin-top: -1px; 3 | margin-bottom: 4rem; 4 | 5 | &-header { 6 | border-top: 1px solid var(--bs-light); 7 | padding-top: 3rem; 8 | margin-bottom: 3rem; 9 | align-items: center; 10 | display: flex; 11 | &-title { 12 | margin-bottom: 0; 13 | font-size: 1rem; 14 | } 15 | &-link { 16 | margin-bottom: 0; 17 | color: rgba(#000, 0.5); 18 | font-size: 0.875rem; 19 | font-weight: 500; 20 | text-decoration: none; 21 | &:hover { 22 | color: $color; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/AdvancedView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/advanced/" 3 | @inject IStringLocalizer _localizer 4 | 5 | 6 | 7 |

@_localizer["advanced-settings"]

8 |
9 |
10 | @_localizer["under-development"]. 11 | 12 | @_localizer["more-info"] 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/Blogifier/Storages/StorageReference.cs: -------------------------------------------------------------------------------- 1 | namespace Blogifier.Storages; 2 | 3 | 4 | // TOTO In the sql server implementation, 5 | // it is found that there may be problems in the design of StorageReference table, 6 | // and the storage reference function is not implemented for the time being, so it is commented out 7 | 8 | 9 | //public class StorageReference 10 | //{ 11 | // public DateTime CreatedAt { get; set; } 12 | // public int StorageId { get; set; } 13 | // public Storage Storage { get; set; } = default!; 14 | // public int EntityId { get; set; } 15 | // public StorageReferenceType Type { get; set; } 16 | // public Post? Post { get; set; } 17 | //} 18 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/BlogifierSharedConstant.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using System.Text.Json; 3 | 4 | namespace Blogifier; 5 | 6 | public static class BlogifierSharedConstant 7 | { 8 | public const string PolicyAdminName = "Administrator"; 9 | public static readonly string PolicyAdminValue = $"{((int)UserType.Administrator)}"; 10 | public static readonly string DefaultAvatar = "/img/avatar.jpg"; 11 | public static readonly string DefaultCover = "/img/cover.jpg"; 12 | public static readonly string DefaultLogo = "img/logo-sm.png"; 13 | public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new(JsonSerializerDefaults.Web); 14 | } 15 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/include/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn-rounded { 2 | border-radius: 6rem; 3 | } 4 | 5 | .btn { 6 | text-transform: capitalize; 7 | 8 | &-link { 9 | } 10 | 11 | &-outline { 12 | &-default { 13 | border-color: $secondary; 14 | color: $gray-600; 15 | 16 | &:hover { 17 | color: $gray-800; 18 | border-color: $gray-500; 19 | } 20 | } 21 | } 22 | 23 | &-xs { 24 | padding: 0.125rem 0.5rem; 25 | font-size: 0.75rem; 26 | border-radius: 0.2rem; 27 | } 28 | &-block { 29 | width: 100%; 30 | } 31 | 32 | &-floating { 33 | height: 3.125rem; 34 | font-size: 0.9375rem; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Models/ThemeSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class Field 6 | { 7 | public string Id { get; set; } = default!; 8 | public string Label { get; set; } = default!; 9 | public string Type { get; set; } = default!; 10 | public string Value { get; set; } = default!; 11 | public List Options { get; set; } = default!; 12 | } 13 | 14 | public class Section 15 | { 16 | public string Label { get; set; } = default!; 17 | public List Fields { get; set; } = default!; 18 | } 19 | 20 | public class ThemeSettings 21 | { 22 | public List
Sections { get; set; } = default!; 23 | } 24 | -------------------------------------------------------------------------------- /src/Blogifier/Data/ValueGeneration/UtcDateTimeValueGenerator .cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | using Microsoft.EntityFrameworkCore.ValueGeneration; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Data.ValueGeneration; 8 | 9 | public class DateTimetValueGenerator : ValueGenerator 10 | { 11 | public override bool GeneratesTemporaryValues => false; 12 | 13 | public override DateTime Next(EntityEntry entry) => DateTime.UtcNow; 14 | 15 | public override ValueTask NextAsync(EntityEntry entry, CancellationToken cancellationToken = default) => 16 | ValueTask.FromResult(DateTime.UtcNow); 17 | } 18 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Drive/DriveView.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/drive/" 2 | @inject IStringLocalizer _localizer 3 | 4 | 5 | 6 |
7 |

@_localizer["drive"]

8 |
9 |

@_localizer["drive-describe"]

10 |
11 | @_localizer["under-development"]. 12 | 13 | @_localizer["more-info"] 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Blogifier.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | True 19 | True 20 | Resource.resx 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostEditorDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Shared; 5 | 6 | public class PostEditorDto 7 | { 8 | public int Id { get; set; } 9 | public string Title { get; set; } = default!; 10 | public string? Slug { get; set; } 11 | public string Description { get; set; } = default!; 12 | public string Content { get; set; } = default!; 13 | public string? Cover { get; set; } 14 | public DateTime? PublishedAt { get; set; } 15 | public PostType PostType { get; set; } 16 | public PostState State { get; set; } 17 | public List? Categories { get; set; } 18 | public List? Storages { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Storages/IStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Storages; 8 | 9 | public interface IStorageProvider 10 | { 11 | Task ExistsAsync(string slug); 12 | Task GetCheckStoragAsync(string path); 13 | Task GetAsync(string slug, Func callback); 14 | Task AddAsync(DateTime uploadAt, int userid, string path, string fileName, Stream stream, string contentType); 15 | Task AddAsync(DateTime uploadAt, int userid, string path, string fileName, byte[] bytes, string contentType); 16 | } 17 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/CommentsView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/comments/" 3 | @inject HttpClient _http 4 | @inject IStringLocalizer _localizer 5 | @inject IToaster _toaster 6 | 7 | 8 | 9 |

@_localizer["comments-settings"]

10 |
11 |
12 | @_localizer["under-development"]. 13 | 14 | @_localizer["more-info"] 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/MenusView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/customize/menus/" 3 | @inject IStringLocalizer _localizer 4 | 5 | 6 | 7 |

@_localizer["menus"]

8 |

@_localizer["theme-customization-desc"]

9 |
10 |
11 | @_localizer["under-development"]. 12 | 13 | @_localizer["more-info"] 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/WidgetsView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/customize/widgets/" 3 | @inject IStringLocalizer _localizer 4 | 5 | 6 | 7 |

@_localizer["widgets"]

8 |

@_localizer["theme-customization-desc"]

9 |
10 |
11 | @_localizer["under-development"]. 12 | 13 | @_localizer["more-info"] 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Shared; 5 | 6 | public class PostDto 7 | { 8 | public int Id { get; set; } 9 | public UserDto User { get; set; } = default!; 10 | public string Title { get; set; } = default!; 11 | public string? Slug { get; set; } = default!; 12 | public string Description { get; set; } = default!; 13 | public string Content { get; set; } = default!; 14 | public string? Cover { get; set; } 15 | public int Views { get; set; } 16 | public DateTime? PublishedAt { get; set; } 17 | public PostType PostType { get; set; } 18 | public PostState State { get; set; } 19 | public List? Categories { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/search.cshtml: -------------------------------------------------------------------------------- 1 | @model SearchModel 2 | @inject IStringLocalizer _localizer 3 | @{ 4 | Layout = "layouts/_main.cshtml"; 5 | } 6 | 7 |
8 |
9 | 10 |
11 |

Search Results

12 | @foreach (var post in Model.Pager.Items) 13 | { 14 |
15 |
16 | @post.Title 17 |
18 |
19 | @post.Title 20 |

@post.Description

21 |
22 |
23 | } 24 |
25 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/MailController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Newsletters; 2 | using Blogifier.Shared; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Interfaces; 8 | 9 | [Route("api/mail")] 10 | [ApiController] 11 | [Authorize] 12 | public class MailController(EmailManager emailManager) : ControllerBase 13 | { 14 | private readonly EmailManager _emailManager = emailManager; 15 | 16 | [HttpGet("settings")] 17 | public async Task GetSettingsAsync() => await _emailManager.GetSettingsAsync(); 18 | 19 | [HttpPut("settings")] 20 | public async Task PutSettingsAsync([FromBody] MailSettingDto input) => await _emailManager.PutSettingsAsync(input); 21 | } 22 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/ThemesView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/themes/" 3 | @inject HttpClient _http 4 | @inject IToaster _toaster 5 | @inject IStringLocalizer _localizer 6 | 7 | 8 | 9 |

@_localizer["menus"]

10 |

@_localizer["theme-customization-desc"]

11 |
12 |
13 | @_localizer["under-development"]. 14 | 15 | @_localizer["more-info"] 16 | 17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/include/_section.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | &-content { 3 | width: 100%; 4 | &:not(:last-child) { 5 | margin-bottom: 4rem; 6 | } 7 | 8 | @media screen and (min-width: 992px) { 9 | &.-full { 10 | max-width: 100%; 11 | } 12 | &.-half { 13 | max-width: 50%; 14 | } 15 | } 16 | } 17 | 18 | &-title { 19 | font-size: 1.25rem; 20 | font-weight: 400; 21 | border-bottom: 1px solid $gray-200; 22 | padding-bottom: 0.5rem; 23 | margin-bottom: 1rem; 24 | } 25 | 26 | &-desc { 27 | &:not(.alert) { 28 | color: $gray-600; 29 | } 30 | } 31 | 32 | &-subtitle { 33 | font-size: 1rem; 34 | font-weight: 500; 35 | margin-bottom: 0.5rem; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/MailSettingDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class MailSettingDto 6 | { 7 | public bool Enabled { get; set; } 8 | [StringLength(160)] 9 | public string Host { get; set; } = default!; 10 | public int Port { get; set; } 11 | [EmailAddress] 12 | [StringLength(120)] 13 | public string UserEmail { get; set; } = default!; 14 | [StringLength(120)] 15 | public string UserPassword { get; set; } = default!; 16 | [StringLength(120)] 17 | public string FromName { get; set; } = default!; 18 | [EmailAddress] 19 | [StringLength(120)] 20 | public string FromEmail { get; set; } = default!; 21 | [StringLength(120)] 22 | public string ToName { get; set; } = default!; 23 | } 24 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/UserEditorDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class UserEditorDto 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } = default!; 10 | [Required] 11 | public string UserName { get; set; } = default!; 12 | [DataType(DataType.Password)] 13 | public string? Password { get; set; } = default!; 14 | [DataType(DataType.Password)] 15 | [Compare("Password", ErrorMessage = "Passwords do not match")] 16 | public string? PasswordConfirm { get; set; } = default!; 17 | [Required] 18 | public string NickName { get; set; } = default!; 19 | public string? Avatar { get; set; } 20 | public string? Bio { get; set; } 21 | public UserType Type { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine as sdk 2 | # TOTO zh-CH 3 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 4 | RUN apk add --no-cache npm 5 | # Copy everything else and build 6 | COPY ./ /opt/blogifier 7 | WORKDIR /opt/blogifier 8 | RUN ["dotnet","publish", "-c", "Release","/p:RuntimeIdentifier=linux-musl-x64", "./src/Blogifier/Blogifier.csproj","-o","dist" ] 9 | 10 | FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine as run 11 | # TOTO zh-CH 12 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 13 | RUN apk add --no-cache icu-libs 14 | ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false 15 | COPY --from=sdk /opt/blogifier/dist /opt/blogifier/ 16 | WORKDIR /opt/blogifier 17 | ENTRYPOINT ["dotnet", "Blogifier.dll"] 18 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/MainDto.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Identity; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Shared; 5 | 6 | public class MainDto 7 | { 8 | public string Title { get; set; } = default!; 9 | public string Description { get; set; } = default!; 10 | public string? Logo { get; set; } 11 | public string Theme { get; set; } = default!; 12 | public string? HeaderScript { get; set; } 13 | public string? FooterScript { get; set; } 14 | public string Version { get; set; } = default!; 15 | public int ItemsPerPage { get; set; } 16 | public IEnumerable Categories { get; set; } = default!; 17 | public string? AbsoluteUrl { get; set; } 18 | public string? PathUrl { get; set; } 19 | public BlogifierClaims? Claims { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Services/ToasterService.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared.Resources; 2 | using Microsoft.Extensions.Localization; 3 | using Sotsera.Blazor.Toaster; 4 | using System.Net.Http; 5 | 6 | namespace Blogifier.Admin.Services; 7 | 8 | public class ToasterService(IToaster toaster, IStringLocalizer localizer) 9 | { 10 | private readonly IToaster _toaster = toaster; 11 | private readonly IStringLocalizer _localizer = localizer; 12 | 13 | public bool CheckResponse(HttpResponseMessage response) 14 | { 15 | if (response.IsSuccessStatusCode) 16 | { 17 | _toaster.Success(_localizer["completed"]); 18 | return true; 19 | } 20 | else 21 | { 22 | _toaster.Error(_localizer["generic-error"]); 23 | return false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/include/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn-rounded { 2 | border-radius: 6rem; 3 | } 4 | 5 | .btn { 6 | text-transform: capitalize; 7 | 8 | &-link {} 9 | 10 | &-outline { 11 | &-default { 12 | border-color: $secondary; 13 | color: $gray-600; 14 | 15 | &:hover { 16 | color: $gray-800; 17 | border-color: $gray-500; 18 | } 19 | } 20 | } 21 | 22 | &-xs { 23 | padding: 0.125rem 0.5rem; 24 | font-size: 0.75rem; 25 | border-radius: 0.2rem; 26 | } 27 | 28 | &-block { 29 | width: 100%; 30 | } 31 | 32 | &-floating { 33 | height: 3.125rem; 34 | font-size: 0.9375rem; 35 | } 36 | 37 | &-a { 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Blogifier/Profiles/PostProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Shared; 3 | 4 | namespace Blogifier.Profiles; 5 | 6 | public class PostProfile : Profile 7 | { 8 | public PostProfile() 9 | { 10 | CreateMap(); 11 | CreateMap(); 12 | CreateMap(); 13 | CreateMap(); 14 | CreateMap() 15 | .ForMember(d => d.Categories, opt => opt.MapFrom(src => src.PostCategories)) 16 | //.ForMember(d => d.Storages, opt => opt.MapFrom(src => src.StorageReferences)) 17 | .ReverseMap() 18 | .ForMember(d => d.PostCategories, opt => opt.MapFrom(src => src.Categories)) 19 | //.ForMember(d => d.StorageReferences, opt => opt.MapFrom(src => src.Storages)) 20 | ; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/CustomizeView.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/settings/customize/" 2 | 3 | @layout SettingsLayout 4 | 5 | @inject HttpClient _http 6 | @inject IStringLocalizer _localizer 7 | @inject IJSRuntime _jsRuntime 8 | @inject IToaster _toaster 9 | 10 | 11 | 12 |

@_localizer["menus"]

13 |

@_localizer["theme-customization-desc"]

14 |
15 |
16 | @_localizer["under-development"]. 17 | 18 | @_localizer["more-info"] 19 | 20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /docs/01-Authentication.md: -------------------------------------------------------------------------------- 1 | 2 | ### User Login and Registration 3 | On the first login, user gets redirected to the `admin/register` page to register a new account. 4 | Once account created, registration page is disabled and this user becomes a blog owner. 5 | 6 | ### Authentication 7 | The blog posts, including blog themes, are all run as public, server-side rendered MVC site. 8 | 9 | Anything under `admin` is Blazor Web Assembly application and is guarded by custom authentication provider (`BlogAuthenticationStateProvider`). 10 | User password is one-way hashed and saved in the `Authors` table on the back-end. 11 | The salt used to hash password pulled from `appsettings.json` configuration file and should be updated **before** creating user account. 12 | 13 | ``` 14 | "Blogifier": { 15 | ... 16 | "Salt": "SECRET-CHANGE-ME!" 17 | } 18 | ``` -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/layout/_main.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | cursor: default; 6 | overflow-y: scroll; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | 11 | &::-webkit-scrollbar { 12 | width: 0.75rem; 13 | } 14 | 15 | &::-webkit-scrollbar-track { 16 | background-color: #fff; 17 | box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.3); 18 | } 19 | 20 | &::-webkit-scrollbar-thumb { 21 | background-color: $gray-500; 22 | border-radius: 5rem; 23 | &:hover { 24 | background: $gray-600; 25 | } 26 | } 27 | } 28 | 29 | // index.html 30 | .blogifier { 31 | height: 100%; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | // shared/MainLayout.razor 37 | .content { 38 | } 39 | 40 | // Components/NavMenuComponent.razor 41 | .menu { 42 | } 43 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/bi-hash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Blogifier/Identity/SignInManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace Blogifier.Identity; 8 | 9 | public class SignInManager : SignInManager 10 | { 11 | public SignInManager( 12 | UserManager userManager, 13 | IHttpContextAccessor contextAccessor, 14 | IUserClaimsPrincipalFactory claimsFactory, 15 | IOptions optionsAccessor, 16 | ILogger> logger, 17 | IAuthenticationSchemeProvider schemes, 18 | IUserConfirmation confirmation) 19 | : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) 20 | { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Blogifier/Identity/UserInfo.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using Microsoft.AspNetCore.Identity; 3 | using System; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Blogifier.Identity; 7 | 8 | public class UserInfo : IdentityUser 9 | { 10 | public UserInfo() : base() 11 | { 12 | 13 | } 14 | 15 | public UserInfo(string userName) : base() => UserName = userName; 16 | 17 | public DateTime CreatedAt { get; set; } 18 | public DateTime UpdatedAt { get; set; } 19 | [StringLength(256)] 20 | public string NickName { get; set; } = default!; 21 | [StringLength(1024)] 22 | public string? Avatar { get; set; } 23 | [StringLength(2048)] 24 | public string? Bio { get; set; } 25 | [StringLength(32)] 26 | public string? Gender { get; set; } 27 | public UserType Type { get; set; } 28 | public UserState State { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Blogifier.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.IO; 4 | 5 | namespace Blogifier.Tests 6 | { 7 | public class TestHelper 8 | { 9 | public string Slash { get { return Path.DirectorySeparatorChar.ToString(); } } 10 | public string ContextRoot 11 | { 12 | get 13 | { 14 | string path = Directory.GetCurrentDirectory(); 15 | return path.Substring(0, path.IndexOf($"tests{Slash}Blogifier.Tests")); 16 | } 17 | } 18 | 19 | public AppDbContext GetDbContext() 20 | { 21 | return new AppDbContext(new DbContextOptionsBuilder().UseSqlite(GetDataSource()).Options); 22 | } 23 | 24 | private string GetDataSource() 25 | { 26 | return $"DataSource={ContextRoot}src{Slash}Blogifier{Slash}Blog.db"; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostToHtmlDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Blogifier.Shared; 5 | 6 | public class PostToHtmlDto 7 | { 8 | public int Id { get; set; } 9 | public UserDto User { get; set; } = default!; 10 | public string Title { get; set; } = default!; 11 | public string? Slug { get; set; } = default!; 12 | public string Description { get; set; } = default!; 13 | public string DescriptionHtml { get; set; } = default!; 14 | public string Content { get; set; } = default!; 15 | public string ContentHtml { get; set; } = default!; 16 | public string? Cover { get; set; } 17 | public int Views { get; set; } 18 | public DateTime? PublishedAt { get; set; } 19 | public PostType PostType { get; set; } 20 | public PostState State { get; set; } 21 | public List? Categories { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/include/_blazor.scss: -------------------------------------------------------------------------------- 1 | .blogifier-loading { 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | z-index: 10000; 6 | background: #fff; 7 | } 8 | 9 | .blogifier-error { 10 | position: fixed; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | z-index: 10001; 15 | background: $danger; 16 | padding: 1rem; 17 | display: none; 18 | 19 | &-message { 20 | margin-bottom: 0; 21 | font-size: 1rem; 22 | font-weight: 500; 23 | color: #fff; 24 | } 25 | 26 | &-btn { 27 | padding: 0; 28 | margin: 0; 29 | border: 0; 30 | background: none; 31 | width: 3rem; 32 | line-height: 3rem; 33 | vertical-align: top; 34 | cursor: pointer; 35 | 36 | .bi { 37 | fill: #fff; 38 | } 39 | 40 | &:hover { 41 | .bi { 42 | transform: scale(1.2); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/404.cshtml: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer _localizer 2 | @{ 3 | Layout = "layouts/_base.cshtml"; 4 | } 5 | 6 | @section HeaderScript{ 7 | @_localizer["page-not-found"] 8 | 9 | } 10 | 11 |
12 |

@_localizer["page-not-found"]

13 |

@_localizer["page-not-found-message"]

14 |

15 | 16 | 17 | 18 | 19 | @_localizer["home"] 20 | 21 |

22 |
23 | 24 | -------------------------------------------------------------------------------- /src/Blogifier/Storages/Storage.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using Blogifier.Identity; 3 | using Blogifier.Shared; 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace Blogifier.Storages; 8 | 9 | public class Storage : AppEntity 10 | { 11 | public int UserId { get; set; } 12 | public UserInfo User { get; set; } = default!; 13 | public DateTime CreatedAt { get; set; } 14 | public DateTime UploadAt { get; set; } 15 | [StringLength(2048)] 16 | public string Slug { get; set; } = default!; 17 | [StringLength(1024)] 18 | public string Name { get; set; } = default!; 19 | [StringLength(2048)] 20 | public string Path { get; set; } = default!; 21 | public long Length { get; set; } 22 | [StringLength(128)] 23 | public string ContentType { get; set; } = default!; 24 | public StorageType Type { get; set; } 25 | //public List? StorageReferences { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/components/_pagination.scss: -------------------------------------------------------------------------------- 1 | .pagination { 2 | margin: 4rem auto 4rem; 3 | height: 3rem; 4 | align-items: center; 5 | justify-content: center; 6 | 7 | &-item { 8 | margin: 0 0.5rem; 9 | z-index: 5; 10 | } 11 | &-link { 12 | display: block; 13 | color: #888; 14 | font-weight: 500; 15 | font-size: 0.875rem; 16 | height: 3rem; 17 | line-height: 3rem; 18 | text-decoration: none; 19 | text-transform: uppercase; 20 | transition: opacity ease 0.2s; 21 | opacity: 0.5; 22 | 23 | &:hover { 24 | opacity: 1; 25 | color: $color; 26 | .bi { 27 | width: 1.375rem; 28 | height: 1.375rem; 29 | } 30 | } 31 | .bi { 32 | width: 1.25rem; 33 | height: 1.25rem; 34 | transition: all ease 0.2s; 35 | margin: 0 0.25rem; 36 | transform: translateY(-0.0625rem); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using System.Text.Json 4 | 5 | @using Microsoft.AspNetCore.Components.Authorization 6 | @using Microsoft.AspNetCore.Components.Forms 7 | @using Microsoft.AspNetCore.Components.Routing 8 | @using Microsoft.AspNetCore.Components.Web 9 | @using Microsoft.AspNetCore.Components.Web.Virtualization 10 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 11 | @using Microsoft.JSInterop 12 | @using Microsoft.Extensions.Localization 13 | @using Microsoft.Extensions.Logging 14 | 15 | @using Sotsera.Blazor.Toaster 16 | 17 | @using Blogifier; 18 | @using Blogifier.Admin 19 | @using Blogifier.Admin.Components 20 | @using Blogifier.Admin.Shared 21 | @using Blogifier.Admin.Services 22 | @using Blogifier.Shared 23 | @using Blogifier.Shared.Resources 24 | @using Blogifier.Helper; 25 | @using Blogifier.Identity 26 | @using Blogifier.Models 27 | @using Blogifier.Admin.Interop 28 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Extensions/PrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Identity; 2 | using System; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | 6 | namespace Blogifier.Shared; 7 | 8 | public static class PrincipalExtensions 9 | { 10 | public static string FirstValue(this ClaimsPrincipal principal, string claimType) 11 | { 12 | var claim = principal.Claims.First(m => claimType.Equals(m.Type, StringComparison.OrdinalIgnoreCase)); 13 | return claim.Value; 14 | } 15 | 16 | public static string? FirstOrDefault(this ClaimsPrincipal principal, string claimType) 17 | { 18 | var claim = principal.Claims.FirstOrDefault(m => claimType.Equals(m.Type, StringComparison.OrdinalIgnoreCase)); 19 | return claim?.Value; 20 | } 21 | 22 | public static int FirstUserId(this ClaimsPrincipal principal) 23 | { 24 | var userIdString = FirstValue(principal, BlogifierClaimTypes.UserId); 25 | return int.Parse(userIdString); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/ImportController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Posts; 2 | using Blogifier.Shared; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace Blogifier.Interfaces; 9 | 10 | [Route("api/import")] 11 | [Authorize] 12 | [ApiController] 13 | public class ImportController(ImportManager importManager) : ControllerBase 14 | { 15 | private readonly ImportManager _importManager = importManager; 16 | 17 | [HttpGet("rss")] 18 | public ImportDto Rss([FromQuery] ImportRssDto request, [FromServices] ImportRssProvider importRssProvider) 19 | { 20 | return importRssProvider.Analysis(request.FeedUrl); 21 | } 22 | 23 | [HttpPost("write")] 24 | public async Task> Write([FromBody] ImportDto request) 25 | { 26 | var userId = User.FirstUserId(); 27 | return await _importManager.WriteAsync(request, userId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/PostManager.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace Blogifier.Posts; 6 | 7 | public class PostManager( 8 | PostProvider postProvider, 9 | MarkdigProvider markdigProvider) 10 | { 11 | private readonly PostProvider _postProvider = postProvider; 12 | private readonly MarkdigProvider _markdigProvider = markdigProvider; 13 | 14 | public async Task GetToHtmlAsync(string slug) 15 | { 16 | var postSlug = await _postProvider.GetAsync(slug); 17 | postSlug.Post.ContentHtml = _markdigProvider.ToHtml(postSlug.Post.Content); 18 | postSlug.Post.DescriptionHtml = _markdigProvider.ToHtml(postSlug.Post.Description); 19 | 20 | foreach (var related in postSlug.Related) 21 | { 22 | var relatedDto = postSlug.Related.First(m => m.Id == related.Id); 23 | relatedDto.DescriptionHtml = _markdigProvider.ToHtml(related.Description); 24 | } 25 | return postSlug; 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/CategoryController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Blogs; 2 | using Blogifier.Posts; 3 | using Blogifier.Shared; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Controllers; 8 | 9 | [Route("category")] 10 | public class CategoryController( 11 | MainMamager mainMamager, 12 | PostProvider postProvider) : Controller 13 | { 14 | private readonly MainMamager _mainMamager = mainMamager; 15 | private readonly PostProvider _postProvider = postProvider; 16 | 17 | [HttpGet("{category}")] 18 | public async Task Category([FromRoute] string category, [FromQuery] int page = 1) 19 | { 20 | var main = await _mainMamager.GetAsync(); 21 | var pager = await _postProvider.GetByCategoryAsync(category, page, main.ItemsPerPage); 22 | pager.Configure(main.PathUrl, "page"); 23 | var model = new CategoryModel(category, pager, main); 24 | return View($"~/Views/Themes/{main.Theme}/category.cshtml", model); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/layout/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | padding-top: 4rem; 3 | padding-bottom: 4rem; 4 | 5 | &-title { 6 | display: inline-block; 7 | margin: 0; 8 | font-size: var(--bf-header-title-size); 9 | font-weight: var(--bf-header-title-weight); 10 | color: var(--bf-header-title-color); 11 | line-height: 1.2; 12 | text-decoration: none; 13 | &:hover, 14 | &:focus { 15 | color: var(--bf-header-title-hover); 16 | } 17 | } 18 | 19 | &-desc { 20 | margin: 0; 21 | margin-top: 0.432rem; 22 | font-size: 0.875rem; 23 | font-weight: 400; 24 | color: var(--bf-header-desc-color, #666); 25 | } 26 | 27 | &-logo { 28 | display: block; 29 | &-img { 30 | width: var(--bf-header-logo-width); 31 | height: var(--bf-header-logo-height); 32 | } 33 | } 34 | 35 | @media (max-width: 767px) { 36 | padding: 0; 37 | margin-bottom: 2rem; 38 | background-color: var(--bs-light); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/components/newsletter.cshtml: -------------------------------------------------------------------------------- 1 | @model MainModel 2 | 3 | 18 | -------------------------------------------------------------------------------- /src/Blogifier/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "IIS Express": { 4 | "commandName": "IISExpress", 5 | "environmentVariables": { 6 | "ASPNETCORE_ENVIRONMENT": "Development" 7 | }, 8 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 9 | }, 10 | "Blogifier": { 11 | "commandName": "Project", 12 | "launchBrowser": false, 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | }, 16 | "dotnetRunMessages": "true", 17 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 18 | "applicationUrl": "http://localhost:5000" 19 | } 20 | }, 21 | "iisSettings": { 22 | "windowsAuthentication": false, 23 | "anonymousAuthentication": true, 24 | "iisExpress": { 25 | "applicationUrl": "http://localhost:3169", 26 | "sslPort": 0 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/NewsletterController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Newsletters; 2 | using Blogifier.Shared; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace Blogifier.Interfaces; 9 | 10 | [Route("api/newsletter")] 11 | [ApiController] 12 | [Authorize] 13 | public class NewsletterController(NewsletterProvider newsletterProvider) : ControllerBase 14 | { 15 | private readonly NewsletterProvider _newsletterProvider = newsletterProvider; 16 | 17 | [HttpGet("items")] 18 | public async Task> GetItemsAsync() => await _newsletterProvider.GetItemsAsync(); 19 | 20 | [HttpDelete("{id:int}")] 21 | public async Task DeleteAsync([FromRoute] int id) => await _newsletterProvider.DeleteAsync(id); 22 | 23 | [HttpGet("send/{postId:int}")] 24 | public async Task SendNewsletter([FromRoute] int postId, [FromServices] EmailManager emailManager) => 25 | await emailManager.SendNewsletter(postId); 26 | } 27 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:3169", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Blogifier": { 20 | "commandName": "Project", 21 | "dotnetRunMessages": "true", 22 | "launchBrowser": true, 23 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_base.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 26 | 27 | @await RenderSectionAsync("HeaderScript", required: false) 28 | 29 | 30 | 31 | @RenderBody() 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/post/nav.cshtml: -------------------------------------------------------------------------------- 1 | @model PostModel 2 | 3 | @inject IStringLocalizer _localizer 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Blogs; 3 | using Blogifier.Shared; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Logging; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Controllers; 10 | 11 | public class ErrorController( 12 | ILogger logger, 13 | IMapper mapper, 14 | MainMamager mainMamager) : Controller 15 | { 16 | protected readonly ILogger _logger = logger; 17 | protected readonly IMapper _mapper = mapper; 18 | protected readonly MainMamager _mainMamager = mainMamager; 19 | 20 | [Route("404")] 21 | public async Task Error404() 22 | { 23 | try 24 | { 25 | var data = await _mainMamager.GetAsync(); 26 | var model = new MainModel(data); 27 | return View($"~/Views/Themes/{data.Theme}/404.cshtml", model); 28 | } 29 | catch (Exception ex) 30 | { 31 | _logger.LogError(ex, "error page exception"); 32 | return View($"~/Views/404.cshtml"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/ReverseProvider.cs: -------------------------------------------------------------------------------- 1 | using ReverseMarkdown; 2 | 3 | namespace Blogifier.Posts; 4 | 5 | public class ReverseProvider 6 | { 7 | private readonly Converter _converter; 8 | 9 | public ReverseProvider() 10 | { 11 | var config = new Config 12 | { 13 | // generate GitHub flavoured markdown, supported for BR, PRE and table tags 14 | GithubFlavored = true, 15 | // will ignore all comments 16 | RemoveComments = true, 17 | // Include the unknown tag completely in the result (default as well) 18 | UnknownTags = Config.UnknownTagsOption.Bypass, 19 | // remove markdown output for links where appropriate 20 | SmartHrefHandling = false, 21 | PassThroughTags = new string[] { "figure" } 22 | }; 23 | _converter = new Converter(config); 24 | } 25 | 26 | public string ToMarkdown(string html) 27 | { 28 | var markdown = _converter.Convert(html); 29 | //_logger.LogDebug("ToMarkdown html:{html}, markdown:{markdown}", html, markdown); 30 | return markdown; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch and Debug", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "program": "dotnet", 12 | "args": [ 13 | "watch" 14 | ], 15 | "cwd": "${workspaceFolder}/src/Blogifier", 16 | "env": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | }, 19 | "launchBrowser": { 20 | "enabled": true, 21 | "args": "${auto-detect-url}", 22 | "windows": { 23 | "command": "cmd.exe", 24 | "args": "/C start ${auto-detect-url}", 25 | }, 26 | "osx": { 27 | "command": "open" 28 | }, 29 | "linux": { 30 | "command": "xdg-open" 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/Blogifier/Identity/UserManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace Blogifier.Identity; 9 | 10 | public class UserManager( 11 | IUserStore store, 12 | IOptions optionsAccessor, 13 | IPasswordHasher passwordHasher, 14 | IEnumerable> userValidators, 15 | IEnumerable> passwordValidators, 16 | ILookupNormalizer keyNormalizer, 17 | IdentityErrorDescriber errors, 18 | IServiceProvider services, 19 | ILogger> logger, 20 | UserProvider userProvider) : UserManager(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) 21 | { 22 | protected readonly UserProvider _userProvider = userProvider; 23 | 24 | public Task FindByIdAsync(int userId) => _userProvider.FindAsync(userId); 25 | } 26 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/StorageController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Storages; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.OutputCaching; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Controllers; 8 | 9 | public class StorageController( 10 | IStorageProvider storageProvider) : ControllerBase 11 | { 12 | private readonly IStorageProvider _storageProvider = storageProvider; 13 | 14 | [HttpGet($"{BlogifierConstant.StorageRowPhysicalRoot}/{{**slug}}")] 15 | [ResponseCache(VaryByHeader = "User-Agent", Duration = 3600)] 16 | [OutputCache(PolicyName = BlogifierConstant.OutputCacheExpire1)] 17 | public async Task GetAsync([FromRoute] string slug) 18 | { 19 | var memoryStream = new MemoryStream(); 20 | var storage = await _storageProvider.GetAsync(slug, 21 | (stream, cancellationToken) => stream.CopyToAsync(memoryStream, cancellationToken)); 22 | if (storage == null) return NotFound(); 23 | memoryStream.Position = 0; 24 | return File(memoryStream, storage.ContentType); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blogifier/Identity/UserClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.Extensions.Options; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | 6 | namespace Blogifier.Identity; 7 | 8 | public class UserClaimsPrincipalFactory : UserClaimsPrincipalFactory 9 | { 10 | public UserClaimsPrincipalFactory( 11 | UserManager userManager, 12 | IOptions options) 13 | : base(userManager, options) 14 | { 15 | } 16 | 17 | public override async Task CreateAsync(UserInfo user) 18 | { 19 | var claimsPrincipal = await base.CreateAsync(user); 20 | var id = new ClaimsIdentity("Application"); 21 | id.AddClaim(new Claim(BlogifierClaimTypes.NickName, user.NickName)); 22 | id.AddClaim(new Claim(BlogifierClaimTypes.Type, ((int)user.Type).ToString())); 23 | if (!string.IsNullOrEmpty(user.Avatar)) 24 | id.AddClaim(new Claim(BlogifierClaimTypes.Avatar, user.Avatar)); 25 | claimsPrincipal.AddIdentity(id); 26 | return claimsPrincipal; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/SearchController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Blogs; 2 | using Blogifier.Posts; 3 | using Blogifier.Shared; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Controllers; 8 | 9 | [Route("search")] 10 | public class SearchController( 11 | MainMamager mainMamager, 12 | PostProvider postProvider) : Controller 13 | { 14 | private readonly MainMamager _mainMamager = mainMamager; 15 | private readonly PostProvider _postProvider = postProvider; 16 | 17 | [HttpPost] 18 | public async Task Post([FromQuery] string term, [FromQuery] int page = 1) 19 | { 20 | if (!string.IsNullOrEmpty(term)) 21 | { 22 | var main = await _mainMamager.GetAsync(); 23 | var pager = await _postProvider.GetSearchAsync(term, page, main.ItemsPerPage); 24 | pager.Configure(main.PathUrl, "page"); 25 | var model = new SearchModel(pager, main); 26 | return View($"~/Views/Themes/{main.Theme}/search.cshtml", model); 27 | } 28 | else 29 | { 30 | return Redirect("~/"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/AnalyticsController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Blogs; 2 | using Blogifier.Shared; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Interfaces; 8 | 9 | [Route("api/analytics")] 10 | [ApiController] 11 | [Authorize] 12 | public class AnalyticsController(AnalyticsProvider analyticsProvider) : ControllerBase 13 | { 14 | private readonly AnalyticsProvider _analyticsProvider = analyticsProvider; 15 | 16 | [HttpGet] 17 | public async Task GetAnalytics() 18 | { 19 | var blogs = await _analyticsProvider.GetPostSummaryAsync(); 20 | return new AnalyticsDto { Blogs = blogs }; 21 | } 22 | 23 | //[HttpPut("displayType/{typeId:int}")] 24 | //public async Task SaveDisplayType(int typeId) 25 | //{ 26 | // await _analyticsProvider.SaveDisplayType(typeId); 27 | //} 28 | 29 | //[HttpPut("displayPeriod/{typeId:int}")] 30 | //public async Task SaveDisplayPeriod(int typeId) 31 | //{ 32 | // await _analyticsProvider.SaveDisplayPeriod(typeId); 33 | //} 34 | } 35 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/components/header.cshtml: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer _localizer 2 | @model MainModel 3 | @{ 4 | @* TODO: Customfield *@ 5 | var siteMark = "titleDesc"; 6 | } 7 | 8 |
9 |
10 |
11 | @if (siteMark == "logo") 12 | { 13 | 16 | } 17 | else if (siteMark == "title") 18 | { 19 | @Model.Main.Title 20 | } 21 | else if (siteMark == "titleDesc") 22 | { 23 | @Model.Main.Title 24 |

@Model.Main.Description

25 | } 26 |
27 | 28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/pages/settings/_themes.scss: -------------------------------------------------------------------------------- 1 | .themes { 2 | &-item { 3 | margin-bottom: 3rem; 4 | } 5 | &-thumbnail { 6 | border-radius: 0.5rem; 7 | display: block; 8 | // 4:5 9 | height: 15.625rem; 10 | width: 12.5rem; 11 | max-width: 12.5rem; 12 | min-width: 12.5rem; 13 | object-fit: cover; 14 | object-position: center; 15 | border: 1px solid $gray-200; 16 | } 17 | 18 | &-info { 19 | flex-grow: 1; 20 | margin-left: 2rem; 21 | padding-top: 1.5rem; 22 | } 23 | 24 | &-title { 25 | margin-bottom: 1.25rem; 26 | } 27 | 28 | &-meta { 29 | list-style: none; 30 | padding: 0; 31 | margin: 0 0 1.25rem; 32 | color: $gray-600; 33 | 34 | a[href] { 35 | color: $gray-600; 36 | border-bottom: 1px solid $gray-500; 37 | 38 | &:hover { 39 | color: $blogifier; 40 | border-color: $blogifier; 41 | } 42 | } 43 | 44 | &-item { 45 | margin-right: 1.5rem; 46 | } 47 | } 48 | 49 | &-desc { 50 | margin-bottom: 1.5rem; 51 | width: 100%; 52 | max-width: 75%; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Blogifier.Tests/Blogifier.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rxtur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/pages/settings/_about.scss: -------------------------------------------------------------------------------- 1 | .about { 2 | &-blogifier { 3 | margin: 0; 4 | padding: 0; 5 | list-style: none; 6 | &-nav { 7 | list-style: none; 8 | margin: 0; 9 | padding: 0; 10 | li { 11 | font-weight: 500; 12 | display: inline-block; 13 | margin-right: 2rem; 14 | } 15 | 16 | a { 17 | display: inline-block; 18 | } 19 | 20 | .bi { 21 | margin-right: 0.125rem; 22 | transform: translateY(-0.125rem); 23 | } 24 | } 25 | 26 | &-desc { 27 | font-size: 1rem; 28 | width: 100%; 29 | max-width: 28rem; 30 | } 31 | } 32 | 33 | &-info { 34 | list-style: none; 35 | margin: 0; 36 | padding: 0; 37 | } 38 | 39 | &-info { 40 | padding: 0; 41 | background: none; 42 | border: 0; 43 | width: 100%; 44 | 45 | &-item { 46 | margin-bottom: 0.5rem; 47 | display: flex; 48 | } 49 | &-label { 50 | font-weight: 500; 51 | min-width: 10rem; 52 | } 53 | &-value { 54 | color: $gray-800; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Dtos/PostPagerDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blogifier.Shared; 4 | 5 | public class PostPagerDto 6 | { 7 | public PostPagerDto(IEnumerable items, int total, int page, int pageSize) 8 | { 9 | Items = items; 10 | Total = total; 11 | Page = page; 12 | PageSize = pageSize; 13 | Pagination = total > pageSize; 14 | } 15 | 16 | public IEnumerable Items { get; set; } 17 | public int Total { get; set; } 18 | public int PageSize { get; } 19 | public int Page { get; } 20 | public bool Pagination { get; set; } 21 | public string? LinkToOlder { get; set; } 22 | public string? LinkToNewer { get; set; } 23 | public void Configure(string? path, string queryKey) 24 | { 25 | if (path != null && Pagination) 26 | { 27 | if (Page != 1) 28 | { 29 | var page = Page - 1; 30 | LinkToOlder = $"{path}?{queryKey}={page}"; 31 | } 32 | 33 | if (Page * PageSize < Total) 34 | { 35 | var page = Page + 1; 36 | LinkToNewer = $"{path}?{queryKey}={page}"; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Blogifier/Newsletters/SubscriberProvider.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Data; 3 | using Blogifier.Shared; 4 | using Microsoft.EntityFrameworkCore; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Blogifier.Newsletters; 11 | 12 | public class SubscriberProvider(IMapper mapper, AppDbContext dbContext) : AppProvider(dbContext) 13 | { 14 | private readonly IMapper _mapper = mapper; 15 | 16 | public async Task> GetItemsAsync() 17 | { 18 | var query = _dbContext.Subscribers 19 | .AsNoTracking() 20 | .OrderByDescending(n => n.CreatedAt); 21 | return await _mapper.ProjectTo(query).ToListAsync(); 22 | } 23 | 24 | public async Task ApplyAsync(SubscriberApplyDto input) 25 | { 26 | 27 | if (await _dbContext.Subscribers.AnyAsync(m => m.Email == input.Email)) 28 | return 0; 29 | else 30 | { 31 | var data = _mapper.Map(input); 32 | _dbContext.Subscribers.Add(data); 33 | await _dbContext.SaveChangesAsync(); 34 | return 1; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Blogifier/Posts/Post.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using Blogifier.Identity; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace Blogifier.Shared; 8 | 9 | public class Post : AppEntity 10 | { 11 | public DateTime CreatedAt { get; set; } 12 | public DateTime UpdatedAt { get; set; } 13 | public int UserId { get; set; } 14 | public UserInfo User { get; set; } = default!; 15 | [Required] 16 | [StringLength(160)] 17 | public string Title { get; set; } = default!; 18 | [Required] 19 | [StringLength(160)] 20 | public string Slug { get; set; } = default!; 21 | [Required] 22 | [StringLength(450)] 23 | public string Description { get; set; } = default!; 24 | public string Content { get; set; } = default!; 25 | [StringLength(160)] 26 | public string? Cover { get; set; } 27 | public int Views { get; set; } 28 | public DateTime? PublishedAt { get; set; } 29 | public PostType PostType { get; set; } 30 | public PostState State { get; set; } 31 | public List? PostCategories { get; set; } 32 | //public List? StorageReferences { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/SubscriberController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Newsletters; 2 | using Blogifier.Shared; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Interfaces; 10 | 11 | [Route("api/subscriber")] 12 | [ApiController] 13 | public class SubscriberController(SubscriberProvider subscriberProvider) : ControllerBase 14 | { 15 | private readonly SubscriberProvider _subscriberProvider = subscriberProvider; 16 | 17 | [HttpGet("items")] 18 | [Authorize] 19 | public async Task> GetItemsAsync() => await _subscriberProvider.GetItemsAsync(); 20 | 21 | [HttpDelete("{id:int}")] 22 | [Authorize] 23 | public async Task DeleteAsync([FromRoute] int id) => await _subscriberProvider.DeleteAsync(id); 24 | 25 | [HttpPost("apply")] 26 | public async Task ApplyAsync([FromBody] SubscriberApplyDto input) 27 | { 28 | var res = await _subscriberProvider.ApplyAsync(input); 29 | 30 | if(res == 1) 31 | { 32 | return Ok(); 33 | } 34 | 35 | return BadRequest(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Blogifier/Interfaces/StorageController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Shared; 2 | using Blogifier.Storages; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Interfaces; 10 | 11 | [Route("api/storage")] 12 | [ApiController] 13 | [Authorize] 14 | public class StorageController( 15 | IStorageProvider storageProvider, 16 | StorageManager storageManager) : ControllerBase 17 | { 18 | private readonly IStorageProvider _storageProvider = storageProvider; 19 | private readonly StorageManager _storageManager = storageManager; 20 | 21 | [HttpPut("exists")] 22 | public async Task ExistsAsync([FromBody] string slug) 23 | { 24 | if (await _storageProvider.ExistsAsync(slug)) 25 | { 26 | return Ok(); 27 | } 28 | return BadRequest(); 29 | } 30 | 31 | [HttpPost("upload")] 32 | public async Task Upload([FromForm] IFormFile file) 33 | { 34 | var userId = User.FirstUserId(); 35 | var currTime = DateTime.UtcNow; 36 | return await _storageManager.UploadAsync(currTime, userId, file); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Blogifier/Blogifier.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Blogifier/Blogifier.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/Blogifier/Blogifier.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/components/footer.cshtml: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer _localizer 2 | @model MainModel 3 | 4 | 13 | 14 |
15 | 16 | 23 |
24 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/blogifier.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Blogifier v3.0 (https://blogifier.net) 4 | Copyright 2012-2021 5 | 6 | by Farzin 7 | https://farzin.dev 8 | 9 | Using: 10 | - Bootstrap v5.1 (https://getbootstrap.com/) 11 | 12 | */ 13 | 14 | // helpers 15 | @import "helpers/variables"; 16 | @import "helpers/bootstrap"; 17 | @import "helpers/mixins"; 18 | @import "helpers/reset"; 19 | 20 | // layout 21 | @import "layout/main"; 22 | @import "layout/nav"; 23 | @import "layout/sidebar"; 24 | 25 | // components 26 | @import "include/blazor"; 27 | @import "include/section"; 28 | @import "include/buttons"; 29 | @import "include/forms"; 30 | @import "include/list"; 31 | @import "include/highlight"; 32 | @import "include/toaster"; 33 | @import "include/editor"; 34 | @import "include/tooltips"; 35 | @import "include/icons"; 36 | 37 | // pages > account 38 | @import "pages/account/account"; 39 | 40 | // pages > dashbaord 41 | @import "pages/dashboard/dashboard"; 42 | 43 | // pages > editor 44 | @import "pages/editor/editor"; 45 | 46 | // pages > settings 47 | @import "pages/settings/settings"; 48 | @import "pages/settings/about"; 49 | @import "pages/settings/themes"; 50 | @import "pages/settings/customize"; 51 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/components/pagination.cshtml: -------------------------------------------------------------------------------- 1 | @model PostPagerModel 2 | @inject IStringLocalizer _localizer 3 | 4 | @if (Model.Pager.Pagination) 5 | { 6 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Blogifier/Data/AppProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blogifier.Data; 8 | 9 | public class AppProvider where T : AppEntity where TKey : IEquatable 10 | { 11 | protected readonly AppDbContext _dbContext; 12 | 13 | protected AppProvider(AppDbContext dbContext) => _dbContext = dbContext; 14 | 15 | protected async Task AddAsync(T entity) 16 | { 17 | _dbContext.Set().Add(entity); 18 | await _dbContext.SaveChangesAsync(); 19 | } 20 | 21 | public Task DeleteAsync(TKey id) 22 | { 23 | var query = _dbContext.Set() 24 | .Where(m => id.Equals(m.Id)); 25 | return DeleteInternalAsync(query); 26 | } 27 | 28 | public Task DeleteAsync(IEnumerable? ids) 29 | { 30 | if (ids != null && ids.Any()) 31 | { 32 | var query = _dbContext.Set() 33 | .Where(m => ids.Contains(m.Id)); 34 | return DeleteInternalAsync(query); 35 | } 36 | return Task.CompletedTask; 37 | } 38 | 39 | protected static async Task DeleteInternalAsync(IQueryable query) 40 | { 41 | await query.ExecuteDeleteAsync(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Blogs; 2 | using Blogifier.Models; 3 | using Blogifier.Posts; 4 | using Blogifier.Shared; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Controllers; 10 | 11 | public class HomeController( 12 | ILogger logger, 13 | MainMamager mainMamager, 14 | PostProvider postProvider) : Controller 15 | { 16 | private readonly ILogger _logger = logger; 17 | private readonly MainMamager _mainMamager = mainMamager; 18 | private readonly PostProvider _postProvider = postProvider; 19 | 20 | [HttpGet] 21 | public async Task Index([FromQuery] int page = 1) 22 | { 23 | MainDto main; 24 | try 25 | { 26 | main = await _mainMamager.GetAsync(); 27 | } 28 | catch (BlogNotIitializeException ex) 29 | { 30 | _logger.LogError(ex, "blgo not iitialize redirect"); 31 | return Redirect("~/account/initialize"); 32 | } 33 | var pager = await _postProvider.GetPostsAsync(page, main.ItemsPerPage); 34 | pager.Configure(main.PathUrl, "page"); 35 | var model = new IndexModel(pager, main); 36 | return View($"~/Views/Themes/{main.Theme}/index.cshtml", model); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Shared/NewsletterLayout.razor: -------------------------------------------------------------------------------- 1 | @layout MainLayout 2 | @inherits LayoutComponentBase 3 | @inject NavigationManager _navigationManager 4 | @inject IJSRuntime _jsRuntime 5 | @inject IStringLocalizer _localizer 6 |
7 |
8 | 22 |
23 | @Body 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/helpers/_variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $blogifier: #7241fd; 3 | $default: #e1e5e9; 4 | $secondary: #8b949e; 5 | $gold: #ff9f2e; 6 | $black: #1c1c1c; 7 | $success: #00b45a; 8 | $danger: #ff4d58; 9 | 10 | // colors 11 | $color: var(--bf-color); 12 | $color-hover: #888; 13 | 14 | // brands colors 15 | $color-blogifier: #622aff; 16 | $color-facebook: #1678f2; 17 | $color-twitter: #1da1f2; 18 | $color-github: #171617; 19 | $color-pinterest: #e60023; 20 | $color-linkedin: #0a66c2; 21 | $color-instagram: #b900b4; 22 | $color-youtube: #ff0300; 23 | $color-telegram: #0088cc; 24 | $color-whatsapp: #25d366; 25 | 26 | $brands: ( 27 | "blogifier": $color-blogifier, 28 | "facebook": $color-facebook, 29 | "twitter": $color-twitter, 30 | "github": $color-github, 31 | "pinterest": $color-pinterest, 32 | "linkedin": $color-linkedin, 33 | "instagram": $color-instagram, 34 | "telegram": $color-telegram, 35 | "youtube": $color-youtube, 36 | "whatsapp": $color-whatsapp, 37 | ); 38 | 39 | // syntax highlighter 40 | $hljs-foreground: #212529; 41 | $hljs-background: #f8f9fa; 42 | $hljs-comments: #90a4ae; 43 | $hljs-blue: #006ee0; 44 | $hljs-red: #c30; 45 | $hljs-green: #168174; 46 | $hljs-sky: #2f6f9f; 47 | 48 | // radius 49 | $radius: 0.125rem; 50 | $radius-var: var(--bf-radius, $radius); 51 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/password.cshtml: -------------------------------------------------------------------------------- 1 | @model AccountProfilePasswordModel 2 | 3 | @{ 4 | Layout = "layouts/_profile.cshtml"; 5 | } 6 | 7 | @inject IStringLocalizer _localizer 8 | 9 | @section HeaderScript{ 10 | @_localizer["edit-profile"] 11 | } 12 | 13 |

@_localizer["change-password"]

14 |
15 | @if (!string.IsNullOrEmpty(Model.Error)) 16 | { 17 | 18 | } 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/Blogifier/Options/OptionProvider.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Caching.Distributed; 4 | using Microsoft.Extensions.Logging; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Blogifier.Options; 9 | 10 | public class OptionProvider( 11 | ILogger logger, 12 | IDistributedCache distributedCache, 13 | AppDbContext dbContext) 14 | { 15 | private readonly ILogger _logger = logger; 16 | private readonly AppDbContext _dbContext = dbContext; 17 | 18 | public async Task AnyKeyAsync(string key) => await _dbContext.Options.AnyAsync(m => m.Key == key); 19 | 20 | public async Task GetByValueAsync(string key) => 21 | await _dbContext.Options 22 | .AsNoTracking() 23 | .Where(m => m.Key == key) 24 | .Select(m => m.Value) 25 | .FirstOrDefaultAsync(); 26 | 27 | public async Task SetValue(string key, string value) 28 | { 29 | var option = await _dbContext.Options 30 | .Where(m => m.Key == key) 31 | .FirstOrDefaultAsync(); 32 | if (option == null) 33 | { 34 | _dbContext.Options.Add(new OptionInfo { Key = key, Value = value }); 35 | } 36 | else 37 | { 38 | option.Value = value; 39 | } 40 | await _dbContext.SaveChangesAsync(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | 5 | mysql: 6 | image: mysql:latest 7 | container_name: example_mysql 8 | command: 9 | - --default-authentication-plugin=mysql_native_password 10 | - --character-set-server=utf8mb4 11 | - --collation-server=utf8mb4_unicode_ci 12 | - --lower_case_table_names=1 13 | - --performance_schema=off 14 | restart: always 15 | environment: 16 | TZ: "Asia/Shanghai" 17 | MYSQL_ROOT_PASSWORD: blogifier 18 | volumes: 19 | - ./deploy/data/mysql/data:/var/lib/mysql 20 | 21 | redis: 22 | image: redis:alpine 23 | container_name: example_redis 24 | restart: always 25 | command: --requirepass "blogifier" 26 | volumes: 27 | - ./deploy/data/redis/data:/data 28 | 29 | blogifier: 30 | container_name: example_blogifier 31 | image: dorthl/blogifier:latest 32 | restart: always 33 | build: . 34 | ports: 35 | - '8080:80' 36 | environment: 37 | TZ: 'Asia/Shanghai' 38 | Blogifier__DbProvider: MySql 39 | Blogifier__ConnString: Server=mysql;Database=blogifier;User=root;Password=blogifier; 40 | Blogifier__Redis: redis:6379,password=blogifier,defaultDatabase=0 41 | volumes: 42 | - ./deploy/data/blogifier/app_data:/opt/blogifier/App_Data 43 | mem_limit: 256MB 44 | 45 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogifier-themes-standard", 3 | "version": "1.0.0", 4 | "description": "only for compiling sass files", 5 | "author": "Farzin", 6 | "private": true, 7 | "scripts": { 8 | "start": "gulp", 9 | "build": "gulp build", 10 | "build:Debug": "gulp build", 11 | "build:Release": "gulp release build" 12 | }, 13 | "dependencies": { 14 | "bootstrap": "~5.1.0", 15 | "highlight.js": "^11.8.0" 16 | }, 17 | "devDependencies": { 18 | "@babel/preset-env": "^7.22.9", 19 | "@rollup/plugin-babel": "^6.0.3", 20 | "@rollup/plugin-commonjs": "^25.0.0", 21 | "@rollup/plugin-node-resolve": "^15.1.0", 22 | "@rollup/plugin-terser": "^0.4.3", 23 | "@rollup/stream": "^3.0.0", 24 | "autoprefixer": "^10.4.14", 25 | "cssnano": "^6.0.1", 26 | "del": "^7.0.0", 27 | "gulp": "^4.0.2", 28 | "gulp-notify": "^4.0.0", 29 | "gulp-plumber": "^1.2.1", 30 | "gulp-postcss": "^9.0.1", 31 | "gulp-rename": "^2.0.0", 32 | "gulp-sass": "^5.1.0", 33 | "gulp-size": "^4.0.1", 34 | "gulp-sourcemaps": "^3.0.0", 35 | "gulp-svg-sprite": "^2.0.3", 36 | "gulp-uglify": "^3.0.2", 37 | "rollup": "^3.23.1", 38 | "rtlcss": "^3.3.0", 39 | "sass": "~1.32.12", 40 | "vinyl-buffer": "^1.0.1", 41 | "vinyl-source-stream": "^2.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/helpers/_reset.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | } 4 | 5 | iframe { 6 | width: 100% !important; 7 | margin-top: 1rem; 8 | margin-bottom: 1.5rem; 9 | } 10 | 11 | figure, 12 | p, 13 | ul, 14 | ol { 15 | margin-bottom: 1.5rem; 16 | } 17 | 18 | table { 19 | width: 100%; 20 | } 21 | 22 | blockquote { 23 | font-weight: 300; 24 | position: relative; 25 | padding: 1rem 3rem; 26 | margin-bottom: 1.5rem; 27 | &::before, 28 | &::after { 29 | position: absolute; 30 | 31 | content: ""; 32 | font-size: 5rem; 33 | width: 2.5rem; 34 | height: 2.5rem; 35 | background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDQ4IDQ4IiB3aWR0aD0iNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDM0aDZsNC04di0xMmgtMTJ2MTJoNnptMTYgMGg2bDQtOHYtMTJoLTEydjEyaDZ6Ii8+PHBhdGggZD0iTTAgMGg0OHY0OGgtNDh6IiBmaWxsPSJub25lIi8+PC9zdmc+"); 36 | display: block; 37 | background-size: 2.5rem; 38 | background-repeat: no-repeat; 39 | opacity: 0.1; 40 | } 41 | 42 | &::before { 43 | top: 0.75rem; 44 | left: 0; 45 | transform: rotate(180deg); 46 | } 47 | 48 | &::after { 49 | bottom: 0.75rem; 50 | right: 0; 51 | } 52 | 53 | p { 54 | &:last-child { 55 | margin-bottom: 0; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogifier.admin", 3 | "Author": "https://github.com/farzindev", 4 | "version": "1.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "gulp", 8 | "build": "gulp build", 9 | "build:Debug": "gulp build", 10 | "build:Release": "gulp release build" 11 | }, 12 | "dependencies": { 13 | "bootstrap": "~5.1.0", 14 | "autosize": "^6.0.1", 15 | "chart.js": "~2.9.4", 16 | "easymde": "^2.18.0", 17 | "highlight.js": "~10.7.2" 18 | }, 19 | "devDependencies": { 20 | "@babel/preset-env": "^7.22.9", 21 | "@rollup/plugin-babel": "^6.0.3", 22 | "@rollup/plugin-commonjs": "^25.0.0", 23 | "@rollup/plugin-node-resolve": "^15.1.0", 24 | "@rollup/plugin-terser": "^0.4.3", 25 | "@rollup/stream": "^3.0.0", 26 | "autoprefixer": "^10.4.14", 27 | "cssnano": "^6.0.1", 28 | "del": "^7.0.0", 29 | "gulp": "^4.0.2", 30 | "gulp-notify": "^4.0.0", 31 | "gulp-plumber": "^1.2.1", 32 | "gulp-postcss": "^9.0.1", 33 | "gulp-rename": "^2.0.0", 34 | "gulp-sass": "^5.1.0", 35 | "gulp-size": "^4.0.1", 36 | "gulp-sourcemaps": "^3.0.0", 37 | "gulp-svg-sprite": "^2.0.3", 38 | "gulp-uglify": "^3.0.2", 39 | "rollup": "^3.23.1", 40 | "rtlcss": "^3.3.0", 41 | "sass": "~1.32.12", 42 | "vinyl-buffer": "^1.0.1", 43 | "vinyl-source-stream": "^2.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/pages/account/_account.scss: -------------------------------------------------------------------------------- 1 | .account { 2 | width: 22rem; 3 | max-width: 100%; 4 | padding: 4rem 1.5rem; 5 | margin-right: auto; 6 | margin-left: auto; 7 | 8 | @media screen and (min-width: 992px) { 9 | transform: translateY(-8rem); 10 | margin: auto; 11 | } 12 | 13 | // header 14 | &-header { 15 | margin-bottom: 2rem; 16 | } 17 | 18 | &-title { 19 | font-size: 1.25rem; 20 | font-weight: 400; 21 | 22 | &:last-of-type { 23 | margin: 0; 24 | } 25 | } 26 | 27 | &-desc { 28 | font-size: 0.875rem; 29 | } 30 | 31 | // message 32 | &-message { 33 | margin-bottom: 2rem; 34 | font-size: 0.875rem; 35 | padding: 0.5rem 1rem; 36 | border-radius: 0.25rem; 37 | 38 | &.-error { 39 | background-color: $danger; 40 | color: #fff; 41 | } 42 | 43 | &.-warning { 44 | background-color: $warning; 45 | color: #fff; 46 | } 47 | &.-success { 48 | background-color: $success; 49 | color: #fff; 50 | } 51 | } 52 | 53 | // logo 54 | &-logo { 55 | display: block; 56 | width: 2.5rem; 57 | height: 2.5rem; 58 | margin: 0 auto 5rem; 59 | } 60 | 61 | // back 62 | &-back { 63 | position: absolute; 64 | top: 2rem; 65 | left: 2rem; 66 | width: 3rem; 67 | height: 3rem; 68 | text-align: center; 69 | line-height: 3rem; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Blogifier/Identity/UserProvider.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Data; 3 | using Blogifier.Shared; 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Identity; 10 | 11 | public class UserProvider(IMapper mapper, AppDbContext dbContext) 12 | { 13 | private readonly IMapper _mapper = mapper; 14 | private readonly AppDbContext _dbContext = dbContext; 15 | 16 | public async Task FindAsync(int id) 17 | { 18 | var query = _dbContext.Users 19 | .AsNoTracking() 20 | .Where(m => m.Id == id); 21 | return await query.FirstAsync(); 22 | } 23 | 24 | public async Task FirstByIdAsync(int id) 25 | { 26 | var query = _dbContext.Users 27 | .AsNoTracking() 28 | .Where(m => m.Id == id); 29 | return await _mapper.ProjectTo(query).FirstAsync(); 30 | } 31 | 32 | public async Task> GetAsync() 33 | { 34 | var query = _dbContext.Users 35 | .AsNoTracking(); 36 | return await _mapper.ProjectTo(query).ToListAsync(); 37 | } 38 | 39 | public async Task GetAsync(int id) 40 | { 41 | var query = _dbContext.Users 42 | .AsNoTracking() 43 | .Where(m => m.Id == id); 44 | return await _mapper.ProjectTo(query).FirstOrDefaultAsync(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Blogifier/Identity/IdentityExtensions.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Data; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Blogifier.Identity; 6 | 7 | public static class IdentityExtensions 8 | { 9 | public static IServiceCollection AddIdentity(this IServiceCollection services) 10 | { 11 | services.AddScoped(); 12 | services.AddIdentityCore(options => 13 | { 14 | options.User.RequireUniqueEmail = true; 15 | options.Password.RequireUppercase = false; 16 | options.Password.RequireNonAlphanumeric = false; 17 | options.ClaimsIdentity.UserIdClaimType = BlogifierClaimTypes.UserId; 18 | options.ClaimsIdentity.UserNameClaimType = BlogifierClaimTypes.UserName; 19 | options.ClaimsIdentity.EmailClaimType = BlogifierClaimTypes.Email; 20 | options.ClaimsIdentity.SecurityStampClaimType = BlogifierClaimTypes.SecurityStamp; 21 | }).AddUserManager() 22 | .AddSignInManager() 23 | .AddEntityFrameworkStores() 24 | .AddDefaultTokenProviders() 25 | .AddClaimsPrincipalFactory(); 26 | services.ConfigureApplicationCookie(options => 27 | { 28 | options.AccessDeniedPath = "/account/accessdenied"; 29 | options.LoginPath = "/account/login"; 30 | }); 31 | return services; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sections": [ 3 | { 4 | "Label": "Social Buttons", 5 | "Fields": [ 6 | { 7 | "Id": "enabled", 8 | "Label": "Enabled", 9 | "Type": "checkbox", 10 | "Value": "False", 11 | "Options": null 12 | }, 13 | { 14 | "Id": "facebook", 15 | "Label": "Facebook", 16 | "Type": "text", 17 | "Value": "blogifierdotnet", 18 | "Options": null 19 | }, 20 | { 21 | "Id": "twitter", 22 | "Label": "Twitter", 23 | "Type": "text", 24 | "Value": "blogifierdotnet", 25 | "Options": null 26 | } 27 | ] 28 | }, 29 | { 30 | "Label": "Select Dropdown Sample", 31 | "Fields": [ 32 | { 33 | "Id": "dd-sample", 34 | "Label": "Select Sample", 35 | "Type": "select", 36 | "Value": "two", 37 | "Options": [ 38 | "one", 39 | "two", 40 | "three" 41 | ] 42 | } 43 | ] 44 | }, 45 | { 46 | "Label": "Section Three", 47 | "Fields": [ 48 | { 49 | "Id": "txt-area", 50 | "Label": "Text Area Sample", 51 | "Type": "textarea", 52 | "Value": "Some test text here", 53 | "Options": null 54 | } 55 | ] 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/pages/settings/_settings.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | &-cover { 3 | height: 8rem; 4 | width: 100%; 5 | background: #fff; 6 | border-radius: 0.25rem; 7 | padding: 0; 8 | border: 0; 9 | overflow: hidden; 10 | position: relative; 11 | 12 | &::after { 13 | content: ""; 14 | position: absolute; 15 | z-index: 5; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | background-color: rgba(#fff, 0.5); 21 | background-position: center; 22 | background-size: 2rem; 23 | background-repeat: no-repeat; 24 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-paperclip' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 3a2.5 2.5 0 0 1 5 0v9a1.5 1.5 0 0 1-3 0V5a.5.5 0 0 1 1 0v7a.5.5 0 0 0 1 0V3a1.5 1.5 0 1 0-3 0v9a2.5 2.5 0 0 0 5 0V5a.5.5 0 0 1 1 0v7a3.5 3.5 0 1 1-7 0V3z'/%3E%3C/svg%3E"); 25 | opacity: 0; 26 | visibility: hidden; 27 | transition: opacity ease-in-out 0.15s; 28 | } 29 | 30 | &-img { 31 | border-radius: 0.25rem; 32 | width: 100%; 33 | height: 100%; 34 | object-fit: cover; 35 | object-position: center; 36 | } 37 | 38 | &:focus, 39 | &:hover { 40 | outline: none; 41 | 42 | &::after { 43 | opacity: 1; 44 | visibility: visible; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Blogifier/Caches/CacheExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.DataProtection; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using StackExchange.Redis; 6 | using System.IO; 7 | 8 | namespace Blogifier.Caches; 9 | 10 | public static class CacheExtensions 11 | { 12 | public static IServiceCollection AddCache(this IServiceCollection services, IWebHostEnvironment environment, IConfiguration configuration) 13 | { 14 | var redisConnectionString = configuration.GetSection("Blogifier:Redis").Value; 15 | if (string.IsNullOrEmpty(redisConnectionString)) 16 | { 17 | services.AddDistributedMemoryCache(); 18 | var dataProtectionPath = Path.Combine(environment.ContentRootPath, "App_Data", "DataProtection-Keys"); 19 | var dataProtectionDirectory = new DirectoryInfo(dataProtectionPath); 20 | services.AddDataProtection().PersistKeysToFileSystem(dataProtectionDirectory); 21 | } 22 | else 23 | { 24 | services.AddStackExchangeRedisCache(options => 25 | { 26 | options.Configuration = redisConnectionString; 27 | options.InstanceName = "blogifier:"; 28 | }); 29 | var redisConnectionMultiplexer = ConnectionMultiplexer.Connect(redisConnectionString); 30 | services.AddDataProtection().PersistKeysToStackExchangeRedis(redisConnectionMultiplexer, "DataProtection-Keys"); 31 | } 32 | return services; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/login.cshtml: -------------------------------------------------------------------------------- 1 | @model AccountLoginModel 2 | @{ 3 | Layout = "layouts/_account.cshtml"; 4 | var redirectUri = Url.Content("~/account/login"); 5 | } 6 | @inject IStringLocalizer _localizer 7 | 8 | @section HeaderScript{ 9 | @_localizer["login"] 10 | } 11 | 12 | @if (Model.ShowError) 13 | { 14 | 15 | } 16 |
17 | 18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 | @_localizer["register"] 30 |
31 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/PageController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Blogs; 3 | using Blogifier.Posts; 4 | using Blogifier.Shared; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Controllers; 10 | 11 | [Route("page")] 12 | public class PageController( 13 | ILogger logger, 14 | IMapper mapper, 15 | MainMamager mainMamager, 16 | PostManager postManager) : Controller 17 | { 18 | protected readonly ILogger _logger = logger; 19 | protected readonly IMapper _mapper = mapper; 20 | protected readonly MainMamager _mainMamager = mainMamager; 21 | protected readonly PostManager _postManager = postManager; 22 | 23 | [HttpGet("{slug}")] 24 | public async Task GetAsync([FromRoute] string slug) 25 | { 26 | var main = await _mainMamager.GetAsync(); 27 | var postSlug = await _postManager.GetToHtmlAsync(slug); 28 | if (postSlug.Post.State == PostState.Draft) 29 | { 30 | if (User.Identity == null || User.FirstUserId() != postSlug.Post.User.Id) 31 | return Redirect("~/404"); 32 | } 33 | else if (postSlug.Post.PostType == PostType.Page) 34 | { 35 | return Redirect($"~/page/{postSlug.Post.Slug}"); 36 | } 37 | var categoriesUrl = Url.Content("~/category"); 38 | var model = new PostModel(postSlug, categoriesUrl, main); 39 | return View($"~/Views/Themes/{main.Theme}/post.cshtml", model); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/PostController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Blogs; 3 | using Blogifier.Posts; 4 | using Blogifier.Shared; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Controllers; 10 | 11 | [Route("post")] 12 | public class PostController( 13 | ILogger logger, 14 | IMapper mapper, 15 | MainMamager mainMamager, 16 | PostManager postManager) : Controller 17 | { 18 | protected readonly ILogger _logger = logger; 19 | protected readonly IMapper _mapper = mapper; 20 | protected readonly MainMamager _mainMamager = mainMamager; 21 | protected readonly PostManager _postManager = postManager; 22 | 23 | [HttpGet("{slug}")] 24 | public async Task GetAsync([FromRoute] string slug) 25 | { 26 | var main = await _mainMamager.GetAsync(); 27 | var postSlug = await _postManager.GetToHtmlAsync(slug); 28 | if (postSlug.Post.State == PostState.Draft) 29 | { 30 | if (User.Identity == null || User.FirstUserId() != postSlug.Post.User.Id) 31 | return Redirect("~/404"); 32 | } 33 | else if (postSlug.Post.PostType == PostType.Page) 34 | { 35 | return Redirect($"~/page/{postSlug.Post.Slug}"); 36 | } 37 | var categoriesUrl = Url.Content("~/category"); 38 | var model = new PostModel(postSlug, categoriesUrl, main); 39 | return View($"~/Views/Themes/{main.Theme}/post.cshtml", model); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Interop/CommonJsInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Blogifier.Admin.Interop; 7 | 8 | public class CommonJsInterop(IJSRuntime jsRuntime) : IAsyncDisposable 9 | { 10 | private readonly Lazy> moduleTask = new(() => 11 | jsRuntime.InvokeAsync("import", "./admin/js/common.js").AsTask()); 12 | 13 | public async ValueTask SetTooltipAsync() 14 | { 15 | var module = await moduleTask.Value; 16 | await module.InvokeVoidAsync("setTooltip"); 17 | } 18 | 19 | public async ValueTask SetTitleAsync(string content) 20 | { 21 | var module = await moduleTask.Value; 22 | await module.InvokeVoidAsync("setTitle", content); 23 | } 24 | 25 | public async ValueTask TriggerClickAsync(ElementReference? element) 26 | { 27 | var module = await moduleTask.Value; 28 | await module.InvokeVoidAsync("triggerClick", element); 29 | } 30 | 31 | public async ValueTask GetInputFileBlobInfoAsync(ElementReference? inputUpload) 32 | { 33 | var module = await moduleTask.Value; 34 | return await module.InvokeAsync("getInputFileBlobInfo", inputUpload); 35 | } 36 | 37 | public async ValueTask DisposeAsync() 38 | { 39 | if (moduleTask.IsValueCreated) 40 | { 41 | var module = await moduleTask.Value; 42 | await module.DisposeAsync(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Shared/BlogsLayout.razor: -------------------------------------------------------------------------------- 1 | @layout MainLayout 2 | @inherits LayoutComponentBase 3 | @inject NavigationManager _navigationManager 4 | @inject IJSRuntime _jsRuntime 5 | @inject IStringLocalizer _localizer 6 | 7 |
8 |
9 | 27 |
28 | @Body 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Program.cs: -------------------------------------------------------------------------------- 1 | using Blogifier; 2 | using Blogifier.Admin; 3 | using Blogifier.Admin.Interop; 4 | using Blogifier.Admin.Services; 5 | using Blogifier.Identity; 6 | using Microsoft.AspNetCore.Components.Authorization; 7 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Sotsera.Blazor.Toaster.Core.Models; 10 | using System; 11 | using System.Net.Http; 12 | 13 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 14 | builder.RootComponents.Add("#app"); 15 | builder.Services.AddLocalization(); 16 | builder.Services.AddOptions(); 17 | 18 | builder.Services.AddAuthorizationCore(options => 19 | options.AddPolicy(BlogifierSharedConstant.PolicyAdminName, policy => 20 | policy.RequireClaim(BlogifierClaimTypes.Type, BlogifierSharedConstant.PolicyAdminValue))); 21 | 22 | builder.Services.AddScoped(sp => new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }) 23 | { 24 | BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) 25 | }); 26 | builder.Services.AddScoped(); 27 | builder.Services.AddToaster(config => 28 | { 29 | config.PositionClass = Defaults.Classes.Position.BottomRight; 30 | config.PreventDuplicates = true; 31 | config.NewestOnTop = false; 32 | }); 33 | builder.Services.AddScoped(); 34 | builder.Services.AddScoped(); 35 | builder.Services.AddScoped(); 36 | await builder.Build().RunAsync(); 37 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/page/_account.scss: -------------------------------------------------------------------------------- 1 | .blogifier { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .account { 8 | width: 22rem; 9 | max-width: 100%; 10 | padding: 1rem 1.5rem; 11 | margin-right: auto; 12 | margin-left: auto; 13 | 14 | @media screen and (min-width: 992px) { 15 | transform: translateY(-3rem); 16 | margin: auto; 17 | } 18 | 19 | // header 20 | &-header { 21 | margin-bottom: 2rem; 22 | } 23 | 24 | &-title { 25 | font-size: 1.25rem; 26 | font-weight: 400; 27 | 28 | &:last-of-type { 29 | margin: 0; 30 | } 31 | } 32 | 33 | &-desc { 34 | font-size: 0.875rem; 35 | } 36 | 37 | // message 38 | &-message { 39 | margin-bottom: 2rem; 40 | font-size: 0.875rem; 41 | padding: 0.5rem 1rem; 42 | border-radius: 0.25rem; 43 | 44 | &.-error { 45 | background-color: $danger; 46 | color: #fff; 47 | } 48 | 49 | &.-warning { 50 | background-color: $warning; 51 | color: #fff; 52 | } 53 | &.-success { 54 | background-color: $success; 55 | color: #fff; 56 | } 57 | } 58 | 59 | // logo 60 | &-logo { 61 | display: block; 62 | width: 2.5rem; 63 | height: 2.5rem; 64 | margin: 0 auto 5rem; 65 | } 66 | 67 | // back 68 | &-back { 69 | position: absolute; 70 | top: 2rem; 71 | left: 2rem; 72 | width: 3rem; 73 | height: 3rem; 74 | text-align: center; 75 | line-height: 3rem; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Blogifier/Newsletters/NewsletterProvider.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Blogifier.Data; 3 | using Blogifier.Shared; 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Blogifier.Newsletters; 10 | 11 | public class NewsletterProvider(AppDbContext dbContext, IMapper mapper) : AppProvider(dbContext) 12 | { 13 | private readonly IMapper _mapper = mapper; 14 | 15 | public async Task> GetItemsAsync() 16 | { 17 | var query = _dbContext.Newsletters 18 | .AsNoTracking() 19 | .Include(n => n.Post) 20 | .OrderByDescending(n => n.CreatedAt); 21 | return await _mapper.ProjectTo(query).ToListAsync(); 22 | } 23 | 24 | public async Task FirstOrDefaultByPostIdAsync(int postId) 25 | { 26 | var query = _dbContext.Newsletters 27 | .Where(m => m.PostId == postId); 28 | return await _mapper.ProjectTo(query).FirstOrDefaultAsync(); 29 | } 30 | 31 | public async Task AddAsync(int postId, bool success) 32 | { 33 | var entry = new Newsletter 34 | { 35 | PostId = postId, 36 | Success = success, 37 | }; 38 | await AddAsync(entry); 39 | } 40 | 41 | public async Task UpdateAsync(int id, bool success) 42 | { 43 | await _dbContext.Newsletters 44 | .Where(m => m.Id == id) 45 | .ExecuteUpdateAsync(setters => 46 | setters.SetProperty(b => b.Success, success)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build release test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - name: Checkout 14 | uses: actions/checkout@v2.3.1 15 | 16 | - name: Get npm cache directory 17 | id: npm-cache-dir 18 | shell: bash 19 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 20 | 21 | - uses: actions/cache@v3 22 | id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true' 23 | with: 24 | path: ${{ steps.npm-cache-dir.outputs.dir }} 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node 28 | 29 | - uses: actions/cache@v3 30 | with: 31 | path: ~/.nuget/packages 32 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-nuget- 35 | 36 | - name: Setup .NET 37 | uses: actions/setup-dotnet@v3 38 | with: 39 | dotnet-version: '7.0.x' 40 | 41 | - name: Publish Blogifier 42 | run: dotnet publish -c Release /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist 43 | 44 | #- name: Add .nojekyll file 45 | # run: touch dist/.nojekyll 46 | 47 | #- name: Deploy 48 | # uses: JamesIves/github-pages-deploy-action@4.1.4 49 | # with: 50 | # branch: demo 51 | # folder: release 52 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/BlogAuthStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Identity; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using Microsoft.Extensions.Logging; 4 | using System.Net.Http; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace Blogifier.Admin; 9 | 10 | public class BlogAuthStateProvider(ILogger logger, 11 | HttpClient httpClient) : AuthenticationStateProvider 12 | { 13 | private readonly ILogger _logger = logger; 14 | protected readonly HttpClient _httpClient = httpClient; 15 | protected AuthenticationState? _state; 16 | 17 | public override async Task GetAuthenticationStateAsync() 18 | { 19 | if (_state == null) 20 | { 21 | var response = await _httpClient.GetAsync("/api/token/userinfo"); 22 | BlogifierClaims? claims = null; 23 | if (response.IsSuccessStatusCode) 24 | { 25 | var stream = await response.Content.ReadAsStreamAsync(); 26 | if (stream.Length > 0) 27 | { 28 | claims = JsonSerializer.Deserialize(stream, BlogifierSharedConstant.DefaultJsonSerializerOptions)!; 29 | _logger.LogInformation("claims success userName:{UserName}", claims.UserName); 30 | } 31 | } 32 | else 33 | { 34 | _logger.LogError("claims http error StatusCode:{StatusCode}", response.StatusCode); 35 | } 36 | var principal = BlogifierClaims.Generate(claims); 37 | _state = new AuthenticationState(principal); 38 | } 39 | return _state; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Blogifier.Shared/Helper/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Blogifier.Helper; 4 | 5 | public static partial class StringHelper 6 | { 7 | [GeneratedRegex("]*>[\\s\\S]*?", RegexOptions.Compiled)] 8 | public static partial Regex HtmlScriptGeneratedRegex(); 9 | 10 | [GeneratedRegex("]*>[\\s\\S]*?>", RegexOptions.Compiled)] 11 | public static partial Regex HtmlImgGeneratedRegex(); 12 | 13 | [GeneratedRegex("", RegexOptions.Compiled)] 14 | public static partial Regex HtmlImgSrcGeneratedRegex(); 15 | 16 | [GeneratedRegex("]*?src\\s*=\\s*[\"']?([^'\" >]+?)[ '\"][^>]*?>", RegexOptions.Compiled)] 17 | public static partial Regex HtmlImgTagsGeneratedRegex(); 18 | 19 | [GeneratedRegex("(?i)]*?>(?.*?)", RegexOptions.Compiled)] 20 | public static partial Regex HtmlFileGeneratedRegex(); 21 | 22 | [GeneratedRegex("!\\[[^\\]]*\\]\\((blob:[^)]+)\\)", RegexOptions.Compiled)] 23 | public static partial Regex MarkdownImgBlobGeneratedRegex(); 24 | 25 | [GeneratedRegex(@"!\[(?[^\]]+)\]\(data:image\/(?.+);base64,(?.*)\)", RegexOptions.Compiled)] 26 | public static partial Regex MarkdownDataImageBase64BlobGeneratedRegex(); 27 | 28 | [GeneratedRegex(@"blob:(https?://[^/]+/\S+)", RegexOptions.Compiled)] 29 | public static partial Regex BlobUrlGeneratedRegex(); 30 | 31 | [GeneratedRegex(@"data:image\/(?.+);base64,(?.*)", RegexOptions.Compiled)] 32 | public static partial Regex DataImageBase64GeneratedRegex(); 33 | } 34 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/components/_newsletter.scss: -------------------------------------------------------------------------------- 1 | .newsletter { 2 | background-color: #000; 3 | color: #aaa; 4 | border-radius: $radius-var; 5 | margin-top: 4rem; 6 | position: relative; 7 | 8 | &-body { 9 | padding: 3rem; 10 | 11 | @media screen and (min-width: 991px) { 12 | padding: 4rem; 13 | } 14 | } 15 | 16 | &-title { 17 | font-size: 1.75rem; 18 | font-weight: 600; 19 | color: #fff; 20 | } 21 | 22 | &-desc { 23 | margin: 0; 24 | font-size: 1.125rem; 25 | font-weight: 300; 26 | } 27 | 28 | &-form { 29 | border: 2px solid #222; 30 | border-radius: $radius-var; 31 | } 32 | 33 | &-input { 34 | background: none; 35 | border: 0; 36 | flex-grow: 1; 37 | width: 100%; 38 | padding: 0.5rem 1rem; 39 | color: #fff; 40 | outline: none !important; 41 | } 42 | &-btn { 43 | background: none; 44 | border: 0; 45 | padding: 0.5rem 1rem; 46 | color: #fff; 47 | display: block; 48 | border-left: 1px solid #222; 49 | &:focus, 50 | &:hover { 51 | outline: none; 52 | background: #222; 53 | } 54 | } 55 | 56 | &-msg { 57 | padding: 2rem; 58 | text-align: center; 59 | background-color: #000; 60 | border-radius: $radius-var; 61 | position: absolute; 62 | top: 0; 63 | left: 0; 64 | right: 0; 65 | bottom: 0; 66 | display: flex; 67 | flex-direction: column; 68 | margin: 0; 69 | color: #fff; 70 | cursor: pointer; 71 | 72 | .spinner-border { 73 | color: #fff; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Interop/EditorJsInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Blogifier.Admin.Interop; 7 | 8 | public class EditorJsInterop(IJSRuntime jsRuntime) : IAsyncDisposable 9 | { 10 | private readonly Lazy> moduleTask = new(() => 11 | jsRuntime.InvokeAsync("import", "./admin/js/editor.js").AsTask()); 12 | 13 | public async ValueTask LoadEditorAsync(ElementReference? textarea, ElementReference? imageUpload, string toolbar = "fullToolbar") 14 | { 15 | var module = await moduleTask.Value; 16 | await module.InvokeVoidAsync("loadEditor", toolbar, textarea, imageUpload); 17 | } 18 | 19 | public async ValueTask SetEditorValueAsync(string content) 20 | { 21 | var module = await moduleTask.Value; 22 | await module.InvokeVoidAsync("setEditorValue", content); 23 | } 24 | 25 | public async ValueTask GetEditorValueAsync() 26 | { 27 | var module = await moduleTask.Value; 28 | var content = await module.InvokeAsync("getEditorValue"); 29 | return content; 30 | } 31 | 32 | public async ValueTask WriteFrontFileAsync(ElementReference? imageUpload) 33 | { 34 | var module = await moduleTask.Value; 35 | await module.InvokeVoidAsync("writeFrontFile", imageUpload); 36 | } 37 | 38 | public async ValueTask DisposeAsync() 39 | { 40 | if (moduleTask.IsValueCreated) 41 | { 42 | var module = await moduleTask.Value; 43 | await module.DisposeAsync(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Blogifier/Controllers/SitemapController.cs: -------------------------------------------------------------------------------- 1 | using Blogifier.Posts; 2 | using Blogifier.Shared; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Xml.Linq; 8 | 9 | namespace Blogifier.Controllers; 10 | 11 | public class SitemapController(PostProvider postProvider) : ControllerBase 12 | { 13 | private readonly PostProvider _postProvider = postProvider; 14 | 15 | [Route("sitemap")] 16 | [Produces("text/xml")] 17 | public async Task Sitemap() 18 | { 19 | var sitemapNamespace = XNamespace.Get("http://www.sitemaps.org/schemas/sitemap/0.9"); 20 | var posts = await _postProvider.GetAsync(); 21 | var doc = new XDocument( 22 | new XDeclaration("1.0", "utf-8", null), 23 | new XElement(sitemapNamespace + "urlset", 24 | from post in posts 25 | select new XElement(sitemapNamespace + "url", 26 | new XElement(sitemapNamespace + "loc", GetPostUrl(post)), 27 | new XElement(sitemapNamespace + "lastmod", GetPostDate(post)), 28 | new XElement(sitemapNamespace + "changefreq", "monthly") 29 | ) 30 | ) 31 | ); 32 | return Content(doc.Declaration + Environment.NewLine + doc, "text/xml"); 33 | } 34 | 35 | private string GetPostUrl(PostDto post) 36 | { 37 | string webRoot = Url.Content("~/"); 38 | var sitemapBaseUri = $"{Request.Scheme}://{Request.Host}{webRoot}"; 39 | return $"{sitemapBaseUri}posts/{post.Slug}"; 40 | } 41 | 42 | private string GetPostDate(PostDto post) 43 | { 44 | return post.PublishedAt!.Value.ToString("yyyy-MM-ddTHH:mm:sszzz"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/svg/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_account.cshtml: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer _localizer 2 | @model AccountModel 3 | @{ 4 | var redirectUri = Model.RedirectUri ?? "/"; 5 | } 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @await RenderSectionAsync("HeaderScript", required: false) 17 | 18 | 19 | 20 |
21 | 26 | 34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/base.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 14 | font-size: 1rem; 15 | font-weight: 400; 16 | line-height: 1.5; 17 | color: #000; 18 | background-color: #f8f9fa; 19 | display: flex; 20 | height: 100%; 21 | } 22 | 23 | p { 24 | margin: 0; 25 | } 26 | 27 | .container { 28 | width: 32rem; 29 | max-width: 100%; 30 | padding: 2rem; 31 | margin: auto; 32 | } 33 | 34 | .title { 35 | margin: 0 0 1rem; 36 | font-weight: 500; 37 | font-size: 2rem; 38 | } 39 | 40 | .message { 41 | margin-bottom: 2rem; 42 | font-size: 1.25rem; 43 | color: #727272; 44 | } 45 | 46 | .button { 47 | padding: 0 1.5rem 0 .75rem; 48 | border-radius: .3rem; 49 | display: inline-block; 50 | background-color: #009e66; 51 | color: #fff; 52 | text-decoration: none; 53 | font-size: .875rem; 54 | font-weight: 500; 55 | line-height: 2.5rem; 56 | } 57 | 58 | .button:hover { 59 | background-color: #009460; 60 | } 61 | 62 | .button .icon { 63 | vertical-align: middle; 64 | } 65 | 66 | @media screen and (min-width:480px) { 67 | .title { 68 | font-size: 2.5rem; 69 | font-weight: 700; 70 | } 71 | 72 | .message { 73 | font-size: 1.5rem; 74 | font-weight: 300; 75 | } 76 | 77 | .message .br { 78 | display: block; 79 | } 80 | 81 | .button { 82 | line-height: 2.75rem; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_main.cshtml: -------------------------------------------------------------------------------- 1 | @model MainModel 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | @Model.Main.Title 10 | 11 | 12 | 13 | 14 | 32 | 33 | @Html.Raw(Model.Main.HeaderScript) 34 | 35 | 36 | 37 | @RenderBody() 38 | 39 | 40 | 41 | @RenderSection("FooterScript", required: false) 42 | @Html.Raw(Model.Main.FooterScript) 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/wwwroot/admin/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Components/CategoriesComponent.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer _localizer 2 | 3 | @code { 4 | [Parameter] public List Categories { get; set; } = default!; 5 | 6 | protected string Tag { get; set; } = default!; 7 | 8 | protected override void OnInitialized() 9 | { 10 | Tag = string.Empty; 11 | } 12 | 13 | protected void KeyPressed(KeyboardEventArgs eventArgs) 14 | { 15 | if (eventArgs.Code == "Enter") 16 | { 17 | Categories.Add(new CategoryDto { Content = Tag }); 18 | Tag = string.Empty; 19 | } 20 | } 21 | 22 | protected void Remove(string tag) 23 | { 24 | Categories!.Remove(Categories.Where(c => c.Content == tag).First()); 25 | } 26 | } 27 | 38 |
39 | @foreach (var item in Categories) 40 | { 41 |
42 | 43 | 45 | 46 | 47 |
48 | } 49 | 50 |
51 | 52 | -------------------------------------------------------------------------------- /src/Blogifier.Themes.Standard/assets/scss/components/_highlight.scss: -------------------------------------------------------------------------------- 1 | pre > code { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 2rem; 5 | color: $hljs-foreground; 6 | background: $hljs-background; 7 | margin: 0; 8 | font-family: var(--bs-font-monospace); 9 | font-size: 0.875rem; 10 | text-align: left; 11 | white-space: pre; 12 | word-spacing: normal; 13 | word-break: normal; 14 | word-wrap: normal; 15 | line-height: 1.4; 16 | -moz-tab-size: 2; 17 | -o-tab-size: 2; 18 | tab-size: 2; 19 | -webkit-hyphens: none; 20 | -moz-hyphens: none; 21 | -ms-hyphens: none; 22 | hyphens: none; 23 | } 24 | 25 | .hljs-comment { 26 | color: $hljs-comments; 27 | } 28 | 29 | .hljs-keyword, 30 | .hljs-selector-tag, 31 | .hljs-meta-keyword, 32 | .hljs-doctag, 33 | .hljs-section, 34 | .hljs-selector-class, 35 | .hljs-meta, 36 | .hljs-selector-pseudo, 37 | .hljs-attr { 38 | color: $hljs-blue; 39 | } 40 | 41 | .hljs-attribute { 42 | color: #803378; 43 | } 44 | .hljs-name { 45 | color: #168174; 46 | } 47 | .hljs-type, 48 | .hljs-number, 49 | .hljs-selector-id, 50 | .hljs-quote, 51 | .hljs-template-tag, 52 | // .hljs-built_in, 53 | .hljs-literal { 54 | color: #168174; 55 | } 56 | 57 | .hljs-title, 58 | .hljs-string, 59 | .hljs-regexp, 60 | .hljs-symbol, 61 | .hljs-variable, 62 | .hljs-template-variable, 63 | .hljs-link, 64 | .hljs-selector-attr, 65 | .hljs-meta-string { 66 | color: $hljs-red; 67 | } 68 | 69 | .hljs-bullet, 70 | .hljs-code { 71 | color: #cccccc; 72 | } 73 | 74 | .hljs-deletion { 75 | color: #de7176; 76 | } 77 | 78 | .hljs-addition { 79 | color: #76c490; 80 | } 81 | 82 | .hljs-emphasis { 83 | font-style: italic; 84 | } 85 | 86 | .hljs-strong { 87 | font-weight: bold; 88 | } 89 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/assets/scss/include/_highlight.scss: -------------------------------------------------------------------------------- 1 | pre > code { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 2rem; 5 | color: $hljs-foreground; 6 | background: $hljs-background; 7 | margin: 0; 8 | font-family: var(--bs-font-monospace); 9 | font-size: 0.75rem; 10 | text-align: left; 11 | white-space: pre; 12 | word-spacing: normal; 13 | word-break: normal; 14 | word-wrap: normal; 15 | line-height: 1.4; 16 | -moz-tab-size: 2; 17 | -o-tab-size: 2; 18 | tab-size: 2; 19 | -webkit-hyphens: none; 20 | -moz-hyphens: none; 21 | -ms-hyphens: none; 22 | hyphens: none; 23 | } 24 | 25 | pre code { 26 | } 27 | 28 | .hljs-comment { 29 | color: $hljs-comments; 30 | } 31 | 32 | .hljs-keyword, 33 | .hljs-selector-tag, 34 | .hljs-meta-keyword, 35 | .hljs-doctag, 36 | .hljs-section, 37 | .hljs-selector-class, 38 | .hljs-meta, 39 | .hljs-selector-pseudo, 40 | .hljs-attr { 41 | color: $hljs-blue; 42 | } 43 | 44 | .hljs-attribute { 45 | color: #803378; 46 | } 47 | .hljs-name { 48 | color: #168174; 49 | } 50 | .hljs-type, 51 | .hljs-number, 52 | .hljs-selector-id, 53 | .hljs-quote, 54 | .hljs-template-tag, 55 | // .hljs-built_in, 56 | .hljs-literal { 57 | color: #168174; 58 | } 59 | 60 | .hljs-title, 61 | .hljs-string, 62 | .hljs-regexp, 63 | .hljs-symbol, 64 | .hljs-variable, 65 | .hljs-template-variable, 66 | .hljs-link, 67 | .hljs-selector-attr, 68 | .hljs-meta-string { 69 | color: $hljs-red; 70 | } 71 | 72 | .hljs-bullet, 73 | .hljs-code { 74 | color: #cccccc; 75 | } 76 | 77 | .hljs-deletion { 78 | color: #de7176; 79 | } 80 | 81 | .hljs-addition { 82 | color: #76c490; 83 | } 84 | 85 | .hljs-emphasis { 86 | font-style: italic; 87 | } 88 | 89 | .hljs-strong { 90 | font-weight: bold; 91 | } 92 | -------------------------------------------------------------------------------- /src/Blogifier.Admin/Pages/Settings/ScriptsView.razor: -------------------------------------------------------------------------------- 1 | @layout SettingsLayout 2 | @page "/admin/settings/scripts/" 3 | @inject HttpClient _http 4 | @inject IToaster _toaster 5 | @inject IStringLocalizer _localizer 6 | 7 | 8 | 9 |

@_localizer["script-settings"]

10 |

@_localizer["include-scripts"]

11 |
12 | @if (Blog != null) 13 | { 14 | 15 |
16 | 17 |