diff --git a/src/.gitignore b/src/.gitignore index 8a30d258e..3cd9d7922 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -3,6 +3,12 @@ ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +# Client packages (bootstrap, etc.) +lib/ + +# Compiled css +/FluentCMS.Web.UI/wwwroot/css/*.css + # User-specific files *.rsuser *.suo diff --git a/src/FluentCMS.Domain/FluentCMS.Domain.csproj b/src/FluentCMS.Domain/FluentCMS.Domain.csproj index e9ca38176..b487aaa8d 100644 --- a/src/FluentCMS.Domain/FluentCMS.Domain.csproj +++ b/src/FluentCMS.Domain/FluentCMS.Domain.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 enable enable FluentCMS diff --git a/src/FluentCMS.Repository.LiteDb.Tests/FluentCMS.Repository.LiteDb.Tests.csproj b/src/FluentCMS.Repository.LiteDb.Tests/FluentCMS.Repository.LiteDb.Tests.csproj index 0c76e4951..70059f85e 100644 --- a/src/FluentCMS.Repository.LiteDb.Tests/FluentCMS.Repository.LiteDb.Tests.csproj +++ b/src/FluentCMS.Repository.LiteDb.Tests/FluentCMS.Repository.LiteDb.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable diff --git a/src/FluentCMS.Repository.LiteDb/FluentCMS.Repository.LiteDb.csproj b/src/FluentCMS.Repository.LiteDb/FluentCMS.Repository.LiteDb.csproj index 9572bce5d..f2f5e8979 100644 --- a/src/FluentCMS.Repository.LiteDb/FluentCMS.Repository.LiteDb.csproj +++ b/src/FluentCMS.Repository.LiteDb/FluentCMS.Repository.LiteDb.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 enable enable diff --git a/src/FluentCMS.Shared/ConfigurationExtensions.cs b/src/FluentCMS.Shared/ConfigurationExtensions.cs new file mode 100644 index 000000000..1a269a9c6 --- /dev/null +++ b/src/FluentCMS.Shared/ConfigurationExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.Configuration; + +public static class ConfigurationExtensions +{ + public static IConfigurationBuilder AddConfig(this IConfigurationBuilder configBuilder, IHostEnvironment env, string configFolder = "\\Config\\") + { + var folderName = env.ContentRootPath + configFolder; + + // setting base path for config folder + configBuilder.SetBasePath(folderName); + + foreach (var filename in Directory.GetFiles(folderName)) + { + // read only json files + if (Path.GetExtension(filename) != ".json") + continue; + + // accept only root config files + if (Path.GetFileName(filename).Split(".").Length != 2) + continue; + + // adding config file + configBuilder.AddJsonFile(filename, optional: true, reloadOnChange: true) + .AddJsonFile($"{Path.GetFileNameWithoutExtension(filename)}.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + } + + return configBuilder; + } + + public static T GetInstance(this IConfiguration configuration, string sectionName) + { + return configuration.GetSection(sectionName).Get(); + } +} \ No newline at end of file diff --git a/src/FluentCMS.Shared/Controllers/BaseController.cs b/src/FluentCMS.Shared/Controllers/BaseController.cs new file mode 100644 index 000000000..78edac8f4 --- /dev/null +++ b/src/FluentCMS.Shared/Controllers/BaseController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace FluentCMS.Web.Controllers; + +[Route("api/[controller]/[action]/")] +[ApiController] +[Produces("application/json")] +public abstract class BaseController : ControllerBase +{ + +} \ No newline at end of file diff --git a/src/FluentCMS.Shared/Controllers/WeatherForecastController.cs b/src/FluentCMS.Shared/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..7dc025f9a --- /dev/null +++ b/src/FluentCMS.Shared/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using FluentCMS.Models; +using Microsoft.AspNetCore.Mvc; + +namespace FluentCMS.Web.Controllers; + +public class WeatherForecastController : BaseController +{ + private static readonly string[] Summaries = + [ + "Freezing", + "Bracing", + "Chilly", + "Cool", + "Mild", + "Warm", + "Balmy", + "Hot", + "Sweltering", + "Scorching" + ]; + + [HttpGet] + public IEnumerable GetAll() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/src/FluentCMS.Shared/FluentCMS.Shared.csproj b/src/FluentCMS.Shared/FluentCMS.Shared.csproj new file mode 100644 index 000000000..8046e9686 --- /dev/null +++ b/src/FluentCMS.Shared/FluentCMS.Shared.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/src/FluentCMS.Shared/Models/WeatherForecast.cs b/src/FluentCMS.Shared/Models/WeatherForecast.cs new file mode 100644 index 000000000..d7e00741e --- /dev/null +++ b/src/FluentCMS.Shared/Models/WeatherForecast.cs @@ -0,0 +1,9 @@ +namespace FluentCMS.Models; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/src/FluentCMS.Shared/SwaggerServiceExtensions.cs b/src/FluentCMS.Shared/SwaggerServiceExtensions.cs new file mode 100644 index 000000000..ad680f4b0 --- /dev/null +++ b/src/FluentCMS.Shared/SwaggerServiceExtensions.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.OpenApi.Models; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class SwaggerServiceExtensions +{ + private static string _applicationName = string.Empty; + private static string _applicationVersion = string.Empty; + + public static IServiceCollection AddApiDocumentation(this IServiceCollection services, string applicationName = "FluentCMS API", string version = "v.1.0.0") + { + _applicationName = applicationName; + _applicationVersion = version; + + services.AddEndpointsApiExplorer(); + + services.AddSwaggerGen(c => + { + c.OrderActionsBy(x => x.RelativePath); + c.SwaggerDoc("v1", new OpenApiInfo { Title = applicationName, Version = version }); + }); + + //services.AddSwaggerGenNewtonsoftSupport(); + + return services; + + } + + public static IApplicationBuilder UseApiDocumentation(this IApplicationBuilder app, string routePrefix = "doc") + { + // Enable middleware to serve generated Swagger as a JSON endpoint. + app.UseSwagger(); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), + // specifying the Swagger JSON endpoint. + app.UseSwaggerUI(c => + { + c.DisplayRequestDuration(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", _applicationName + ", Version " + _applicationVersion); + c.RoutePrefix = routePrefix; + }); + + return app; + } +} \ No newline at end of file diff --git a/src/FluentCMS.Web.UI/App.razor b/src/FluentCMS.Web.UI/App.razor new file mode 100644 index 000000000..aa25b84e3 --- /dev/null +++ b/src/FluentCMS.Web.UI/App.razor @@ -0,0 +1,15 @@ + + + + + + + FluentCMS + + + + + + + + diff --git a/src/FluentCMS.Web.UI/Components/Badge.razor b/src/FluentCMS.Web.UI/Components/Badge.razor new file mode 100644 index 000000000..ad1ac6394 --- /dev/null +++ b/src/FluentCMS.Web.UI/Components/Badge.razor @@ -0,0 +1,12 @@ +@inherits BaseComponent +@{ + base.BuildRenderTree(__builder); +} +@code { + [Parameter] + [CssProperty] + public Colors Color { get; set; } = Colors.Primary; + + [Parameter] + public override string Tag { get; set; } = "span"; +} diff --git a/src/FluentCMS.Web.UI/Components/Base/BaseComponent.cs b/src/FluentCMS.Web.UI/Components/Base/BaseComponent.cs new file mode 100644 index 000000000..fec399fdf --- /dev/null +++ b/src/FluentCMS.Web.UI/Components/Base/BaseComponent.cs @@ -0,0 +1,142 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace FluentCMS.Web.UI.Components; + +// All components will be inherited from this class +public partial class BaseComponent : ComponentBase +{ + static int _counter = 0; + public BaseComponent() + { + _counter++; + } + + public const string DEFAULT_PREFIX = "f"; + + [Parameter(CaptureUnmatchedValues = true)] + public virtual Dictionary Attributes { get; set; } = []; + + [Parameter] + public virtual string Class { get; set; } = string.Empty; + + [Parameter] + public virtual string Tag { get; set; } = "div"; + + [Parameter] + public virtual string Prefix { get; set; } = DEFAULT_PREFIX; + + [Parameter] + public virtual RenderFragment? ChildContent { get; set; } + + [Parameter] + public ElementReference? Ref { get; set; } + + [Parameter] + [Html] + public virtual string? Id { get; set; } + + [Parameter] + public virtual string? UniqueName { get; set; } + + [Parameter] + public EventCallback RefChanged { get; set; } + + protected override Task OnInitializedAsync() + { + if (string.IsNullOrEmpty(UniqueName)) + UniqueName = GetType().Name; + + if (string.IsNullOrEmpty(Id)) + Id = $"{UniqueName}-{_counter}"; + + return base.OnInitializedAsync(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + //base.BuildRenderTree(builder); + + var _cssClasses = $"{Prefix}-{UniqueName?.PascalToKebabCase()} {GetCssClasses()}"; + if (!string.IsNullOrEmpty(Class)) + _cssClasses += $" {Class}"; + + builder.OpenElement(0, Tag); + + if (Attributes?.Any() == true) + builder.AddMultipleAttributes(1, Attributes); + + builder.AddMultipleAttributes(1, GetHtmlAttributes()); + + builder.AddAttribute(2, "class", $"{_cssClasses}"); + + if (Ref != null) + { + builder.AddElementReferenceCapture(3, async capturedRef => + { + Ref = capturedRef; + await RefChanged.InvokeAsync(Ref.Value); + }); + } + + builder.AddContent(4, ChildContent); + + builder.CloseElement(); + } + + // This function will return an space separated string for generate CSS classes + // It uses reflection to get all properties which have CssProperty attribute and their values + private string GetCssClasses() + { + var _dict = GetCssProps(); + + if (_dict == null || _dict.Count == 0) + return string.Empty; + + var _cssClasses = string.Empty; + + var x = _dict + .Where(x => !string.IsNullOrEmpty(x.Value)) + .Select(item => $"{Prefix}-{UniqueName?.PascalToKebabCase()}-{item.Key.PascalToKebabCase()}-{item.Value.PascalToKebabCase()}"); + + return string.Join(" ", x); + + } + + // This is a function which returns a dictionary of property name and its value + // It will return the properties with CssProperty attribute + private Dictionary GetCssProps() + { + var _dict = new Dictionary(); + + var properties = GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(CssPropertyAttribute))); + foreach (var property in properties) + { + var propertyName = property.Name; + var propertyValue = property.GetValue(this) ?? string.Empty; + + _dict.Add(propertyName, propertyValue.ToString()); + } + return _dict; + } + + // This is a function which returns a dictionary of property name and its value + // It will return the properties with CssProperty attribute + private Dictionary GetHtmlAttributes() + { + var _dict = new Dictionary(); + + var properties = GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(HtmlAttribute))); + foreach (var property in properties) + { + var propertyName = property.Name; + var propertyValue = property.GetValue(this) ?? string.Empty; + + if (propertyValue is bool && (bool)propertyValue == false) + continue; + + _dict.Add(propertyName.PascalToKebabCase(), propertyValue.ToString().PascalToKebabCase()); + } + return _dict; + } +} diff --git a/src/FluentCMS.Web.UI/Components/Base/Colors.cs b/src/FluentCMS.Web.UI/Components/Base/Colors.cs new file mode 100644 index 000000000..7cd09cf0c --- /dev/null +++ b/src/FluentCMS.Web.UI/Components/Base/Colors.cs @@ -0,0 +1,15 @@ +namespace FluentCMS.Web.UI.Components; + +public enum Colors +{ + Primary, + Secondary, + Success, + Danger, + Warning, + Info, + Light, + Dark, + White, + Transparent +} diff --git a/src/FluentCMS.Web.UI/Components/Base/CssPropertyAttribute.cs b/src/FluentCMS.Web.UI/Components/Base/CssPropertyAttribute.cs new file mode 100644 index 000000000..27d821e4a --- /dev/null +++ b/src/FluentCMS.Web.UI/Components/Base/CssPropertyAttribute.cs @@ -0,0 +1,14 @@ +namespace FluentCMS.Web.UI.Components; + +/// +/// This attribute is used to mark a property as a CSS property. +/// This is used to generate the CSS properties for the component. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class CssPropertyAttribute : Attribute +{ + public CssPropertyAttribute() + { + + } +} diff --git a/src/FluentCMS.Web.UI/Components/Base/HtmlAttribute.cs b/src/FluentCMS.Web.UI/Components/Base/HtmlAttribute.cs new file mode 100644 index 000000000..0aa258c3f --- /dev/null +++ b/src/FluentCMS.Web.UI/Components/Base/HtmlAttribute.cs @@ -0,0 +1,13 @@ +namespace FluentCMS.Web.UI.Components; + +/// +/// This attribute is used to mark a property being rendered as HTML attribute. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class HtmlAttribute : Attribute +{ + public HtmlAttribute() + { + + } +} diff --git a/src/FluentCMS.Web.UI/Components/Base/StringExtensions.cs b/src/FluentCMS.Web.UI/Components/Base/StringExtensions.cs new file mode 100644 index 000000000..af69b5585 --- /dev/null +++ b/src/FluentCMS.Web.UI/Components/Base/StringExtensions.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; + +namespace FluentCMS.Web.UI.Components; + +public static class StringExtensions +{ + public static string PascalToKebabCase(this string value) + { + if (string.IsNullOrEmpty(value)) + return value; + + return Regex.Replace( + value, + "(? + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/src/FluentCMS.Web.UI/Layout/MainLayout.razor b/src/FluentCMS.Web.UI/Layout/MainLayout.razor new file mode 100644 index 000000000..3be5523c0 --- /dev/null +++ b/src/FluentCMS.Web.UI/Layout/MainLayout.razor @@ -0,0 +1,16 @@ +@inherits LayoutComponentBase + + + +
+
+ +
+ @Body +
+
+
diff --git a/src/FluentCMS.Web.UI/Layout/Sidebar.razor b/src/FluentCMS.Web.UI/Layout/Sidebar.razor new file mode 100644 index 000000000..78ec540c3 --- /dev/null +++ b/src/FluentCMS.Web.UI/Layout/Sidebar.razor @@ -0,0 +1,26 @@ + + +@code { + +} diff --git a/src/FluentCMS.Web.UI/Pages/Counter.razor b/src/FluentCMS.Web.UI/Pages/Counter.razor new file mode 100644 index 000000000..ef23cb316 --- /dev/null +++ b/src/FluentCMS.Web.UI/Pages/Counter.razor @@ -0,0 +1,18 @@ +@page "/counter" + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/src/FluentCMS.Web.UI/Pages/Error.razor b/src/FluentCMS.Web.UI/Pages/Error.razor new file mode 100644 index 000000000..ee6f5865a --- /dev/null +++ b/src/FluentCMS.Web.UI/Pages/Error.razor @@ -0,0 +1,35 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] public HttpContext? HttpContext { get; set; } + + public string? RequestId { get; set; } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/FluentCMS.Web.UI/Pages/Home.razor b/src/FluentCMS.Web.UI/Pages/Home.razor new file mode 100644 index 000000000..0aa4963d4 --- /dev/null +++ b/src/FluentCMS.Web.UI/Pages/Home.razor @@ -0,0 +1,11 @@ +@page "/" + +Home +Call Weather Api + + + + + +Default +Danger diff --git a/src/FluentCMS.Web.UI/Pages/Weather.razor b/src/FluentCMS.Web.UI/Pages/Weather.razor new file mode 100644 index 000000000..a8595cdfe --- /dev/null +++ b/src/FluentCMS.Web.UI/Pages/Weather.razor @@ -0,0 +1,60 @@ +@page "/weather" +@inject HttpClient _http; + +@using FluentCMS.Models + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (forecasts == null) +{ +

Calling Web Api: @_http.BaseAddress@endpoint

+} +else +{ + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + private List? forecasts; + private string endpoint = "WeatherForecast/GetAll"; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate a loading indicator + await Task.Delay(1000); + + try + { + forecasts = await _http.GetFromJsonAsync>(endpoint); + } + catch (Exception ex) + { + throw; + } + } + +} diff --git a/src/FluentCMS.Web.UI/Program.cs b/src/FluentCMS.Web.UI/Program.cs new file mode 100644 index 000000000..68ab18939 --- /dev/null +++ b/src/FluentCMS.Web.UI/Program.cs @@ -0,0 +1,61 @@ +using FluentCMS; +using FluentCMS.Repository.LiteDb; +using FluentCMS.Web.UI; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddConfig(builder.Environment); + +var services = builder.Services; + +// add FluentCms core +services.AddFluentCMSCore() + .AddLiteDbRepository(b => b.SetFilePath( + builder.Configuration.GetConnectionString("LiteDbFile") ?? throw new Exception("LiteDb file not defined."))); + +// Add services to the container. +services.AddRazorComponents() + .AddInteractiveServerComponents(); + +services.AddControllers(); + +services.AddApiDocumentation(); + +services.AddHttpContextAccessor(); + +services.AddScoped(sp => +{ + var httpContextAccessor = sp.GetRequiredService(); + var request = httpContextAccessor?.HttpContext?.Request; + var domain = string.Format("{0}://{1}/api/", request?.Scheme, request?.Host.Value); + var baseAddress = new Uri(domain); + + return new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }) + { + BaseAddress = new Uri(domain), + }; +}); + + +var app = builder.Build(); + +app.UseExceptionHandler("/Error", createScopeForErrors: true); + +//app.UseHsts(); + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); + +app.UseAntiforgery(); + +app.UseApiDocumentation(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/src/FluentCMS.Web.UI/Properties/launchSettings.json b/src/FluentCMS.Web.UI/Properties/launchSettings.json new file mode 100644 index 000000000..596a944d2 --- /dev/null +++ b/src/FluentCMS.Web.UI/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3240", + "sslPort": 44362 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5120", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7164;http://localhost:5120", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/src/FluentCMS.Web.UI/Routes.razor b/src/FluentCMS.Web.UI/Routes.razor new file mode 100644 index 000000000..d0df78161 --- /dev/null +++ b/src/FluentCMS.Web.UI/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/FluentCMS.Web.UI/_Imports.razor b/src/FluentCMS.Web.UI/_Imports.razor new file mode 100644 index 000000000..09be33f41 --- /dev/null +++ b/src/FluentCMS.Web.UI/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using FluentCMS.Web.UI +@using FluentCMS.Web.UI.Components +@using FluentCMS.Web.UI.Layout diff --git a/src/FluentCMS.Web.UI/compilerconfig.json b/src/FluentCMS.Web.UI/compilerconfig.json new file mode 100644 index 000000000..36db12af9 --- /dev/null +++ b/src/FluentCMS.Web.UI/compilerconfig.json @@ -0,0 +1,6 @@ +[ + { + "outputFile": "wwwroot/css/App.css", + "inputFile": "wwwroot/css/App.scss" + } +] \ No newline at end of file diff --git a/src/FluentCMS.Web.UI/compilerconfig.json.defaults b/src/FluentCMS.Web.UI/compilerconfig.json.defaults new file mode 100644 index 000000000..41870cc54 --- /dev/null +++ b/src/FluentCMS.Web.UI/compilerconfig.json.defaults @@ -0,0 +1,71 @@ +{ + "compilers": { + "less": { + "autoPrefix": "", + "cssComb": "none", + "ieCompat": true, + "math": null, + "strictMath": false, + "strictUnits": false, + "relativeUrls": true, + "rootPath": "", + "sourceMapRoot": "", + "sourceMapBasePath": "", + "sourceMap": false + }, + "sass": { + "autoPrefix": "", + "loadPaths": "", + "style": "expanded", + "relativeUrls": true, + "sourceMap": false + }, + "nodesass": { + "autoPrefix": "", + "includePath": "", + "indentType": "space", + "indentWidth": 2, + "outputStyle": "nested", + "precision": 5, + "relativeUrls": true, + "sourceMapRoot": "", + "lineFeed": "", + "sourceMap": false + }, + "stylus": { + "sourceMap": false + }, + "babel": { + "sourceMap": false + }, + "coffeescript": { + "bare": false, + "runtimeMode": "node", + "sourceMap": false + }, + "handlebars": { + "root": "", + "noBOM": false, + "name": "", + "namespace": "", + "knownHelpersOnly": false, + "forcePartial": false, + "knownHelpers": [], + "commonjs": "", + "amd": false, + "sourceMap": false + } + }, + "minifiers": { + "css": { + "enabled": true, + "termSemicolons": true, + "gzip": false + }, + "javascript": { + "enabled": true, + "termSemicolons": true, + "gzip": false + } + } +} \ No newline at end of file diff --git a/src/FluentCMS.Web.UI/libman.json b/src/FluentCMS.Web.UI/libman.json new file mode 100644 index 000000000..376b7bc60 --- /dev/null +++ b/src/FluentCMS.Web.UI/libman.json @@ -0,0 +1,12 @@ +{ + // See more detail here: + // https://stackoverflow.com/questions/67775732/how-to-use-bootstrap-5-sass-on-visual-studio-2019 + "version": "1.0", + "defaultProvider": "unpkg", + "libraries": [ + { + "library": "bootstrap@5.3.2", + "destination": "wwwroot/lib/bootstrap/" + } + ] +} \ No newline at end of file diff --git a/src/FluentCMS.Web.UI/wwwroot/css/App.scss b/src/FluentCMS.Web.UI/wwwroot/css/App.scss new file mode 100644 index 000000000..23363d3a6 --- /dev/null +++ b/src/FluentCMS.Web.UI/wwwroot/css/App.scss @@ -0,0 +1,56 @@ +// Default prefix for component's CSS classes +$prefix: 'f-'; + +// bootstrap main SCSS file +@import "../lib/bootstrap/scss/bootstrap"; + +// Custom Components +@import './components/badge'; + +nav.navbar { + background: linear-gradient($primary, $white) +} + +/* + * Sidebar + */ + +@media (min-width: 768px) { + .sidebar .offcanvas-lg { + position: -webkit-sticky; + position: sticky; + top: 48px; + } + + .navbar-search { + display: block; + } +} + +.sidebar .nav-link { + font-size: .875rem; + font-weight: 500; +} + +.sidebar .nav-link.active { + color: #2470dc; +} + +.sidebar-heading { + font-size: .75rem; +} + +/* + * Navbar + */ + +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + background-color: rgba(0, 0, 0, .25); + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); +} + +.navbar .form-control { + padding: .75rem 1rem; +} diff --git a/src/FluentCMS.Web.UI/wwwroot/css/components/_badge.scss b/src/FluentCMS.Web.UI/wwwroot/css/components/_badge.scss new file mode 100644 index 000000000..7c4cde178 --- /dev/null +++ b/src/FluentCMS.Web.UI/wwwroot/css/components/_badge.scss @@ -0,0 +1,11 @@ +$prefix-badge: $prefix + 'badge' !default; + +.#{$prefix-badge} { + @extend .badge; + + @each $color, $value in $theme-colors { + &-color-#{$color} { + @extend .text-bg-#{$color}; + } + } +} diff --git a/src/FluentCMS.sln b/src/FluentCMS.sln index e78e0272c..2e6c1a205 100644 --- a/src/FluentCMS.sln +++ b/src/FluentCMS.sln @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentCMS.Repository.LiteDb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentCMS.Repository.LiteDb.Tests", "FluentCMS.Repository.LiteDb.Tests\FluentCMS.Repository.LiteDb.Tests.csproj", "{F89AFE91-0413-4808-AEF9-5C380B3227E3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentCMS.Shared", "FluentCMS.Shared\FluentCMS.Shared.csproj", "{2F992396-641A-4BE0-87E3-F4A3EB691923}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentCMS.Web.UI", "FluentCMS.Web.UI\FluentCMS.Web.UI.csproj", "{144A959D-F8C0-4478-B8DA-57A2959B8C7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,14 @@ Global {F89AFE91-0413-4808-AEF9-5C380B3227E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {F89AFE91-0413-4808-AEF9-5C380B3227E3}.Release|Any CPU.ActiveCfg = Release|Any CPU {F89AFE91-0413-4808-AEF9-5C380B3227E3}.Release|Any CPU.Build.0 = Release|Any CPU + {2F992396-641A-4BE0-87E3-F4A3EB691923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F992396-641A-4BE0-87E3-F4A3EB691923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F992396-641A-4BE0-87E3-F4A3EB691923}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F992396-641A-4BE0-87E3-F4A3EB691923}.Release|Any CPU.Build.0 = Release|Any CPU + {144A959D-F8C0-4478-B8DA-57A2959B8C7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {144A959D-F8C0-4478-B8DA-57A2959B8C7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {144A959D-F8C0-4478-B8DA-57A2959B8C7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {144A959D-F8C0-4478-B8DA-57A2959B8C7D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE