diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..34c3e19a0 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.5", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..9b5784fb0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +**/bin +**/obj +**/node_modules +Dockerfile +build.sh diff --git a/.editorconfig b/.editorconfig index 56f423934..8b2395878 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,202 +1,154 @@ -# editorconfig.org -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -trim_trailing_whitespace = true -insert_final_newline = true -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_property = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_event = false:silent -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -tab_width = 2 -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_code_quality_unused_parameters = all:suggestion -dotnet_style_readonly_field = true:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_allow_multiple_blank_lines_experimental = true:silent -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_predefined_type_for_member_access = true:silent -dotnet_style_predefined_type_for_locals_parameters_members = true:silent - -[*.{cshtml}] -indent_size = 2 - -[*.{razor}] -indent_size = 2 - -[*.{css,scss,js,json,yml}] -indent_size = 2 - -[*.{xml,csproj,config,*proj,targets,props}] -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false - -[*.sln] -indent_style = tab - -[*.cs] -#### 命名样式 #### - -# 命名规则 - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# 符号规范 - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# 命名样式 - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -csharp_using_directive_placement = outside_namespace:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_conditional_delegate_call = true:suggestion -csharp_style_var_for_built_in_types = false:silent -csharp_style_var_when_type_is_apparent = false:silent -csharp_style_var_elsewhere = false:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_prefer_static_local_function = true:suggestion -csharp_style_prefer_readonly_struct = true:suggestion -csharp_space_around_binary_operators = before_and_after -csharp_indent_labels = one_less_than_current -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent -csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent -csharp_style_prefer_switch_expression = true:suggestion -csharp_style_prefer_pattern_matching = true:silent -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion -csharp_style_prefer_extended_property_pattern = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -[*.vb] -#### 命名样式 #### - -# 命名规则 - -dotnet_naming_rule.interface_should_be_以_i_开始.severity = suggestion -dotnet_naming_rule.interface_should_be_以_i_开始.symbols = interface -dotnet_naming_rule.interface_should_be_以_i_开始.style = 以_i_开始 - -dotnet_naming_rule.类型_should_be_帕斯卡拼写法.severity = suggestion -dotnet_naming_rule.类型_should_be_帕斯卡拼写法.symbols = 类型 -dotnet_naming_rule.类型_should_be_帕斯卡拼写法.style = 帕斯卡拼写法 - -dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.severity = suggestion -dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.symbols = 非字段成员 -dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯卡拼写法 - -# 符号规范 - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected -dotnet_naming_symbols.类型.required_modifiers = - -dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method -dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected -dotnet_naming_symbols.非字段成员.required_modifiers = - -# 命名样式 - -dotnet_naming_style.以_i_开始.required_prefix = I -dotnet_naming_style.以_i_开始.required_suffix = -dotnet_naming_style.以_i_开始.word_separator = -dotnet_naming_style.以_i_开始.capitalization = pascal_case - -dotnet_naming_style.帕斯卡拼写法.required_prefix = -dotnet_naming_style.帕斯卡拼写法.required_suffix = -dotnet_naming_style.帕斯卡拼写法.word_separator = -dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case - -dotnet_naming_style.帕斯卡拼写法.required_prefix = -dotnet_naming_style.帕斯卡拼写法.required_suffix = -dotnet_naming_style.帕斯卡拼写法.word_separator = -dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case +# editorconfig.org +root = true + +[*] +charset = utf-8 +# end_of_line = crlf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +tab_width = 2 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_predefined_type_for_locals_parameters_members = true:silent + +[*.{cshtml}] +indent_size = 2 + +[*.{razor}] +indent_size = 2 + +[*.{css,scss,js,json,yml}] +indent_size = 2 + +[*.{xml,csproj,config,*proj,targets,props}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.sln] +indent_style = tab + +[*.cs] +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# 命名样式 + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +csharp_using_directive_placement = outside_namespace:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_space_around_binary_operators = before_and_after +csharp_indent_labels = one_less_than_current +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index f19ea3857..1baadf4bd 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -19,19 +19,20 @@ jobs: key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} restore-keys: | ${{ runner.os }}-nuget- + - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '7.0.x' - name: Publish Blogifier - run: dotnet publish -c Release -o release --nologo + run: dotnet publish -c Release /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist - name: Add .nojekyll file - run: touch release/.nojekyll + run: touch dist/.nojekyll - - name: Deploy - uses: JamesIves/github-pages-deploy-action@4.1.4 - with: - branch: demo - folder: release + #- name: Deploy + # uses: JamesIves/github-pages-deploy-action@4.1.4 + # with: + # branch: demo + # folder: release diff --git a/.gitignore b/.gitignore index 2c454277b..9d8d3b095 100644 --- a/.gitignore +++ b/.gitignore @@ -1,358 +1,360 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -.DS_Store -Blog.db-shm -Blog.db-wal - -App_Data/ - -appsettings.Development.json +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +.DS_Store +Blog.db-shm +Blog.db-wal + +App_Data/ + +appsettings.Development.json + +dist diff --git a/.vscode/launch.json b/.vscode/launch.json index 000b34039..b90bfb380 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch and Debug Blazor WebAssembly App", + "name": "Launch and Debug", "type": "coreclr", "request": "launch", "program": "dotnet", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0db3279e4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/Blogifier.sln b/Blogifier.sln index fa587eb3c..0482bd743 100644 --- a/Blogifier.sln +++ b/Blogifier.sln @@ -30,6 +30,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{D2E9C8BA Dockerfile = Dockerfile EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blogifier.Themes.Standard", "src\Blogifier.Themes.Standard\Blogifier.Themes.Standard.csproj", "{7784DF02-6BCF-40A9-B327-C1F173975714}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "themes", "themes", "{4155E512-4F91-48D3-823A-45B7C71125A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,12 +92,25 @@ Global {27EB38D0-E275-4033-93B6-3ED6E6B7B442}.Release|x64.Build.0 = Release|Any CPU {27EB38D0-E275-4033-93B6-3ED6E6B7B442}.Release|x86.ActiveCfg = Release|Any CPU {27EB38D0-E275-4033-93B6-3ED6E6B7B442}.Release|x86.Build.0 = Release|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Debug|x64.ActiveCfg = Debug|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Debug|x64.Build.0 = Debug|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Debug|x86.ActiveCfg = Debug|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Debug|x86.Build.0 = Debug|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Release|Any CPU.Build.0 = Release|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Release|x64.ActiveCfg = Release|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Release|x64.Build.0 = Release|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Release|x86.ActiveCfg = Release|Any CPU + {7784DF02-6BCF-40A9-B327-C1F173975714}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {27EB38D0-E275-4033-93B6-3ED6E6B7B442} = {8DDBCDEC-BFD8-4737-93BD-5259C3AE9CAE} + {7784DF02-6BCF-40A9-B327-C1F173975714} = {4155E512-4F91-48D3-823A-45B7C71125A0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8AA706E7-D820-4892-9F60-FCEEDC51FF5E} diff --git a/Dockerfile b/Dockerfile index 9302a36e6..9b213ead1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,14 @@ -FROM mcr.microsoft.com/dotnet/aspnet:6.0 as base - +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine as sdk # Copy everything else and build COPY ./ /opt/blogifier WORKDIR /opt/blogifier +RUN ["dotnet","publish", "-c", "Release","/p:RuntimeIdentifier=linux-musl-x64", "./src/Blogifier/Blogifier.csproj","-o","dist" ] -RUN ["dotnet","publish","./src/Blogifier/Blogifier.csproj","-o","./outputs" ] - -FROM mcr.microsoft.com/dotnet/aspnet:6.0 as run -COPY --from=base /opt/blogifier/outputs /opt/blogifier/outputs -WORKDIR /opt/blogifier/outputs -ENTRYPOINT ["dotnet", "Blogifier.dll"] \ No newline at end of file +FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine as run +# TOTO zh-CH +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +RUN apk add --no-cache icu-libs +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +COPY --from=sdk /opt/blogifier/dist /opt/blogifier/ +WORKDIR /opt/blogifier +ENTRYPOINT ["dotnet", "Blogifier.dll"] diff --git a/README.md b/README.md index 893e2b50e..761adafde 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@

Blogifier

- Blogifier is a self-hosted open source publishing platform written in ASP.NET and Blazor WebAssembly. It can be used to quickly and easily set up a lightweight, but fully functional personal or group blog. + Blogifier is a self-hosted open source publishing platform written in ASP.NET and Blazor WebAssembly. + It can be used to quickly and easily set up a lightweight, but fully functional personal or group blog.



@@ -9,7 +10,7 @@ Steps to install compiled application on the server for a self-hosting: -1. .NET Core Runtime (currently 6.0) must be installed on your host server. +1. .NET Core Runtime (currently 7.0) must be installed on your host server. 2. [Download](https://github.com/blogifierdotnet/Blogifier/releases) the latest release. 3. Unzip and copy to your host server.
4. Restart your website. diff --git a/build.cmd b/build.cmd new file mode 100644 index 000000000..a71b5741b --- /dev/null +++ b/build.cmd @@ -0,0 +1,2 @@ +dotnet clean +dotnet build -c Debug /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..b0214ddf2 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +dotnet clean +dotnet build -c Debug /p:RuntimeIdentifier=win-x64 ./src/Blogifier/Blogifier.csproj --output dist diff --git a/docs/02-Database.md b/docs/02-Database.md index 87dbf68d2..87fef7c58 100644 --- a/docs/02-Database.md +++ b/docs/02-Database.md @@ -18,6 +18,10 @@ as Default project and run these commands: ``` Add-Migration Init -o Data\Migrations Update-Database + +# cil +dotnet ef migrations Init -o Data\Migrations +dotnet ef migrations remove ``` First command should re-generate provider specific code migrations and second will diff --git a/publish.cmd b/publish.cmd new file mode 100644 index 000000000..4eec9c5bd --- /dev/null +++ b/publish.cmd @@ -0,0 +1 @@ +dotnet publish -c Release /p:RuntimeIdentifier=win-x64 ./src/Blogifier/Blogifier.csproj -v minimal --output dist diff --git a/publish.sh b/publish.sh new file mode 100644 index 000000000..39fb990e8 --- /dev/null +++ b/publish.sh @@ -0,0 +1,7 @@ +# Local machine +# rm -fr dist +# dotnet publish -c Release /p:RuntimeIdentifier=linux-x64 ./src/Blogifier/Blogifier.csproj --output dist + +# docker +docker build -t dorthl/blogifier:latest . +docker push dorthl/blogifier:latest diff --git a/src/Blogifier.Admin/BlogAuthStateProvider.cs b/src/Blogifier.Admin/BlogAuthStateProvider.cs index a13538361..84a836e59 100644 --- a/src/Blogifier.Admin/BlogAuthStateProvider.cs +++ b/src/Blogifier.Admin/BlogAuthStateProvider.cs @@ -1,34 +1,46 @@ -using Blogifier.Shared; +using Blogifier.Identity; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Logging; using System.Net.Http; -using System.Net.Http.Json; -using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; namespace Blogifier.Admin; public class BlogAuthStateProvider : AuthenticationStateProvider { - private readonly HttpClient _httpClient; + private readonly ILogger _logger; + protected readonly HttpClient _httpClient; + protected AuthenticationState? _state; - public BlogAuthStateProvider(HttpClient httpClient) + public BlogAuthStateProvider(ILogger logger, HttpClient httpClient) { + _logger = logger; _httpClient = httpClient; } public override async Task GetAuthenticationStateAsync() { - var author = await _httpClient.GetFromJsonAsync("api/author/getcurrent"); - if (author != null && author.Email != null) + if (_state == null) { - var claim = new Claim(ClaimTypes.Name, author.Email); - var claimsIdentity = new ClaimsIdentity(new[] { claim }, "serverAuth"); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - return new AuthenticationState(claimsPrincipal); - } - else - { - return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); + var response = await _httpClient.GetAsync("/api/token/userinfo"); + BlogifierClaims? claims = null; + if (response.IsSuccessStatusCode) + { + var stream = await response.Content.ReadAsStreamAsync(); + if (stream.Length > 0) + { + claims = JsonSerializer.Deserialize(stream, BlogifierConstant.DefaultJsonSerializerOptionss)!; + _logger.LogInformation("claims success userName:{UserName}", claims.UserName); + } + } + else + { + _logger.LogError("claims http error StatusCode:{StatusCode}", response.StatusCode); + } + var principal = BlogifierClaims.Generate(claims); + _state = new AuthenticationState(principal); } + return _state; } } diff --git a/src/Blogifier.Admin/BlogStateProvider.cs b/src/Blogifier.Admin/BlogStateProvider.cs deleted file mode 100644 index 1004ee9b7..000000000 --- a/src/Blogifier.Admin/BlogStateProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Blogifier.Shared; -using System; - -namespace Blogifier.Admin; - -public class BlogStateProvider -{ - public PostType PostType { get; set; } = PostType.Post; - - public event Action OnChange = default!; - - public void SetPostType(PostType postType) - { - PostType = postType; - NotifyStateChanged(); - } - - private void NotifyStateChanged() => OnChange?.Invoke(); -} diff --git a/src/Blogifier.Admin/Blogifier.Admin.csproj b/src/Blogifier.Admin/Blogifier.Admin.csproj index 4a6c6eba4..e9e15754a 100644 --- a/src/Blogifier.Admin/Blogifier.Admin.csproj +++ b/src/Blogifier.Admin/Blogifier.Admin.csproj @@ -2,18 +2,86 @@ net7.0 enable + false + assets\ + + + - + + + + + + + + + + + + + $(ClientAssetsDirectory)package-lock.json;$(ClientAssetsDirectory)package.json + $(ClientAssetsDirectory)node_modules\.package-lock.json + $(MSBuildProjectFile);$(ClientAssetsRestoreInputs) + + DispatchToInnerBuilds + + true + $(ClientAssetsDirectory)\dist\ + + + + + + + + + + + + + + + + + + + + + <_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(ClientAssetsBuildOutput)'))/ + + + <_ClientAssetsBuildOutput Include="$(ClientAssetsBuildOutput)**"> + + + + + + + + + + + + + + + + diff --git a/src/Blogifier.Admin/Components/Blog/CategoriesComponent.razor b/src/Blogifier.Admin/Components/Blog/CategoriesComponent.razor deleted file mode 100644 index 34bc65cb1..000000000 --- a/src/Blogifier.Admin/Components/Blog/CategoriesComponent.razor +++ /dev/null @@ -1,64 +0,0 @@ -@inject HttpClient _http -@inject IStringLocalizer _localizer - -
- @if (PostCategories != null) - { - foreach (var item in PostCategories) - { -
- - - - -
- } - } - -
- -@code { - [Parameter] public Post Post { get; set; } - - protected string Tag { get; set; } - public List PostCategories { get; set; } - - protected override async Task OnInitializedAsync() - { - await Load(); - } - - protected async Task Load() - { - Tag = ""; - PostCategories = (await _http.GetFromJsonAsync>($"api/category/{Post.Id}")).ToList(); - } - - protected async Task KeyPressed(KeyboardEventArgs eventArgs) - { - if (eventArgs.Code == "Enter") - { - PostCategories.Add(new Category { Content = Tag }); - Tag = await Task.FromResult(""); - } - } - - protected async Task Remove(string tag) - { - var tagToRemove = await Task.FromResult(PostCategories.Where(c => c.Content == tag).FirstOrDefault()); - PostCategories.Remove(tagToRemove); - } -} diff --git a/src/Blogifier.Admin/Components/CategoriesComponent.razor b/src/Blogifier.Admin/Components/CategoriesComponent.razor new file mode 100644 index 000000000..192cb7870 --- /dev/null +++ b/src/Blogifier.Admin/Components/CategoriesComponent.razor @@ -0,0 +1,51 @@ +@inject IStringLocalizer _localizer + +@code { + [Parameter] public List Categories { get; set; } = default!; + + protected string Tag { get; set; } = default!; + + protected override void OnInitialized() + { + Tag = string.Empty; + } + + protected void KeyPressed(KeyboardEventArgs eventArgs) + { + if (eventArgs.Code == "Enter") + { + Categories.Add(new CategoryDto { Content = Tag }); + Tag = string.Empty; + } + } + + protected void Remove(string tag) + { + Categories!.Remove(Categories.Where(c => c.Content == tag).First()); + } +} + +
+ @foreach (var item in Categories) + { +
+ + + + +
+ } + +
+ diff --git a/src/Blogifier.Admin/Components/Customize/MenusComponent.razor b/src/Blogifier.Admin/Components/Customize/MenusComponent.razor deleted file mode 100644 index 53e198642..000000000 --- a/src/Blogifier.Admin/Components/Customize/MenusComponent.razor +++ /dev/null @@ -1,6 +0,0 @@ -@inject IStringLocalizer _localizer -
- @_localizer["under-development"]. - @_localizer["more-info"] -
diff --git a/src/Blogifier.Admin/Components/Customize/SettingsComponent.razor b/src/Blogifier.Admin/Components/Customize/SettingsComponent.razor deleted file mode 100644 index 0b9d47782..000000000 --- a/src/Blogifier.Admin/Components/Customize/SettingsComponent.razor +++ /dev/null @@ -1,100 +0,0 @@ -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IJSRuntime JSRuntime - -@if (Settings != null) -{ -
- @foreach (var item in Settings.Sections) - { - var sec = item.Label.GetHashCode(); - -
-

- -

-
- -
- @foreach (var field in item.Fields) - { -
- @if (field.Type == "select") - { - - - } - else if (field.Type == "checkbox") - { -
- - -
- } - else if (field.Type == "textarea") - { - - - } - else - { - - - } -
- } -
-
-
- } -
-} - -@code { - protected Blog Blog { get; set; } - protected ThemeItem CurrentTheme { get; set; } - protected ToasterComponent Toaster; - protected ThemeSettings Settings { get; set; } - - protected override async Task OnInitializedAsync() - { - await Load(); - } - - public async Task Load() - { - Blog = await _http.GetFromJsonAsync("api/blog"); - var allThemes = await _http.GetFromJsonAsync>($"api/storage/themes"); - - Settings = await _http.GetFromJsonAsync($"api/theme/{Blog.Theme}"); - } - - public async Task Save() - { - foreach (var section in Settings.Sections) - { - foreach (var field in section.Fields) - { - var val = await JSRuntime.InvokeAsync("commonJsFunctions.getFieldValue", field); - field.Value = val.ToString(); - } - } - return await _http.PostAsJsonAsync($"api/theme/{Blog.Theme}", Settings); - } -} diff --git a/src/Blogifier.Admin/Components/Customize/WidgetsComponent.razor b/src/Blogifier.Admin/Components/Customize/WidgetsComponent.razor deleted file mode 100644 index 53e198642..000000000 --- a/src/Blogifier.Admin/Components/Customize/WidgetsComponent.razor +++ /dev/null @@ -1,6 +0,0 @@ -@inject IStringLocalizer _localizer -
- @_localizer["under-development"]. - @_localizer["more-info"] -
diff --git a/src/Blogifier.Admin/Components/Dashboard/AnalyticsComponent.razor b/src/Blogifier.Admin/Components/Dashboard/AnalyticsComponent.razor deleted file mode 100644 index 66eed7ab0..000000000 --- a/src/Blogifier.Admin/Components/Dashboard/AnalyticsComponent.razor +++ /dev/null @@ -1,226 +0,0 @@ -@using ChartJs.Blazor -@using ChartJs.Blazor.Common -@using ChartJs.Blazor.Common.Enums -@using ChartJs.Blazor.Util -@using ChartJs.Blazor.BarChart -@using System.Drawing - -@inject HttpClient _http -@inject IStringLocalizer _localizer - -
-
-
@_localizer["analytics"]
- -
- - - - - - - -
- - -@code { - protected ToasterComponent Toaster; - private BarConfig _config; - protected List _visits; - - protected bool _hideGraph = false; - protected bool _hideList = true; - - protected List _dateOptions; - protected AnalyticsModel _model; - - protected override async Task OnInitializedAsync() - { - _config = GetConfig(); - - _dateOptions = new List(); - _dateOptions.Add(new OptionItem { Id = 1, Title = _localizer["today"] }); - _dateOptions.Add(new OptionItem { Id = 2, Title = _localizer["yesterday"] }); - _dateOptions.Add(new OptionItem { Id = 3, Title = _localizer["7-days"] }); - _dateOptions.Add(new OptionItem { Id = 4, Title = _localizer["30-days"] }); - _dateOptions.Add(new OptionItem { Id = 5, Title = _localizer["90-days"] }); - - await Load(); - } - - protected async Task Load() - { - IDataset dataset = new BarDataset() - { - Label = "Latest Post Views", - BackgroundColor = ColorUtil.FromDrawingColor(Color.FromArgb(98, 42, 255)), - BorderWidth = 0 - }; - - _model = await _http.GetFromJsonAsync("api/analytics"); - - if (_model == null || _model.LatestPostViews == null) - { - LoadData(dataset, TestData()); - } - else - { - _hideList = _model.DisplayType == AnalyticsListType.Graph; - _hideGraph = _model.DisplayType == AnalyticsListType.List; - - LoadData(dataset, _model.LatestPostViews); - } - - if (_config.Data.Datasets.Count > 0) - { - _config.Data.Datasets.Clear(); - } - - _config.Data.Datasets.Add(dataset); - } - - protected BarConfig GetConfig() - { - return new BarConfig - { - Options = new BarOptions - { - Responsive = true, - Legend = new Legend - { - Position = Position.Top - } - } - }; - } - - protected void LoadData(IDataset dataset, BarChartModel model) - { - _visits = new List(); - var labels = model.Labels.ToList(); - var values = model.Data.ToList(); - - _config.Data.Labels.Clear(); - - for (int i = 0; i < labels.Count; i++) - { - _config.Data.Labels.Add(labels[i]); - dataset.Add(values[i]); - - _visits.Add(new PostVisit { Name = labels[i], Value = values[i] }); - } - _visits = _visits.OrderByDescending(v => v.Value).ToList(); - } - - protected async Task ToggleAnalyticsView(bool isGraph) - { - _hideGraph = await Task.FromResult(!isGraph); - _hideList = isGraph; - - int typeId = isGraph ? 2 : 1; - - Toaster.Toast(await _http.PutAsJsonAsync($"api/analytics/displayType/{typeId}", typeId)); - } - - protected async Task DateOptionSelect(int id) - { - Blog blog = await _http.GetFromJsonAsync("api/blog"); - blog.AnalyticsPeriod = id; - _model.DisplayPeriod = (AnalyticsPeriod)id; - Toaster.Toast(await _http.PutAsJsonAsync("api/blog", blog)); - await Load(); - } - - protected BarChartModel TestData() - { - return new BarChartModel() - { - Labels = new List() { "Post One", "Post Two", "Post Three", "Post Four", "Post Five", "Post Six" }, - Data = new List() { 12036, 15350, 9457, 11104, 7938, 8136 } - }; - } - - protected string PeriodLabel() - { - if (_model == null) - return ""; - return _model.DisplayPeriod.ToString(); - } -} diff --git a/src/Blogifier.Admin/Components/Dashboard/TotalsComponent.razor b/src/Blogifier.Admin/Components/Dashboard/TotalsComponent.razor deleted file mode 100644 index 95192830c..000000000 --- a/src/Blogifier.Admin/Components/Dashboard/TotalsComponent.razor +++ /dev/null @@ -1,35 +0,0 @@ -@inject HttpClient _http -@inject IStringLocalizer _localizer - -@if (_model != null) -{ - -} - -@code { - protected AnalyticsModel _model; - - protected override async Task OnInitializedAsync() - { - _model = await _http.GetFromJsonAsync("api/analytics"); - } -} diff --git a/src/Blogifier.Admin/Components/EditorComponent.razor b/src/Blogifier.Admin/Components/EditorComponent.razor index 6d347736b..a75ff1054 100644 --- a/src/Blogifier.Admin/Components/EditorComponent.razor +++ b/src/Blogifier.Admin/Components/EditorComponent.razor @@ -1,20 +1,52 @@ -@inject IJSRuntime JSRuntime +@implements IAsyncDisposable @inject IStringLocalizer _localizer +@inject IJSRuntime _jsRuntime +
- +
+ @code { - [Parameter] public string Content { get; set; } - [Parameter] public string Toolbar { get; set; } + + [Parameter] public string Toolbar { get; set; } = default!; + + private ValueTask _taskModule; + private ElementReference? _textareaReference; + private InputFile? inputImageFiles; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JSRuntime.InvokeAsync("commonJsFunctions.loadEditor", Toolbar); + _taskModule = _jsRuntime.InvokeAsync("import", "./admin/js/editor.min.js"); + var module = await _taskModule; + await module.InvokeVoidAsync("loadEditor", Toolbar, _textareaReference, inputImageFiles?.Element); } + } + + protected async Task LoadImageFiles(InputFileChangeEventArgs args) + { + var module = await _taskModule; + await module.InvokeVoidAsync("previewImage", inputImageFiles?.Element); + } - await JSRuntime.InvokeAsync("commonJsFunctions.setEditorValue", Content); + public async ValueTask SetValueAsync(string value) + { + var module = await _taskModule; + await module.InvokeVoidAsync("setEditorValue", value); + } + + public async ValueTask GetValueAsync() + { + var module = await _taskModule; + var content = await module.InvokeAsync("getEditorValue"); + return content; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + var module = await _taskModule; + await module.DisposeAsync(); } } diff --git a/src/Blogifier.Admin/Components/NavMenuComponent.razor b/src/Blogifier.Admin/Components/NavMenuComponent.razor new file mode 100644 index 000000000..a139ee4e9 --- /dev/null +++ b/src/Blogifier.Admin/Components/NavMenuComponent.razor @@ -0,0 +1,126 @@ +@inject HttpClient _http +@inject NavigationManager _navigationManager +@inject AuthenticationStateProvider _stateProvider +@inject IJSRuntime _jsRuntime +@inject IStringLocalizer _localizer + +@code { + + protected BlogifierClaims? _claims; + + + protected override async Task OnInitializedAsync() + { + var state = await _stateProvider.GetAuthenticationStateAsync(); + var identity = state.User.Identity; + _claims = BlogifierClaims.Analysis(state.User); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await _jsRuntime.InvokeAsync("commonJsFunctions.setTooltip", ""); + } + } +} + + diff --git a/src/Blogifier.Admin/Components/PageTitleComponent.razor b/src/Blogifier.Admin/Components/PageTitleComponent.razor new file mode 100644 index 000000000..fba394574 --- /dev/null +++ b/src/Blogifier.Admin/Components/PageTitleComponent.razor @@ -0,0 +1,11 @@ +@inject IJSRuntime _jsRuntime; + +@code { + [Parameter] public string Title { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await SetTitleAsync(); + } + private async Task SetTitleAsync() => await _jsRuntime.InvokeVoidAsync("commonJsFunctions.setTitle", Title); +} diff --git a/src/Blogifier.Admin/Components/RedirectComponent.razor b/src/Blogifier.Admin/Components/RedirectComponent.razor new file mode 100644 index 000000000..85d57ba8f --- /dev/null +++ b/src/Blogifier.Admin/Components/RedirectComponent.razor @@ -0,0 +1,8 @@ +@inject NavigationManager _navigationManager + +@code { + protected override void OnInitialized() + { + _navigationManager.NavigateTo($"account?redirectUri={Uri.EscapeDataString(_navigationManager.Uri)}", true, true); + } +} diff --git a/src/Blogifier.Admin/Components/ToasterComponent.razor b/src/Blogifier.Admin/Components/ToasterComponent.razor deleted file mode 100644 index cd59056b3..000000000 --- a/src/Blogifier.Admin/Components/ToasterComponent.razor +++ /dev/null @@ -1,12 +0,0 @@ -@inject IToaster _toaster -@inject IStringLocalizer _localizer - -@code { - public void Toast(HttpResponseMessage msg) - { - if (msg.IsSuccessStatusCode) - _toaster.Success(_localizer["completed"]); - else - _toaster.Error(_localizer["generic-error"]); - } -} diff --git a/src/Blogifier.Admin/Dtos/FrontCategoryItemDto.cs b/src/Blogifier.Admin/Dtos/FrontCategoryItemDto.cs new file mode 100644 index 000000000..5cd40f3b5 --- /dev/null +++ b/src/Blogifier.Admin/Dtos/FrontCategoryItemDto.cs @@ -0,0 +1,6 @@ +namespace Blogifier.Shared; + +public class FrontCategoryItemDto : CategoryItemDto +{ + public bool Selected { get; set; } +} diff --git a/src/Blogifier.Admin/Dtos/FrontImportDto.cs b/src/Blogifier.Admin/Dtos/FrontImportDto.cs new file mode 100644 index 000000000..51a3e21be --- /dev/null +++ b/src/Blogifier.Admin/Dtos/FrontImportDto.cs @@ -0,0 +1,9 @@ +using Blogifier.Shared; +using System.Collections.Generic; + +namespace Blogifier.Admin; + +public class FrontImportDto : ImportDto +{ + public new List Posts { get; set; } = default!; +} diff --git a/src/Blogifier.Admin/Dtos/FrontPostEditorDto.cs b/src/Blogifier.Admin/Dtos/FrontPostEditorDto.cs new file mode 100644 index 000000000..0952133c6 --- /dev/null +++ b/src/Blogifier.Admin/Dtos/FrontPostEditorDto.cs @@ -0,0 +1,9 @@ +using Blogifier.Shared; + +namespace Blogifier.Admin; + +public class FrontPostEditorDto : PostEditorDto +{ + public bool Selected { get; set; } + public bool? ImportComplete { get; set; } +} diff --git a/src/Blogifier.Admin/Dtos/FrontPostItemDto.cs b/src/Blogifier.Admin/Dtos/FrontPostItemDto.cs new file mode 100644 index 000000000..a3fb239a3 --- /dev/null +++ b/src/Blogifier.Admin/Dtos/FrontPostItemDto.cs @@ -0,0 +1,8 @@ +using Blogifier.Shared; + +namespace Blogifier.Admin; + +public class FrontPostItemDto : PostItemDto +{ + public bool Selected { get; set; } +} diff --git a/src/Blogifier.Admin/Dtos/FrontUserInfoDto.cs b/src/Blogifier.Admin/Dtos/FrontUserInfoDto.cs new file mode 100644 index 000000000..bbf1ac17f --- /dev/null +++ b/src/Blogifier.Admin/Dtos/FrontUserInfoDto.cs @@ -0,0 +1,8 @@ +using Blogifier.Shared; + +namespace Blogifier.Admin; + +public class FrontUserInfoDto : UserInfoDto +{ + public bool Selected { get; set; } +} diff --git a/src/Blogifier.Admin/Pages/Account/Login.razor b/src/Blogifier.Admin/Pages/Account/Login.razor deleted file mode 100644 index f8fe78611..000000000 --- a/src/Blogifier.Admin/Pages/Account/Login.razor +++ /dev/null @@ -1,32 +0,0 @@ -@layout AccountLayout -@page "/admin/login/" -@inject HttpClient Http -@inject NavigationManager _navigationManager -@inject IStringLocalizer _localizer - - - -@if (showError) -{ - -} - -@if (model != null) -{ - - -
- - - -
-
- - - -
- -
-} diff --git a/src/Blogifier.Admin/Pages/Account/Login.razor.cs b/src/Blogifier.Admin/Pages/Account/Login.razor.cs deleted file mode 100644 index 32ba0b3db..000000000 --- a/src/Blogifier.Admin/Pages/Account/Login.razor.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Blogifier.Shared; -using Microsoft.AspNetCore.WebUtilities; -using System; -using System.Linq; -using System.Net.Http.Json; -using System.Threading.Tasks; - -namespace Blogifier.Admin.Pages.Account -{ - public partial class Login - { - public bool showError = false; - public LoginModel model = new LoginModel { Email = "", Password = "" }; - - public async Task LoginUser() - { - var returnUrl = "admin/"; - var uri = _navigationManager.ToAbsoluteUri(_navigationManager.Uri); - - if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var param)) - returnUrl = param.First(); - - if (!IsLocalUrl(returnUrl)) - returnUrl = "admin/"; - - var result = await Http.PostAsJsonAsync("api/author/login", model); - - if (result.IsSuccessStatusCode) - { - showError = false; - _navigationManager.NavigateTo(returnUrl, true); - } - else - { - showError = true; - StateHasChanged(); - } - } - - static bool IsLocalUrl(string url) - { - if (url.Contains("//")) - return false; - - Uri result; - return Uri.TryCreate(url, UriKind.Relative, out result); - } - } -} diff --git a/src/Blogifier.Admin/Pages/Account/LogoutView.razor b/src/Blogifier.Admin/Pages/Account/LogoutView.razor deleted file mode 100644 index c6cdb0ea1..000000000 --- a/src/Blogifier.Admin/Pages/Account/LogoutView.razor +++ /dev/null @@ -1,14 +0,0 @@ -@page "/admin/logout/" -@inject HttpClient _http -@inject NavigationManager _navigationManager -@inject IStringLocalizer _localizer - - - -@code { - protected override async Task OnInitializedAsync() - { - await _http.GetFromJsonAsync("api/author/logout"); - _navigationManager.NavigateTo(_navigationManager.BaseUri, true); - } -} diff --git a/src/Blogifier.Admin/Pages/Account/PasswordView.razor b/src/Blogifier.Admin/Pages/Account/PasswordView.razor deleted file mode 100644 index dafae0d27..000000000 --- a/src/Blogifier.Admin/Pages/Account/PasswordView.razor +++ /dev/null @@ -1,58 +0,0 @@ -@layout ProfileLayout -@page "/admin/profile/password/" -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IToaster _toaster - - - -

@_localizer["change-password"]

-
- @if (Author != null) - { - - -
- - - -
-
- - - -
-
- -
-
- } -
- - -@code { - protected Author Author { get; set; } - protected RegisterModel Model { get; set; } - protected ToasterComponent Toaster; - - protected override async Task OnInitializedAsync() - { - await Load(); - } - - protected async Task Load() - { - Author = await _http.GetFromJsonAsync("api/author/getcurrent"); - Model = new RegisterModel - { - Name = Author.DisplayName, - Email = Author.Email - }; - } - - protected async Task SavePassword() - { - Toaster.Toast(await _http.PutAsJsonAsync("api/author/changepassword", Model)); - await Load(); - } -} diff --git a/src/Blogifier.Admin/Pages/Account/ProfileView.razor b/src/Blogifier.Admin/Pages/Account/ProfileView.razor deleted file mode 100644 index 176c6367e..000000000 --- a/src/Blogifier.Admin/Pages/Account/ProfileView.razor +++ /dev/null @@ -1,94 +0,0 @@ -@layout ProfileLayout -@page "/admin/profile/" -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IToaster _toaster - - - -

@_localizer["edit-profile"]

-
- @if (Author != null) - { - - -
- -
- @Author.DisplayName - - -
-
-
- - - -
-
- - - -
-
- - -
-
- - -
-
- -} - - - -@code { - protected ToasterComponent Toaster; - protected List Categories { get; set; } - protected Category CurrentCategory { get; set; } - protected string SearchTerm { get; set; } - protected bool IsEdit = false; - - protected override async Task OnInitializedAsync() - { - await Load(); - } - - protected async Task Load() - { - Categories = await _http.GetFromJsonAsync>($"api/category"); - } - - protected async Task ShowEdit(int categoryId) - { - CurrentCategory = await _http.GetFromJsonAsync($"api/category/byId/{categoryId}"); - IsEdit = true; - } - - protected void CancelEdit() - { - CurrentCategory = null; - IsEdit = false; - } - - protected async Task SaveEdit() - { - Toaster.Toast(await _http.PutAsJsonAsync("api/category", CurrentCategory)); - CurrentCategory = null; - IsEdit = false; - await Load(); - } - - public void CheckAll(object checkValue) - { - bool isChecked = (bool)checkValue; - Categories.ForEach(p => p.Selected = isChecked); - StateHasChanged(); - } - - public async Task RunAction(GroupAction action) - { - string confirm = _localizer["confirm-delete"]; - bool confirmed = false; - - if (action == GroupAction.Delete) - { - confirmed = await _jsruntime.InvokeAsync("confirm", confirm); - if (!confirmed) - return; - } - - foreach (var category in Categories) - { - if (category.Selected) - { - await _http.DeleteAsync($"api/category/{category.Id}"); - } - } - await Load(); - } - - protected async Task SearchKeyPress(KeyboardEventArgs e) - { - if (e.Key == "Enter") - await SearchCategories(); - } - - protected async Task SearchCategories() - { - if (string.IsNullOrEmpty(SearchTerm)) - SearchTerm = "*"; - - Categories = await _http.GetFromJsonAsync>($"api/category/{SearchTerm}"); - SearchTerm = ""; - } -} diff --git a/src/Blogifier.Admin/Pages/Blog/Editor.razor b/src/Blogifier.Admin/Pages/Blog/Editor.razor deleted file mode 100644 index 2eb89cdb5..000000000 --- a/src/Blogifier.Admin/Pages/Blog/Editor.razor +++ /dev/null @@ -1,245 +0,0 @@ -@page "/admin/editor/{Slug?}/" -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IJSRuntime _jsruntime -@inject IToaster _toaster -@inject NavigationManager _navigation -@inject BlogStateProvider _stateprovider -@implements IDisposable - - - -@if (Post != null) -{ -
-
- @_localizer[ -
-
- @if (Post.Id == 0) - { - - - } - else - { - if (Post.PublishedAt > DateTime.MinValue) - { - - - - - - - - @_localizer["view"] - - } - else - { - - - - } - } -
-
-
-
- - -
- - -
-
-
-
- -
-} - - - -@code -{ - protected Author Author { get; set; } - protected Post Post { get; set; } - protected ToasterComponent Toaster; - protected CategoriesComponent Categories; - protected string Confirm { get; set; } - protected string PostTile - { - get - { - return _stateprovider.PostType == PostType.Post ? _localizer["post-title"] : _localizer["page-title"]; - } - } - - [Parameter] -#nullable enable - public string? Slug { get; set; } -#nullable disable - - protected override async Task OnInitializedAsync() - { - Author = await _http.GetFromJsonAsync("api/author/getcurrent"); - Confirm = _localizer["confirm-delete"]; - _stateprovider.OnChange += StateHasChanged; - await Load(); - } - - protected async Task Load() - { - Post = NewPost(); - - if (!string.IsNullOrEmpty(Slug)) - { - Post = await _http.GetFromJsonAsync($"api/post/byslug/{Slug}"); - var headTitle = _localizer["edit"] + " - " + Post.Title; - await _jsruntime.InvokeVoidAsync("commonJsFunctions.setTitle", headTitle); - } - } - - protected async Task SavePost(PostAction postAction) - { - Post.Content = await _jsruntime.InvokeAsync("commonJsFunctions.getEditorValue", ""); - Post.PostType = _stateprovider.PostType; - - Post.Cover = await _jsruntime.InvokeAsync("commonJsFunctions.getSrcValue", "postCover"); - Post.Cover = Post.Cover.Replace(_navigation.BaseUri, ""); - - if (string.IsNullOrEmpty(Post.Cover)) - Post.Cover = Constants.DefaultCover; - - if (string.IsNullOrEmpty(Post.Description)) - Post.Description = Post.Title; - - if (string.IsNullOrEmpty(Post.Title) || string.IsNullOrEmpty(Post.Content)) - { - _toaster.Error(_localizer["title-content-required"]); - return; - } - - if (postAction == PostAction.Publish) - Post.PublishedAt = DateTime.UtcNow; - - if (postAction == PostAction.Unpublish) - Post.PublishedAt = DateTime.MinValue; - - if (Post.Id == 0) - { - Post.Slug = await _http.GetStringAsync($"api/post/getslug/{Post.Title}"); - HttpResponseMessage result = await _http.PostAsJsonAsync($"api/post/add", Post); - - if (result.IsSuccessStatusCode) - { - Post = await _http.GetFromJsonAsync($"api/post/byslug/{Post.Slug}"); - await _http.PutAsJsonAsync>($"api/category/{Post.Id}", Categories.PostCategories); - - if (postAction == PostAction.Publish) - { - await _http.GetFromJsonAsync($"api/newsletter/send/{Post.Id}"); - } - - _navigation.NavigateTo($"/admin/editor/{Post.Slug}"); - await Load(); - } - - Toaster.Toast(result); - } - else - { - Toaster.Toast(await _http.PutAsJsonAsync($"api/post/update", Post)); - - await _http.PutAsJsonAsync>($"api/category/{Post.Id}", Categories.PostCategories); - - await _http.GetFromJsonAsync($"api/newsletter/send/{Post.Id}"); - await Load(); - } - } - - protected async Task Save() - { - await SavePost(PostAction.Save); - } - - protected async Task Publish() - { - await SavePost(PostAction.Publish); - } - - protected async Task Unpublish() - { - await SavePost(PostAction.Unpublish); - } - - protected async Task Remove(int id) - { - if (await _jsruntime.InvokeAsync("confirm", Confirm)) - { - Toaster.Toast(await _http.DeleteAsync($"api/post/{id}")); - _navigation.NavigateTo($"admin"); - } - } - - protected async Task ResetCover() - { - Post.Cover = Constants.DefaultCover; - await Save(); - } - - protected async Task RemoveCover() - { - Post.Cover = null; - await Save(); - } - - protected Post NewPost() - { - return new Post - { - Id = 0, - Title = "", - Description = "", - Content = "", - AuthorId = Author.Id, - PostType = PostType.Post, - Cover = Constants.DefaultCover - }; - } - - public void Dispose() - { - _stateprovider.OnChange -= StateHasChanged; - } -} diff --git a/src/Blogifier.Admin/Pages/Blog/ImportView.razor b/src/Blogifier.Admin/Pages/Blog/ImportView.razor deleted file mode 100644 index d6a271d07..000000000 --- a/src/Blogifier.Admin/Pages/Blog/ImportView.razor +++ /dev/null @@ -1,131 +0,0 @@ -@layout BlogLayout -@page "/admin/blog/import/" -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IToaster _toaster -@inject IJSRuntime _jsRuntime - - - -@if (Import == null) -{ -

@_localizer["import"]

-
- - -
- - - -
-
- -
-
-
-} -else -{ -
-
@Import.Posts.Count @_localizer["import-message-found"].
-
- -
-
    - @foreach (var post in Import.Posts) - { -
  • - - - -
    @post.Title
    -
    @post.Published.ToFriendlyShortDateString()
    -
  • - } -
-
@StatusMsg
-
- - -
-
-} - -@code { - protected ImportRssRequest Model { get; set; } = default!; - protected ImportDto? Import { get; set; } - protected string StatusMsg { get; set; } = default!; - protected string StatusMsgCss { get; set; } = default!; - - protected override void OnInitialized() - { - OnLoad(); - } - - protected void OnLoad() - { - Model = new ImportRssRequest { FeedUrl = "" }; - Import = null; - StatusMsg = ""; - } - - protected async Task OnAnalysis() - { - Import = await _http.GetFromJsonAsync($"api/import/rss?feedUrl={Model.FeedUrl}"); - } - - protected async Task OnImport() - { - int successCnt = 0; - int failedCnt = 0; - - foreach (var post in Import!.Posts!) - { - if (!post.Selected) - continue; - - var result = await _http.PostAsJsonAsync("api/import/write", post); - if (result.IsSuccessStatusCode) - { - await _jsRuntime.InvokeAsync("commonJsFunctions.replaceElement", post.Slug, true); - successCnt++; - } - else - { - await _jsRuntime.InvokeAsync("commonJsFunctions.replaceElement", post.Slug, false); - failedCnt++; - } - } - - if (failedCnt == 0 && successCnt > 0) - { - StatusMsg = $"Imported {successCnt} posts."; - StatusMsgCss = $"alert-success"; - } - else - { - StatusMsg = $"Imported {successCnt} posts out of {successCnt + failedCnt}. Please check logs for errors."; - StatusMsgCss = $"alert-warning"; - } - } - - public void CheckAll(object? checkValue) - { - bool isChecked = checkValue != null ? (bool)checkValue : false; - Import!.Posts.ForEach(p => p.Selected = isChecked); - StateHasChanged(); - } -} diff --git a/src/Blogifier.Admin/Pages/Blog/PostsView.razor b/src/Blogifier.Admin/Pages/Blog/PostsView.razor deleted file mode 100644 index a2044d4e6..000000000 --- a/src/Blogifier.Admin/Pages/Blog/PostsView.razor +++ /dev/null @@ -1,313 +0,0 @@ -@layout BlogLayout -@page "/admin/blog/" -@using Blogifier.Shared -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IJSRuntime _jsruntime -@inject IToaster _toaster -@inject NavigationManager _navigation -@inject BlogStateProvider _stateprovider -@implements IDisposable - - - - -
- - - - @_localizer["new-post"] - - - - - - - -
- - - -@if (Posts != null && Posts.Count > 0) -{ -
-
    - - - @{ - string pubDate = post.PublishedAt > DateTime.MinValue ? post.PublishedAt.ToFriendlyShortDateString() : @_localizer["draft"]; - string pubStatus = post.PublishedAt > DateTime.MinValue ? "published" : ""; - string featured = post.IsFeatured ? "featured" : ""; - } - -
  • - - - @post.Title - - - @pubDate - - - - - - - - - - -
  • -
    -
-
-} -else -{ -
- - - - -

@_localizer["list-is-empty"]

-
-} - - -@code { - protected List Posts { get; set; } - protected Author Author { get; set; } - - protected string SearchTerm { get; set; } - - protected string FilterLabel { get; set; } - protected PublishedStatus FilterValue { get; set; } - - protected string PostTypeLabel { get; set; } - protected string PostTypeButton { get; set; } - protected PostType PostTypeValue { get; set; } - - protected override async Task OnInitializedAsync() - { - Author = await _http.GetFromJsonAsync("api/author/getcurrent"); - - FilterLabel = _localizer["all"]; - PostTypeLabel = _localizer["posts"]; - PostTypeButton = _localizer["post"]; - - _stateprovider.OnChange += StateHasChanged; - _stateprovider.PostType = PostType.Post; - PostTypeValue = PostType.Post; - - await Load(); - } - - protected async Task Load() - { - Posts = await _http.GetFromJsonAsync>($"api/post/list/{FilterValue}/{PostTypeValue}"); - } - - public void CheckAll(object checkValue) - { - bool isChecked = (bool)checkValue; - Posts.ForEach(p => p.Selected = isChecked); - StateHasChanged(); - } - - public async Task RunAction(GroupAction action) - { - string confirm = _localizer["confirm-delete"]; - bool confirmed = false; - - if (action == GroupAction.Delete) - { - confirmed = await _jsruntime.InvokeAsync("confirm", confirm); - if (!confirmed) - return; - } - - foreach (var post in Posts) - { - if (post.Selected) - { - switch (action) - { - case GroupAction.Publish: - await _http.PutAsJsonAsync($"api/post/publish/{post.Id}", true); - break; - case GroupAction.Unpublish: - await _http.PutAsJsonAsync($"api/post/publish/{post.Id}", false); - break; - case GroupAction.Feature: - await _http.PutAsJsonAsync($"api/post/featured/{post.Id}", !post.IsFeatured); - break; - case GroupAction.Delete: - await _http.DeleteAsync($"api/post/{post.Id}"); - break; - } - } - } - await Load(); - } - - protected async Task SearchKeyPress(KeyboardEventArgs e) - { - if (e.Key == "Enter") - await SearchPosts(); - } - - protected async Task SearchPosts() - { - if (string.IsNullOrEmpty(SearchTerm)) - SearchTerm = "*"; - - Posts = await _http.GetFromJsonAsync>($"api/post/list/search/{SearchTerm}"); - SearchTerm = ""; - } - - protected async Task GetPosts() - { - PostTypeLabel = _localizer["posts"]; - PostTypeButton = _localizer["post"]; - PostTypeValue = PostType.Post; - _stateprovider.SetPostType(PostType.Post); - await Load(); - } - - protected async Task GetPages() - { - PostTypeLabel = _localizer["pages"]; - PostTypeButton = _localizer["page"]; - PostTypeValue = PostType.Page; - _stateprovider.SetPostType(PostType.Page); - await Load(); - } - - public async Task Filter(PublishedStatus filter) - { - FilterValue = filter; - switch (filter) - { - case PublishedStatus.Published: - FilterLabel = _localizer["published"]; - break; - case PublishedStatus.Drafts: - FilterLabel = _localizer["draft", true]; - break; - case PublishedStatus.Featured: - FilterLabel = _localizer["featured"]; - break; - default: - FilterLabel = _localizer["all"]; - break; - } - await Load(); - } - - public async Task Publish(Post post) - { - Toast(await _http.PutAsJsonAsync($"api/post/publish/{post.Id}", (post.PublishedAt == DateTime.MinValue))); - await Load(); - } - - public async Task Featured(Post post) - { - Toast(await _http.PutAsJsonAsync($"api/post/featured/{post.Id}", !post.IsFeatured)); - await Load(); - } - - protected void Toast(HttpResponseMessage msg) - { - if (msg.IsSuccessStatusCode) - _toaster.Success(_localizer["completed"]); - else - _toaster.Error(_localizer["generic-error"]); - } - - protected void Open() { - } - - public void Dispose() - { - _stateprovider.OnChange -= StateHasChanged; - } -} diff --git a/src/Blogifier.Admin/Pages/Blog/SettingsView.razor b/src/Blogifier.Admin/Pages/Blog/SettingsView.razor deleted file mode 100644 index d1babb613..000000000 --- a/src/Blogifier.Admin/Pages/Blog/SettingsView.razor +++ /dev/null @@ -1,48 +0,0 @@ -@layout BlogLayout -@page "/admin/blog/settings/" -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IToaster _toaster -@inject IJSRuntime JSRuntime - - - -@if (Blog != null) -{ -

@_localizer["blog-settings"]

-
- - - -
-
- - -
-
-
- - -
-
- -
-
-
-} - - -@code { - protected Blog Blog { get; set; } - protected ToasterComponent Toaster; - - protected override async Task OnInitializedAsync() - { - Blog = await _http.GetFromJsonAsync("api/blog"); - } - - protected async Task Save() - { - Toaster.Toast(await _http.PutAsJsonAsync("api/blog", Blog)); - } -} diff --git a/src/Blogifier.Admin/Pages/Blogs/BlogsView.razor b/src/Blogifier.Admin/Pages/Blogs/BlogsView.razor new file mode 100644 index 000000000..c5eb0e836 --- /dev/null +++ b/src/Blogifier.Admin/Pages/Blogs/BlogsView.razor @@ -0,0 +1,249 @@ +@page "/admin/blogs" + +@layout BlogsLayout + +@inject HttpClient _http +@inject IMapper _mapper +@inject NavigationManager _navigation +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsruntime +@inject IToaster _toaster + +@code { + + protected List? Posts { get; set; } + protected string? SearchTerm { get; set; } + protected PublishedStatus FilterValue { get; set; } = PublishedStatus.All; + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + protected async Task LoadAsync() + { + var result = await _http.GetFromJsonAsync>($"api/post/items/{(int)FilterValue}/{(int)PostType.Post}"); + Posts = _mapper.Map>(result); + } + + public void CheckAll(object? checkValue) + { + if (checkValue != null) + { + var isChecked = (bool)checkValue; + if (Posts != null) Posts.ForEach(item => item.Selected = isChecked); + StateHasChanged(); + } + } + + private async Task RunAction(GroupAction action) + { + if (action == GroupAction.Delete) + { + var confirmedString = _localizer["confirm-delete"].Value; + var confirmed = await _jsruntime.InvokeAsync("confirm", confirmedString); + if (!confirmed) return; + } + + if (Posts != null) + { + var ids = Posts.Where(m => m.Selected).Select(m => m.Id); + if (ids.Any()) + { + var idsString = string.Join(",", ids); + switch (action) + { + case GroupAction.Publish: + await _http.PutAsJsonAsync($"api/post/state/{idsString}", PostState.Release); + break; + case GroupAction.Unpublish: + await _http.PutAsJsonAsync($"api/post/state/{idsString}", PostState.Draft); + break; + case GroupAction.Delete: + await _http.DeleteAsync($"api/post/{idsString}"); + break; + } + } + } + await LoadAsync(); + } + + protected async Task SearchKeyPress(KeyboardEventArgs e) + { + if (e.Key == "Enter") + await SearchPosts(); + } + + protected async Task SearchPosts() + { + if (string.IsNullOrEmpty(SearchTerm)) SearchTerm = "*"; + var result = await _http.GetFromJsonAsync>($"api/post/items/search/{SearchTerm}"); + Posts = _mapper.Map>(result); + } + + public async Task Filter(PublishedStatus filter) + { + FilterValue = filter; + await LoadAsync(); + } + + public async Task Publish(PostItemDto post) + { + var state = post.State != PostState.Release ? PostState.Release : PostState.Draft; + Toast(await _http.PutAsJsonAsync($"api/post/state/{post.Id}", state)); + await LoadAsync(); + } + + public async Task Featured(PostItemDto post) + { + var state = post.State != PostState.Featured ? PostState.Featured : PostState.Release; + Toast(await _http.PutAsJsonAsync($"api/post/state/{post.Id}", state)); + await LoadAsync(); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } +} + + + +
+ + @_localizer["new-post"] + + + + +
+ + + +@if (Posts != null && Posts.Count > 0) +{ +
+
    + +
  • + + + @post.Title + + + @DateTimeHelper.ToFriendlyShortDateString(post.PublishedAt, _localizer["draft"]) + + + + + + + + + + +
  • +
    +
+
+} +else +{ +
+ + + + +

@_localizer["list-is-empty"]

+
+} diff --git a/src/Blogifier.Admin/Pages/Blogs/CategoryEditorView.razor b/src/Blogifier.Admin/Pages/Blogs/CategoryEditorView.razor new file mode 100644 index 000000000..032f65a90 --- /dev/null +++ b/src/Blogifier.Admin/Pages/Blogs/CategoryEditorView.razor @@ -0,0 +1,68 @@ +@page "/admin/blogs/category/{Id:int?}" + +@layout BlogsLayout + +@inject HttpClient _http +@inject IMapper _mapper +@inject NavigationManager _navigation +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsruntime +@inject IToaster _toaster + +

@_localizer["edit-category"]

+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +@code { + + [Parameter] public int? Id { get; set; } + + protected CategoryEitorDto Category { get; set; } = new CategoryEitorDto(); + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + protected async Task LoadAsync() + { + var result = await _http.GetFromJsonAsync($"api/category/byId/{Id}"); + if (result != null) Category = result; + } + + protected void Cancel() + { + _navigation.NavigateTo("/admin/blogs/category"); + } + + protected async Task Save() + { + Toast(await _http.PutAsJsonAsync("api/category", Category)); + Cancel(); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } + +} diff --git a/src/Blogifier.Admin/Pages/Blogs/CategoryView.razor b/src/Blogifier.Admin/Pages/Blogs/CategoryView.razor new file mode 100644 index 000000000..c1111b43f --- /dev/null +++ b/src/Blogifier.Admin/Pages/Blogs/CategoryView.razor @@ -0,0 +1,156 @@ +@page "/admin/blogs/category" + +@layout BlogsLayout + +@inject HttpClient _http +@inject IMapper _mapper +@inject NavigationManager _navigation +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsruntime +@inject IToaster _toaster + + +@code { + + protected List? Categories { get; set; } + protected string? SearchTerm { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + protected async Task LoadAsync() + { + var result = await _http.GetFromJsonAsync>($"api/category/items"); + Categories = _mapper.Map>(result); + } + + protected void CheckAll(object? checkValue) + { + if (checkValue != null && Categories != null) + { + var isChecked = (bool)checkValue; + Categories.ForEach(p => p.Selected = isChecked); + StateHasChanged(); + } + } + + public async Task RunAction(GroupAction action) + { + if (action == GroupAction.Delete) + { + var confirm = _localizer["confirm-delete"]; + var confirmed = await _jsruntime.InvokeAsync("confirm", confirm.Value); + if (!confirmed) return; + } + + if (Categories != null) + { + var categoryeId = Categories.Where(m => m.Selected).Select(m => m.Id); + if (categoryeId.Any()) + { + var categoryeIdsString = string.Join(",", categoryeId); + await _http.DeleteAsync($"api/category/{categoryeIdsString}"); + } + } + await LoadAsync(); + } + + protected async Task SearchKeyPress(KeyboardEventArgs e) + { + if (e.Key == "Enter") + await SearchCategories(); + } + + protected async Task SearchCategories() + { + if (string.IsNullOrEmpty(SearchTerm)) + SearchTerm = "*"; + + Categories = await _http.GetFromJsonAsync>($"api/category/{SearchTerm}"); + SearchTerm = ""; + } + + protected void OnEdit(int categoryId) + { + _navigation.NavigateTo($"/admin/blogs/category/{categoryId}"); + } +} + + + +
+
+ + @*@_localizer["new-category"]*@ + @if (Categories != null && Categories.Count > 0) + { + + } + +
+ + + + @if (Categories != null && Categories.Any()) + { + + } + else + { +
+ + + + +

@_localizer["list-is-empty"]

+
+ } +
+ + diff --git a/src/Blogifier.Admin/Pages/Blogs/EditorView.razor b/src/Blogifier.Admin/Pages/Blogs/EditorView.razor new file mode 100644 index 000000000..861dfe9b8 --- /dev/null +++ b/src/Blogifier.Admin/Pages/Blogs/EditorView.razor @@ -0,0 +1,181 @@ +@page "/admin/blogs/editor/{Slug?}" + +@inject HttpClient _httpClient +@inject IStringLocalizer _localizer +@inject NavigationManager _navigation +@inject IJSRuntime _jsruntime +@inject IToaster _toaster +@inject ToasterService _toasterService +@inject ILogger _logger + + + +
+
+ @_localizer[ +
+
+ @if (string.IsNullOrEmpty(Post.Slug)) + { + + + } + else if (Post.State >= PostState.Release) + { + + + + + + + + @_localizer["view"] + + } + else + { + + + + } +
+
+
+
+ + +
+ + +
+
+
+
+ +
+ +@code { + + [Parameter] public string? Slug { get; set; } + + private EditorComponent _editorComponent = default!; + + protected PostEditorDto Post { get; set; } = new PostEditorDto + { + Title = string.Empty, + Description = string.Empty, + Content = string.Empty, + PostType = PostType.Post, + Cover = BlogifierConstant.DefaultCover, + Categories = new List(), + }; + + protected override async Task OnInitializedAsync() + { + if (!string.IsNullOrEmpty(Slug)) + { + Post = (await _httpClient.GetFromJsonAsync($"api/post/byslug/{Slug}"))!; + if (Post.Categories == null) Post.Categories = new List(); + await SetJsPostAsync(Post); + } + } + + protected async Task SavePostAsync(PostState postState) + { + var content = await _editorComponent.GetValueAsync(); + if (string.IsNullOrEmpty(Post.Title) || string.IsNullOrEmpty(content)) + { + _toaster.Error(_localizer["title-content-required"]); + return; + } + Post.Content = content; + Post.Cover = await _jsruntime.InvokeAsync("commonJsFunctions.getSrcValue", "postCover"); + Post.Cover = Post.Cover.Replace(_navigation.BaseUri, ""); + if (string.IsNullOrEmpty(Post.Cover)) Post.Cover = BlogifierConstant.DefaultCover; + if (string.IsNullOrEmpty(Post.Description)) Post.Description = Post.Title; + Post.State = postState; + if (Post.Id == 0) + { + var response = await _httpClient.PostAsJsonAsync($"api/post/add", Post); + _toasterService.CheckResponse(response); + } + else + { + var response = await _httpClient.PutAsJsonAsync($"api/post/update", Post); + _toasterService.CheckResponse(response); + } + } + + protected async Task SaveAsync() + { + await SavePostAsync(PostState.Draft); + } + + protected async Task PublishAsync() + { + await SavePostAsync(PostState.Release); + } + + protected async Task UnpublishAsync() + { + await SavePostAsync(PostState.Draft); + } + + protected async Task RemoveAsync(int id) + { + if (await _jsruntime.InvokeAsync("confirm", _localizer["confirm-delete"])) + { + var response = await _httpClient.DeleteAsync($"api/post/{id}"); + _toasterService.CheckResponse(response); + _navigation.NavigateTo($"admin"); + } + } + + protected async Task ResetCoverAsync() + { + Post.Cover = BlogifierConstant.DefaultCover; + await SaveAsync(); + } + + protected async Task RemoveCoverAsync() + { + Post.Cover = null; + await SaveAsync(); + } + + private async Task SetJsPostAsync(PostEditorDto post) + { + var headTitle = _localizer["edit"] + " - " + Post.Title; + await _jsruntime.InvokeVoidAsync("commonJsFunctions.setTitle", headTitle); + await _editorComponent.SetValueAsync(Post.Content); + } +} diff --git a/src/Blogifier.Admin/Pages/Blogs/ImportView.razor b/src/Blogifier.Admin/Pages/Blogs/ImportView.razor new file mode 100644 index 000000000..06b3081a3 --- /dev/null +++ b/src/Blogifier.Admin/Pages/Blogs/ImportView.razor @@ -0,0 +1,155 @@ +@page "/admin/blogs/import" +@layout BlogsLayout +@inject HttpClient _http +@inject IStringLocalizer _localizer +@inject IToaster _toaster +@inject IJSRuntime _jsRuntime +@inject IMapper _mapper + + + +@if (Status != null) +{ +
@Status.Msg
+} +@if (Import == null) +{ +

@_localizer["import"]

+
+ + +
+ + + +
+
+ +
+
+
+} +else +{ +
+
@Import.Posts.Count @_localizer["import-message-found"].
+
+ +
+
    + @foreach (var post in Import.Posts) + { +
  • + + @if (!post.ImportComplete.HasValue) + { + + } + else if (post.ImportComplete.Value) + { + + + + + + } + else + { + + + + + + } +
    @post.Title
    +
    @DateTimeHelper.ToFriendlyShortDateString(post.PublishedAt)
    +
  • + } +
+
+ + +
+
+} + +@code { + protected ImportRssDto ImportRss { get; set; } = default!; + protected FrontImportDto? Import { get; set; } + protected AlertStatus? Status { get; set; } + + protected override void OnInitialized() + { + OnLoad(); + } + + protected void OnLoad() + { + ImportRss = new ImportRssDto { FeedUrl = string.Empty }; + Import = null; + Status = null; + } + + protected async Task OnAnalysis() + { + Status = null; + var result = await _http.GetFromJsonAsync($"api/import/rss?feedUrl={ImportRss.FeedUrl}"); + var front = _mapper.Map(result); + Import = front; + } + + protected async Task OnImport() + { + var request = new ImportDto { BaseUrl = Import!.BaseUrl, Posts = new List() }; + foreach (var post in Import!.Posts!) + { + if (!post.Selected) continue; + var inputPost = _mapper.Map(post); + request.Posts.Add(inputPost); + } + if (!request.Posts.Any()) return; + + var response = await _http.PostAsJsonAsync("api/import/write", request); + if (response.IsSuccessStatusCode) + { + var stream = await response.Content.ReadAsStreamAsync(); + var inputPosts = (await JsonSerializer.DeserializeAsync>(stream, BlogifierConstant.DefaultJsonSerializerOptionss))!; + var successCount = 0; + foreach (var post in inputPosts) + { + var importPost = Import.Posts.First(m => m.Title.Equals(post.Title, StringComparison.Ordinal)); + importPost.ImportComplete = true; + successCount++; + } + Status = new AlertStatus($"Imported {successCount} posts.", "alert-success"); + } + else + { + Status = new AlertStatus("import posts errors.", "alert-warning"); + } + } + + protected void CheckAll(object? checkValue) + { + bool isChecked = checkValue != null ? (bool)checkValue : false; + Import!.Posts.ForEach(p => p.Selected = isChecked); + StateHasChanged(); + } + + protected class AlertStatus + { + public string Msg { get; set; } + public string MsgCss { get; set; } + + public AlertStatus(string msg, string msgCss) + { + Msg = msg; + MsgCss = msgCss; + } + } +} diff --git a/src/Blogifier.Admin/Pages/Blogs/SettingsView.razor b/src/Blogifier.Admin/Pages/Blogs/SettingsView.razor new file mode 100644 index 000000000..05216bf85 --- /dev/null +++ b/src/Blogifier.Admin/Pages/Blogs/SettingsView.razor @@ -0,0 +1,56 @@ +@page "/admin/blogs/settings" +@layout BlogsLayout + +@inject HttpClient _http +@inject IStringLocalizer _localizer +@inject IToaster _toaster + +@code { + protected BlogEitorDto? Blog { get; set; } + + protected override async Task OnInitializedAsync() + { + Blog = await _http.GetFromJsonAsync("api/blog"); + } + + protected async Task Save() + { + Toast(await _http.PutAsJsonAsync("api/blog", Blog!)); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } +} + + + +@if (Blog != null) +{ +

@_localizer["blog-settings"]

+
+ + + +
+
+ + +
+
+
+ + +
+
+ +
+
+
+} + + diff --git a/src/Blogifier.Admin/Pages/Dashboard/DashboardView.razor b/src/Blogifier.Admin/Pages/Dashboard/DashboardView.razor deleted file mode 100644 index b32b22d6d..000000000 --- a/src/Blogifier.Admin/Pages/Dashboard/DashboardView.razor +++ /dev/null @@ -1,34 +0,0 @@ -@page "/admin/" -@inject HttpClient _http -@inject IStringLocalizer _localizer - - - diff --git a/src/Blogifier.Admin/Pages/Drive/DriveView.razor b/src/Blogifier.Admin/Pages/Drive/DriveView.razor index 19c3199e8..9edbd18c3 100644 --- a/src/Blogifier.Admin/Pages/Drive/DriveView.razor +++ b/src/Blogifier.Admin/Pages/Drive/DriveView.razor @@ -1,7 +1,7 @@ @page "/admin/drive/" @inject IStringLocalizer _localizer - +

@_localizer["drive"]

@@ -9,8 +9,7 @@

@_localizer["drive-describe"]

@_localizer["under-development"]. - + @_localizer["more-info"]
diff --git a/src/Blogifier.Admin/Pages/HomeView.razor b/src/Blogifier.Admin/Pages/HomeView.razor new file mode 100644 index 000000000..6eb030280 --- /dev/null +++ b/src/Blogifier.Admin/Pages/HomeView.razor @@ -0,0 +1,282 @@ +@using ChartJs.Blazor +@using ChartJs.Blazor.Common +@using ChartJs.Blazor.Common.Enums +@using ChartJs.Blazor.Util +@using ChartJs.Blazor.BarChart +@using System.Drawing + +@page "/admin" + +@inject HttpClient _http +@inject IStringLocalizer _localizer +@inject IToaster _toaster + +@code { + + protected BarConfig _config = default!; + protected List _dateOptions = default!; + protected List _visits = default!; + protected bool _hideGraph = false; + protected bool _hideList = true; + protected AnalyticsDto? _analytics; + + protected override async Task OnInitializedAsync() + { + _config = new BarConfig + { + Options = new BarOptions + { + Responsive = true, + Legend = new Legend + { + Position = Position.Top + } + } + }; + _dateOptions = new List + { + new OptionItem { Id = 1, Title = _localizer["today"] }, + new OptionItem { Id = 2, Title = _localizer["yesterday"] }, + new OptionItem { Id = 3, Title = _localizer["7-days"] }, + new OptionItem { Id = 4, Title = _localizer["30-days"] }, + new OptionItem { Id = 5, Title = _localizer["90-days"] }, + }; + Load(); + _analytics = await _http.GetFromJsonAsync("api/analytics"); + } + + protected void Load() + { + var dataset = new BarDataset() + { + Label = "Latest Post Views", + BackgroundColor = ColorUtil.FromDrawingColor(Color.FromArgb(98, 42, 255)), + BorderWidth = 0 + }; + + if (_analytics == null || _analytics.LatestPostViews == null) + { + LoadData(dataset, TestData()); + } + else + { + _hideList = _analytics.DisplayType == AnalyticsListType.Graph; + _hideGraph = _analytics.DisplayType == AnalyticsListType.List; + + LoadData(dataset, _analytics.LatestPostViews); + } + + if (_config.Data.Datasets.Count > 0) + { + _config.Data.Datasets.Clear(); + } + + _config.Data.Datasets.Add(dataset); + } + + + + protected void LoadData(IDataset dataset, BarChartModel model) + { + _visits = new List(); + var labels = model.Labels.ToList(); + var values = model.Data.ToList(); + + _config.Data.Labels.Clear(); + + for (int i = 0; i < labels.Count; i++) + { + _config.Data.Labels.Add(labels[i]); + dataset.Add(values[i]); + + _visits.Add(new PostVisit { Name = labels[i], Value = values[i] }); + } + _visits = _visits.OrderByDescending(v => v.Value).ToList(); + } + + protected async Task ToggleAnalyticsView(bool isGraph) + { + _hideGraph = await Task.FromResult(!isGraph); + _hideList = isGraph; + + int typeId = isGraph ? 2 : 1; + + Toast(await _http.PutAsJsonAsync($"api/analytics/displayType/{typeId}", typeId)); + } + + protected void DateOptionSelect(int id) + { + //var blog = await _http.GetFromJsonAsync("api/blog"); + //blog.AnalyticsPeriod = id; + //_analytics.DisplayPeriod = (AnalyticsPeriod)id; + //Toast(await _http.PutAsJsonAsync("api/blog", blog)); + //Load(); + } + + protected BarChartModel TestData() + { + return new BarChartModel() + { + Labels = new List() { "Post One", "Post Two", "Post Three", "Post Four", "Post Five", "Post Six" }, + Data = new List() { 12036, 15350, 9457, 11104, 7938, 8136 } + }; + } + + protected string PeriodLabel() + { + if (_analytics == null) + return ""; + return _analytics.DisplayPeriod.ToString(); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } +} + + + +
+
+ +
+
+
+
@_localizer["analytics"]
+ +
+ + + + + + + +
+ +
+
+
diff --git a/src/Blogifier.Admin/Pages/Newsletter/NewsletterView.razor b/src/Blogifier.Admin/Pages/Newsletter/NewsletterView.razor index 00f676460..906898584 100644 --- a/src/Blogifier.Admin/Pages/Newsletter/NewsletterView.razor +++ b/src/Blogifier.Admin/Pages/Newsletter/NewsletterView.razor @@ -1,91 +1,87 @@ -@layout NewsletterLayout @page "/admin/newsletter/" + +@layout NewsletterLayout + @inject HttpClient _http @inject IStringLocalizer _localizer @inject IToaster _toaster - +

@_localizer["newsletters"]

- @if (Newsletters == null || Newsletters.Count == 0) - { -

@_localizer["not-found"]

- } - else + @if (Newsletters != null && Newsletters.Any()) {
    - @foreach (var newsletter in Newsletters) + @foreach (var item in Newsletters) {
  • - @{ - string title = $"{newsletter.Post.Title}"; - string pubDate = newsletter.CreatedAt.ToLocalTime().ToFriendlyDateTimeString(); - string pubStatus = newsletter.Success ? "published" : ""; - } - @title - - @pubDate -
  • }
} + else + { +

@_localizer["not-found"]

+ }
- + @code { - protected ToasterComponent Toaster; - protected List Newsletters; + protected List? Newsletters; protected override async Task OnInitializedAsync() { - await Load(); + await LoadAsync(); } - protected async Task Load() + protected async Task LoadAsync() { - Newsletters = await _http.GetFromJsonAsync>($"api/newsletter/newsletters"); + Newsletters = await _http.GetFromJsonAsync>($"api/newsletter/items"); } - protected async Task RemoveNewsletter(int id) + protected async Task DeleteAsync(int id) { - Toaster.Toast(await _http.DeleteAsync($"api/newsletter/remove/{id}")); - await Load(); + Toast(await _http.DeleteAsync($"api/newsletter/remove/{id}")); + await LoadAsync(); } - protected async Task Resend(int postId) + protected async Task ResendAsync(int postId) { bool success = await _http.GetFromJsonAsync($"api/newsletter/send/{postId}"); if (success) _toaster.Success(_localizer["completed"]); else _toaster.Error(_localizer["generic-error"]); - await Load(); + await LoadAsync(); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); } } diff --git a/src/Blogifier.Admin/Pages/Newsletter/SettingsView.razor b/src/Blogifier.Admin/Pages/Newsletter/SettingsView.razor index ce7ea4f81..f7ffa8ac1 100644 --- a/src/Blogifier.Admin/Pages/Newsletter/SettingsView.razor +++ b/src/Blogifier.Admin/Pages/Newsletter/SettingsView.razor @@ -1,77 +1,86 @@ -@layout NewsletterLayout +@layout NewsletterLayout + @page "/admin/newsletter/settings/" + @inject HttpClient _http @inject IStringLocalizer _localizer +@inject IToaster _toaster - +

@_localizer["newsletter-settings"]

- @if (Mail != null) - { - - - -
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
-
- - -
-
-
- -
-
- } + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+
- @code { - protected ToasterComponent Toaster; - protected Blog Blog { get; set; } - protected MailSetting Mail { get; set; } + protected MailSettingDto Mail { get; set; } = new MailSettingDto(); - protected override async Task OnInitializedAsync() + protected override async Task OnInitializedAsync() + { + var response = await _http.GetAsync("api/mail/settings"); + if (response.IsSuccessStatusCode) { - Blog = await _http.GetFromJsonAsync("api/blog"); - Mail = await _http.GetFromJsonAsync("api/newsletter/mailsettings"); - if (Mail == null) - Mail = new MailSetting(); + var stream = await response.Content.ReadAsStreamAsync(); + if (stream.Length > 0) + { + Mail = (await JsonSerializer.DeserializeAsync(stream, BlogifierConstant.DefaultJsonSerializerOptionss))!; + } } + } - protected async Task Save() - { - Toaster.Toast(await _http.PutAsJsonAsync("api/newsletter/mailsettings", Mail)); - } + protected async Task SaveAsync() + { + Toast(await _http.PutAsJsonAsync("api/mail/settings", Mail)); + } + + protected void Toast(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } } diff --git a/src/Blogifier.Admin/Pages/Newsletter/SubscribersView.razor b/src/Blogifier.Admin/Pages/Newsletter/SubscribersView.razor index 1317cd2d3..f6aaf2d99 100644 --- a/src/Blogifier.Admin/Pages/Newsletter/SubscribersView.razor +++ b/src/Blogifier.Admin/Pages/Newsletter/SubscribersView.razor @@ -1,58 +1,67 @@ -@layout NewsletterLayout @page "/admin/newsletter/subscribers/" + +@layout NewsletterLayout + @inject HttpClient _http @inject IStringLocalizer _localizer +@inject IToaster _toaster - +

@_localizer["subscribers"]

- - @if (Subscribers == null || Subscribers.Count == 0) - { -

@_localizer["not-found"]

- } - else - { -
    - @foreach (var subscriber in Subscribers) - { -
  • - @{ - string title = $"{subscriber.Email} / {subscriber.Country} / {subscriber.Region} / {subscriber.Ip}"; - string pubDate = subscriber.CreatedAt.ToFriendlyDateTimeString(); - } - @title - @pubDate - -
  • - } -
- } + @if (Subscribers != null && Subscribers.Any()) + { +
    + @foreach (var item in Subscribers) + { +
  • + @{ + string title = $"{item.Email} / {item.Country} / {item.Region} / {item.Ip}"; + string pubDate = item.CreatedAt.ToFriendlyDateTimeString(); + } + @title + @pubDate + +
  • + } +
+ } + else + { +

@_localizer["not-found"]

+ }
- @code { - protected ToasterComponent Toaster; - protected List Subscribers; - - protected override async Task OnInitializedAsync() - { - await Load(); - } - - protected async Task Load() - { - Subscribers = await _http.GetFromJsonAsync>($"api/newsletter/subscribers"); - } - - protected async Task RemoveSubscription(int id) - { - Toaster.Toast(await _http.DeleteAsync($"api/newsletter/unsubscribe/{id}")); - await Load(); - } + + protected List? Subscribers; + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + protected async Task LoadAsync() + { + Subscribers = await _http.GetFromJsonAsync>($"api/subscriber/items"); + } + + protected async Task DeleteAsync(int id) + { + Toast(await _http.DeleteAsync($"api/subscriber/{id}")); + await LoadAsync(); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } } diff --git a/src/Blogifier.Admin/Pages/Pages/Editor.razor b/src/Blogifier.Admin/Pages/Pages/Editor.razor deleted file mode 100644 index d187af92f..000000000 --- a/src/Blogifier.Admin/Pages/Pages/Editor.razor +++ /dev/null @@ -1,210 +0,0 @@ -@page "/admin/pages/editor/{Slug?}/" -@inject HttpClient _http -@inject IStringLocalizer _localizer -@inject IJSRuntime _jsruntime -@inject IToaster _toaster -@inject NavigationManager _navigation - - - -
- @if (Post != null) - { - -
- @_localizer[ -
-
- @if (Post.Id == 0) - { - - - } - else - { - if (Post.PublishedAt > DateTime.MinValue) - { - - - - - - - - @_localizer["view"] - - } - else - { - - - - } - } -
-
-
-
- - -
- -
-
-
-
- - } -
- - -@code{ - protected Author Author { get; set; } - protected Post Post { get; set; } - protected ToasterComponent Toaster; - protected string Confirm { get; set; } - - [Parameter] -#nullable enable - public string? Slug { get; set; } -#nullable disable - - protected override async Task OnInitializedAsync() - { - Author = await _http.GetFromJsonAsync("api/author/getcurrent"); - Confirm = _localizer["confirm-delete"]; - await Load(); - } - - protected async Task Load() - { - Post = NewPost(); - - if (!string.IsNullOrEmpty(Slug)) - { - Post = await _http.GetFromJsonAsync($"api/post/byslug/{Slug}"); - var headTitle = _localizer["edit"] + " - " + Post.Title; - await _jsruntime.InvokeVoidAsync("commonJsFunctions.setTitle", headTitle); - } - } - - protected async Task SavePost(PostAction postAction) - { - Post.Content = await _jsruntime.InvokeAsync("commonJsFunctions.getEditorValue", ""); - Post.PostType = PostType.Page; - - Post.Cover = await _jsruntime.InvokeAsync("commonJsFunctions.getSrcValue", "postCover"); - if(string.IsNullOrEmpty(Post.Cover)) - Post.Cover = Constants.DefaultCover; - - if (string.IsNullOrEmpty(Post.Description)) - Post.Description = Post.Title; - - if (string.IsNullOrEmpty(Post.Title) || string.IsNullOrEmpty(Post.Content)) - { - _toaster.Error(_localizer["title-content-required"]); - return; - } - - if (postAction == PostAction.Publish) - Post.PublishedAt = DateTime.UtcNow; - - if (postAction == PostAction.Unpublish) - Post.PublishedAt = DateTime.MinValue; - - if (Post.Id == 0) - { - Post.Slug = await _http.GetStringAsync($"api/post/getslug/{Post.Title}"); - Toaster.Toast(await _http.PostAsJsonAsync($"api/post/add", Post)); - - Post = await _http.GetFromJsonAsync($"api/post/byslug/{Post.Slug}"); - - _navigation.NavigateTo($"/admin/pages/editor/{Post.Slug}"); - await Load(); - } - else - { - Toaster.Toast(await _http.PutAsJsonAsync($"api/post/update", Post)); - await Load(); - } - } - - protected async Task Save() - { - await SavePost(PostAction.Save); - } - - protected async Task Publish() - { - await SavePost(PostAction.Publish); - } - - protected async Task Unpublish() - { - await SavePost(PostAction.Unpublish); - } - - protected async Task Remove(int id) - { - if (await _jsruntime.InvokeAsync("confirm", Confirm)) - { - Toaster.Toast(await _http.DeleteAsync($"api/post/{id}")); - _navigation.NavigateTo($"admin"); - } - } - - protected async Task ResetCover() - { - Post.Cover = Constants.DefaultCover; - await Save(); - } - - protected async Task RemoveCover() - { - Post.Cover = null; - await Save(); - } - - protected Post NewPost() - { - return new Post - { - Id = 0, - Title = "", - Description = "", - Content = "", - AuthorId = Author.Id, - PostType = PostType.Page, - Cover = Constants.DefaultCover - }; - } -} diff --git a/src/Blogifier.Admin/Pages/Pages/EditorView.razor b/src/Blogifier.Admin/Pages/Pages/EditorView.razor new file mode 100644 index 000000000..41f10b742 --- /dev/null +++ b/src/Blogifier.Admin/Pages/Pages/EditorView.razor @@ -0,0 +1,199 @@ +@page "/admin/pages/editor/{Slug?}/" + +@inject HttpClient _httpClient +@inject IStringLocalizer _localizer +@inject IJSRuntime _jsruntime +@inject IToaster _toaster +@inject NavigationManager _navigation + + +
+ @if (Post != null) + { +
+ @_localizer[ +
+
+ @if (string.IsNullOrEmpty(Post.Slug)) + { + + + } + else if (Post.State >= PostState.Release) + { + + + + + + + + @_localizer["view"] + + } + else + { + + + + } +
+
+
+
+ + +
+ +
+
+
+
+ + } +
+ +@code { + + [Parameter] public string? Slug { get; set; } + + private EditorComponent _editorComponent = default!; + + protected PostEditorDto Post { get; set; } = new PostEditorDto + { + Title = string.Empty, + Description = string.Empty, + Content = string.Empty, + PostType = PostType.Page, + Cover = BlogifierConstant.DefaultCover, + Categories = new List(), + }; + + protected override async Task OnInitializedAsync() + { + if (!string.IsNullOrEmpty(Slug)) + { + Post = (await _httpClient.GetFromJsonAsync($"api/post/byslug/{Slug}"))!; + if (Post.Categories == null) Post.Categories = new List(); + await SetJsPostAsync(Post); + } + } + + protected async Task SavePostAsync(PostState postState) + { + Post.State = postState; + Post.Content = await _jsruntime.InvokeAsync("commonJsFunctions.getEditorValue", ""); + if (string.IsNullOrEmpty(Post.Title) || string.IsNullOrEmpty(Post.Content)) + { + _toaster.Error(_localizer["title-content-required"]); + return; + } + + Post.Cover = await _jsruntime.InvokeAsync("commonJsFunctions.getSrcValue", "postCover"); + Post.Cover = Post.Cover.Replace(_navigation.BaseUri, ""); + if (string.IsNullOrEmpty(Post.Cover)) Post.Cover = BlogifierConstant.DefaultCover; + + if (string.IsNullOrEmpty(Post.Description)) Post.Description = Post.Title; + + if (Post.Id == 0) + { + var response = await _httpClient.PostAsJsonAsync($"api/post/add", Post); + await CheckSetEditorResponseAsync(response); + } + else + { + var response = await _httpClient.PutAsJsonAsync($"api/post/update", Post); + await CheckSetEditorResponseAsync(response); + } + } + + protected async Task SaveAsync() + { + await SavePostAsync(PostState.Draft); + } + + protected async Task PublishAsync() + { + await SavePostAsync(PostState.Release); + } + + protected async Task UnpublishAsync() + { + await SavePostAsync(PostState.Draft); + } + + protected async Task RemoveAsync(int id) + { + if (await _jsruntime.InvokeAsync("confirm", _localizer["confirm-delete"])) + { + var result = await _httpClient.DeleteAsync($"api/post/{id}"); + if (result.IsSuccessStatusCode) _toaster.Success(_localizer["completed"]); + else _toaster.Error(_localizer["generic-error"]); + _navigation.NavigateTo($"admin"); + } + } + + protected async Task ResetCoverAsync() + { + Post.Cover = BlogifierConstant.DefaultCover; + await SaveAsync(); + } + + protected async Task RemoveCoverAsync() + { + Post.Cover = null; + await SaveAsync(); + } + + private async Task SetJsPostAsync(PostEditorDto post) + { + var headTitle = _localizer["edit"] + " - " + Post.Title; + await _jsruntime.InvokeVoidAsync("commonJsFunctions.setTitle", headTitle); + await _editorComponent.SetValueAsync(Post.Content); + } + + private async Task CheckSetEditorResponseAsync(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + var stream = await response.Content.ReadAsStreamAsync(); + Post = (await JsonSerializer.DeserializeAsync(stream, BlogifierConstant.DefaultJsonSerializerOptionss))!; + await SetJsPostAsync(Post); + _toaster.Success(_localizer["completed"]); + } + else + { + _toaster.Error(_localizer["generic-error"]); + } + } + +} diff --git a/src/Blogifier.Admin/Pages/Pages/PagesView.razor b/src/Blogifier.Admin/Pages/Pages/PagesView.razor index 06837ab1e..558d73090 100644 --- a/src/Blogifier.Admin/Pages/Pages/PagesView.razor +++ b/src/Blogifier.Admin/Pages/Pages/PagesView.razor @@ -1,268 +1,237 @@ @page "/admin/pages/" -@using Blogifier.Shared + @inject HttpClient _http +@inject IMapper _mapper +@inject NavigationManager _navigation @inject IStringLocalizer _localizer @inject IJSRuntime _jsruntime @inject IToaster _toaster -@inject NavigationManager _navigation -@inject BlogStateProvider _stateprovider -@implements IDisposable - - - -
-
- - - - @_localizer["new-page"] - - @if (Posts != null && Posts.Count > 0) - { - - } - - - - - -
- - - - @if (Posts != null && Posts.Count > 0) - { -
    - - - @{ - string pubDate = post.PublishedAt > DateTime.MinValue ? post.PublishedAt.ToFriendlyShortDateString() : _localizer["draft"]; - string pubStatus = post.PublishedAt > DateTime.MinValue ? "published" : ""; - } -
  • - - - @post.Title - - - @pubDate - - - - - - - - - -
  • -
    -
- } - else - { -
- - - - -

@_localizer["list-is-empty"]

-
- } -
@code { - protected List Posts { get; set; } - protected Author Author { get; set; } - - protected string SearchTerm { get; set; } - - protected string FilterLabel { get; set; } - protected PublishedStatus FilterValue { get; set; } - - protected string PostTypeLabel { get; set; } - protected string PostTypeButton { get; set; } - protected override async Task OnInitializedAsync() - { - Author = await _http.GetFromJsonAsync("api/author/getcurrent"); - - FilterLabel = _localizer["all"]; - PostTypeLabel = _localizer["posts"]; - PostTypeButton = _localizer["post"]; + protected List? Posts { get; set; } + protected string? SearchTerm { get; set; } + protected PublishedStatus FilterValue { get; set; } = PublishedStatus.All; - _stateprovider.OnChange += StateHasChanged; - _stateprovider.PostType = PostType.Post; + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } - await Load(); - } + protected async Task LoadAsync() + { + var result = await _http.GetFromJsonAsync>($"api/post/items/{(int)FilterValue}/{(int)PostType.Page}"); + Posts = _mapper.Map>(result); + } - protected async Task Load() + public void CheckAll(object? checkValue) + { + if (checkValue != null) { - Posts = await _http.GetFromJsonAsync>($"api/post/list/{FilterValue}/{PostType.Page}"); + var isChecked = (bool)checkValue; + foreach (var item in Posts!) + { + item.Selected = isChecked; + } + StateHasChanged(); } + } - public void CheckAll(object checkValue) + private async Task RunAction(GroupAction action) + { + if (action == GroupAction.Delete) { - bool isChecked = (bool)checkValue; - Posts.ForEach(p => p.Selected = isChecked); - StateHasChanged(); + var confirmed = await _jsruntime.InvokeAsync("confirm", _localizer["confirm-delete"]); + if (!confirmed) return; } - public async Task RunAction(GroupAction action) + if (Posts != null) { - string confirm = _localizer["confirm-delete"]; - bool confirmed = false; - - if (action == GroupAction.Delete) - { - confirmed = await _jsruntime.InvokeAsync("confirm", confirm); - if (!confirmed) - return; - } - - foreach (var post in Posts) + var ids = Posts.Where(m => m.Selected).Select(m => m.Id); + if (ids.Any()) + { + var idsString = string.Join(",", ids); + switch (action) { - if (post.Selected) - { - switch (action) - { - case GroupAction.Publish: - await _http.PutAsJsonAsync($"api/post/publish/{post.Id}", true); - break; - case GroupAction.Unpublish: - await _http.PutAsJsonAsync($"api/post/publish/{post.Id}", false); - break; - case GroupAction.Delete: - await _http.DeleteAsync($"api/post/{post.Id}"); - break; - } - } + case GroupAction.Publish: + await _http.PutAsJsonAsync($"api/post/state/{idsString}", PostState.Release); + break; + case GroupAction.Unpublish: + await _http.PutAsJsonAsync($"api/post/state/{idsString}", PostState.Draft); + break; + case GroupAction.Delete: + await _http.DeleteAsync($"api/post/{idsString}"); + break; } - await Load(); + } } + await LoadAsync(); + } + + protected async Task SearchKeyPress(KeyboardEventArgs e) + { + if (e.Key == "Enter") + await SearchPosts(); + } + + protected async Task SearchPosts() + { + if (string.IsNullOrEmpty(SearchTerm)) SearchTerm = "*"; + var result = await _http.GetFromJsonAsync>($"api/post/items/search/{SearchTerm}"); + Posts = _mapper.Map>(result); + } + + public async Task Filter(PublishedStatus filter) + { + FilterValue = filter; + await LoadAsync(); + } + + public async Task Publish(PostItemDto post) + { + Toast(await _http.PutAsJsonAsync($"api/post/state/{post.Id}", PostState.Release)); + await LoadAsync(); + } + + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } +} - protected async Task SearchKeyPress(KeyboardEventArgs e) - { - if (e.Key == "Enter") - await SearchPosts(); - } - protected async Task SearchPosts() - { - if (string.IsNullOrEmpty(SearchTerm)) - SearchTerm = "*"; + - Posts = await _http.GetFromJsonAsync>($"api/post/list/search/{SearchTerm}"); - SearchTerm = ""; - } +
+
- protected async Task GetPages() - { - PostTypeLabel = _localizer["pages"]; - PostTypeButton = _localizer["page"]; - _stateprovider.SetPostType(PostType.Page); - await Load(); - } + - public async Task Filter(PublishedStatus filter) - { - FilterValue = filter; - switch (filter) - { - case PublishedStatus.Published: - FilterLabel = _localizer["published"]; - break; - case PublishedStatus.Drafts: - FilterLabel = _localizer["draft", true]; - break; - default: - FilterLabel = _localizer["all"]; - break; - } - await Load(); - } + @_localizer["new-page"] - public async Task Publish(Post post) + @if (Posts != null && Posts.Count > 0) { - Toast(await _http.PutAsJsonAsync($"api/post/publish/{post.Id}", (post.PublishedAt == DateTime.MinValue))); - await Load(); + } - protected void Toast(HttpResponseMessage msg) - { - if (msg.IsSuccessStatusCode) - _toaster.Success(_localizer["completed"]); - else - _toaster.Error(_localizer["generic-error"]); - } + - protected void Open() - { - } + + +
+ + + + @if (Posts != null && Posts.Count > 0) + { +
    + +
  • + + + @post.Title + + + @DateTimeHelper.ToFriendlyShortDateString(post.PublishedAt, _localizer["draft"]) + + + + + + + + + +
  • +
    +
+ } + else + { +
+ + + + +

@_localizer["list-is-empty"]

+
+ } +
- public void Dispose() - { - _stateprovider.OnChange -= StateHasChanged; - } -} diff --git a/src/Blogifier.Admin/Pages/Settings/AboutView.razor b/src/Blogifier.Admin/Pages/Settings/AboutView.razor index afc3a6e75..8dad57b74 100644 --- a/src/Blogifier.Admin/Pages/Settings/AboutView.razor +++ b/src/Blogifier.Admin/Pages/Settings/AboutView.razor @@ -1,72 +1,74 @@ -@layout SettingsLayout @page "/admin/settings/about/" + +@layout SettingsLayout + @inject HttpClient _http @inject IStringLocalizer _localizer -@inject IJSRuntime JSRuntime - +@code { + protected AboutDto? About { get; set; } + protected override async Task OnInitializedAsync() + { + About = await _http.GetFromJsonAsync($"api/blog/about"); + } +} + + +

@_localizer["about-blogifier"]

- -@if (AboutModel != null) +@if (About != null) { -

@_localizer["specification"]

-
-
    -
  • - -
    @AboutModel.Version
    -
  • -
  • - -
    @AboutModel.OperatingSystem
    -
  • -
  • - -
    @AboutModel.DatabaseProvider
    -
  • -
-
+

@_localizer["specification"]

+
+
    +
  • + +
    @About.Version
    +
  • +
  • + +
    @About.OperatingSystem
    +
  • +
  • + +
    @About.DatabaseProvider
    +
  • +
+
} -@code { - protected AboutModel AboutModel { get; set; } - - protected override async Task OnInitializedAsync() - { - AboutModel = await _http.GetFromJsonAsync($"api/about"); - } -} diff --git a/src/Blogifier.Admin/Pages/Settings/AdvancedView.razor b/src/Blogifier.Admin/Pages/Settings/AdvancedView.razor index 095404f4f..9d55bf830 100644 --- a/src/Blogifier.Admin/Pages/Settings/AdvancedView.razor +++ b/src/Blogifier.Admin/Pages/Settings/AdvancedView.razor @@ -1,14 +1,15 @@ @layout SettingsLayout @page "/admin/settings/advanced/" @inject IStringLocalizer _localizer - + +

@_localizer["advanced-settings"]

-
- @_localizer["under-development"]. - - @_localizer["more-info"] - -
+
+ @_localizer["under-development"]. + + @_localizer["more-info"] + +
diff --git a/src/Blogifier.Admin/Pages/Settings/BasicView.razor b/src/Blogifier.Admin/Pages/Settings/BasicView.razor index 7a49817ee..f0550ea33 100644 --- a/src/Blogifier.Admin/Pages/Settings/BasicView.razor +++ b/src/Blogifier.Admin/Pages/Settings/BasicView.razor @@ -1,49 +1,57 @@ -@layout SettingsLayout -@page "/admin/settings/" +@layout SettingsLayout +@page "/admin/settings" @inject HttpClient _http +@inject IToaster _toaster @inject IStringLocalizer _localizer - + +

@_localizer["basic-settings"]

- @if (Blog != null) - { - - - -
- - -
-
- - -
-
- - -
-
- -
-
- } + @if (Blog != null) + { + + + +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ }
- @code { - protected Blog Blog { get; set; } - protected ToasterComponent Toaster; + protected BlogEitorDto? Blog { get; set; } + + protected override async Task OnInitializedAsync() + { + Blog = await _http.GetFromJsonAsync("api/blog"); + } - protected override async Task OnInitializedAsync() - { - Blog = await _http.GetFromJsonAsync("api/blog"); - } + protected async Task Save() + { + Toast(await _http.PutAsJsonAsync("api/blog", Blog!)); + } - protected async Task Save() - { - Toaster.Toast(await _http.PutAsJsonAsync("api/blog", Blog)); - } + protected void Toast(HttpResponseMessage msg) + { + if (msg.IsSuccessStatusCode) + _toaster.Success(_localizer["completed"]); + else + _toaster.Error(_localizer["generic-error"]); + } } diff --git a/src/Blogifier.Admin/Pages/Settings/CommentsView.razor b/src/Blogifier.Admin/Pages/Settings/CommentsView.razor index 446fbcb37..44b70f5a0 100644 --- a/src/Blogifier.Admin/Pages/Settings/CommentsView.razor +++ b/src/Blogifier.Admin/Pages/Settings/CommentsView.razor @@ -4,14 +4,14 @@ @inject IStringLocalizer _localizer @inject IToaster _toaster - +

@_localizer["comments-settings"]

-
- @_localizer["under-development"]. - - @_localizer["more-info"] - -
+
+ @_localizer["under-development"]. + + @_localizer["more-info"] + +
diff --git a/src/Blogifier.Admin/Pages/Settings/CustomizeView.razor b/src/Blogifier.Admin/Pages/Settings/CustomizeView.razor index e5764664f..ef1f99d0e 100644 --- a/src/Blogifier.Admin/Pages/Settings/CustomizeView.razor +++ b/src/Blogifier.Admin/Pages/Settings/CustomizeView.razor @@ -1,27 +1,22 @@ -@layout SettingsLayout @page "/admin/settings/customize/" + +@layout SettingsLayout + @inject HttpClient _http @inject IStringLocalizer _localizer -@inject IJSRuntime JSRuntime +@inject IJSRuntime _jsRuntime +@inject IToaster _toaster - + -

@_localizer["customization"]

+

@_localizer["menus"]

@_localizer["theme-customization-desc"]

- - - +
+ @_localizer["under-development"]. + + @_localizer["more-info"] + +
- - -@code { - protected ToasterComponent Toaster; - SettingsComponent ChildSettings; - - async Task SaveSettings() - { - Toaster.Toast(await ChildSettings.Save()); - } -} diff --git a/src/Blogifier.Admin/Pages/Settings/MenusView.razor b/src/Blogifier.Admin/Pages/Settings/MenusView.razor index c2cb73184..17a1e737b 100644 --- a/src/Blogifier.Admin/Pages/Settings/MenusView.razor +++ b/src/Blogifier.Admin/Pages/Settings/MenusView.razor @@ -1,15 +1,16 @@ @layout SettingsLayout @page "/admin/settings/customize/menus/" @inject IStringLocalizer _localizer - + +

@_localizer["menus"]

@_localizer["theme-customization-desc"]

-
- @_localizer["under-development"]. - - @_localizer["more-info"] - -
+
+ @_localizer["under-development"]. + + @_localizer["more-info"] + +
diff --git a/src/Blogifier.Admin/Pages/Settings/ScriptsView.razor b/src/Blogifier.Admin/Pages/Settings/ScriptsView.razor index e11501a0c..2c72d77ad 100644 --- a/src/Blogifier.Admin/Pages/Settings/ScriptsView.razor +++ b/src/Blogifier.Admin/Pages/Settings/ScriptsView.razor @@ -1,44 +1,51 @@ @layout SettingsLayout @page "/admin/settings/scripts/" @inject HttpClient _http +@inject IToaster _toaster @inject IStringLocalizer _localizer - +

@_localizer["script-settings"]

@_localizer["include-scripts"]

- @if (Blog != null) - { - -
- - + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+ + diff --git a/src/Blogifier/Views/Themes/standard/layouts/_account.cshtml b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_account.cshtml similarity index 76% rename from src/Blogifier/Views/Themes/standard/layouts/_account.cshtml rename to src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_account.cshtml index 020d74022..51c5a06a8 100644 --- a/src/Blogifier/Views/Themes/standard/layouts/_account.cshtml +++ b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_account.cshtml @@ -8,11 +8,19 @@ @await RenderSectionAsync("HeadMeta",false) - +
+ -
- + diff --git a/src/Blogifier/Views/Themes/standard/layouts/_base.cshtml b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_base.cshtml similarity index 61% rename from src/Blogifier/Views/Themes/standard/layouts/_base.cshtml rename to src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_base.cshtml index 07caff881..307159b3d 100644 --- a/src/Blogifier/Views/Themes/standard/layouts/_base.cshtml +++ b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_base.cshtml @@ -24,16 +24,12 @@ --bf-radius: .5rem; } - + - @RenderBody() - - - - + diff --git a/src/Blogifier/Views/Themes/standard/layouts/_main.cshtml b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_main.cshtml similarity index 54% rename from src/Blogifier/Views/Themes/standard/layouts/_main.cshtml rename to src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_main.cshtml index d74e9ef8e..de7ca6893 100644 --- a/src/Blogifier/Views/Themes/standard/layouts/_main.cshtml +++ b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_main.cshtml @@ -1,4 +1,4 @@ -@model IndexModel +@model MainModel @@ -6,10 +6,10 @@ - @Model.Title - - - + @Model.Main.Title + + + - - @Html.Raw(Model.HeaderScript) + + @Html.Raw(Model.Main.HeaderScript) @RenderBody() - - - - - @Html.Raw(Model.FooterScript) + + @Html.Raw(Model.Main.FooterScript) diff --git a/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_profile.cshtml b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_profile.cshtml new file mode 100644 index 000000000..48e1f3114 --- /dev/null +++ b/src/Blogifier.Themes.Standard/Views/Themes/standard/layouts/_profile.cshtml @@ -0,0 +1,60 @@ +@model AccountProfileModel +@inject IStringLocalizer _localizer + + + + + + + + @await RenderSectionAsync("HeadMeta",false) + + + + + +
+ +
+
+ +
+ @RenderBody() +
+
+
+
+ + + + diff --git a/src/Blogifier/Views/Themes/standard/login.cshtml b/src/Blogifier.Themes.Standard/Views/Themes/standard/login.cshtml similarity index 100% rename from src/Blogifier/Views/Themes/standard/login.cshtml rename to src/Blogifier.Themes.Standard/Views/Themes/standard/login.cshtml diff --git a/src/Blogifier/Views/Themes/standard/Page.cshtml b/src/Blogifier.Themes.Standard/Views/Themes/standard/page.cshtml similarity index 80% rename from src/Blogifier/Views/Themes/standard/Page.cshtml rename to src/Blogifier.Themes.Standard/Views/Themes/standard/page.cshtml index 1801eef89..fa9b93cad 100644 --- a/src/Blogifier/Views/Themes/standard/Page.cshtml +++ b/src/Blogifier.Themes.Standard/Views/Themes/standard/page.cshtml @@ -1,27 +1,23 @@ -@using Blogifier.Shared.Resources -@using Blogifier.Shared -@using Microsoft.Extensions.Localization + +@model PostModel @inject IStringLocalizer _localizer @{ Layout = "layouts/_main.cshtml"; - var postModel = (PostModel)Model; - var post = postModel.Post; - var cover = $"{Url.Content("~/")}{postModel.Post.Cover}"; }