Skip to content

Commit

Permalink
Merge pull request #94 from dotnetcameroon/feat/projects-section
Browse files Browse the repository at this point in the history
Feat/projects section
  • Loading branch information
djoufson authored Jan 3, 2025
2 parents 02f03af + c17743e commit ec62263
Show file tree
Hide file tree
Showing 17 changed files with 320 additions and 76 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# secret files
secrets/
hangfire.db*
projects.db*

# dotenv files
.env
Expand Down
8 changes: 8 additions & 0 deletions app.business/Services/IProjectService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using app.domain.Models.ProjectsAggregate;

namespace app.business.Services;
public interface IProjectService
{
Task<Project[]> GetAllAsync();
Task RefreshAsync(IEnumerable<Project> projects);
}
11 changes: 11 additions & 0 deletions app.domain/Models/ProjectsAggregate/Project.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace app.domain.Models.ProjectsAggregate;
public class Project
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string AuthorHandle { get; set; } = string.Empty;
public string Technologies { get; set; } = string.Empty; // comma separated list
public string? Github { get; set; }
public string? Website { get; set; }
}
19 changes: 19 additions & 0 deletions app.infrastructure/Persistence/Factories/ProjectsFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using app.domain.Models.ProjectsAggregate;
using Bogus;
using EntityFrameworkCore.Seeder.Base;

namespace app.infrastructure.Persistence.Factories;
public class ProjectsFactory : Factory<Project>
{
protected override Faker<Project> BuildRules()
{
return new Faker<Project>()
.RuleFor(p => p.Title, f => f.Lorem.Sentence())
.RuleFor(p => p.Description, f => f.Lorem.Paragraph())
.RuleFor(p => p.Id , f => f.Random.Guid())
.RuleFor(p => p.Github, f => f.Internet.Url())
.RuleFor(p => p.Website, f => f.Internet.Url())
.RuleFor(p => p.AuthorHandle, f => f.Internet.UserName())
.RuleFor(p => p.Technologies, f => string.Join(',', f.Lorem.Words(f.Random.Number(1, 5))));
}
}
17 changes: 17 additions & 0 deletions app.infrastructure/Persistence/ProjectDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using app.domain.Models.ProjectsAggregate;
using Microsoft.EntityFrameworkCore;

namespace app.infrastructure.Persistence;
public class ProjectDbContext(DbContextOptions<ProjectDbContext> options) : DbContext(options)
{
public DbSet<Project> Projects { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Project>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Title);
entity.HasIndex(e => e.Title).IsUnique();
});
}
}
13 changes: 13 additions & 0 deletions app.infrastructure/Persistence/Seeders/ProjectsSeeder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using app.infrastructure.Persistence.Factories;
using EntityFrameworkCore.Seeder.Base;

namespace app.infrastructure.Persistence.Seeders;
public class ProjectsSeeder(ProjectDbContext context) : ISeeder
{
public async Task SeedAsync()
{
var projects = new ProjectsFactory().Generate(5);
await context.Projects.AddRangeAsync(projects);
await context.SaveChangesAsync();
}
}
20 changes: 20 additions & 0 deletions app.infrastructure/Services/ProjectService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using app.business.Services;
using app.domain.Models.ProjectsAggregate;
using app.infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace app.infrastructure.Services;
public class ProjectService(ProjectDbContext context) : IProjectService
{
public Task<Project[]> GetAllAsync()
{
return context.Projects.ToArrayAsync();
}

public async Task RefreshAsync(IEnumerable<Project> projects)
{
await context.Projects.ExecuteDeleteAsync();
await context.Projects.AddRangeAsync(projects);
await context.SaveChangesAsync();
}
}
1 change: 1 addition & 0 deletions app.infrastructure/app.infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
48 changes: 48 additions & 0 deletions app/Api/Projects/ProjectsApi.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Text.Json;
using app.business.Services;
using app.domain.Models.ProjectsAggregate;
using app.shared.Utilities;

namespace app.Api.Projects;
Expand All @@ -8,6 +11,51 @@ public static IEndpointRouteBuilder MapProjectsApi(this IEndpointRouteBuilder en
endpoints.MapGet("/api/projects", () => "Hello Projects!")
.RequireAuthorization(Policies.JwtAuthOnly);

endpoints.MapPost("/api/projects", async (
IFormFile formFile, IProjectService projectService) =>
{
using var reader = new StreamReader(formFile.OpenReadStream());
var content = await reader.ReadToEndAsync();
var projects = JsonSerializer.Deserialize<ProjectDto[]>(content) ?? throw new InvalidOperationException("Invalid JSON");
await projectService.RefreshAsync(projects.Select(p => new Project
{
Id = Guid.NewGuid(),
Title = p.Title,
Description = p.Description,
AuthorHandle = p.AuthorHandle,
Technologies = p.Technologies,
Github = p.Github,
Website = p.Website
}));
return Results.Ok();
})
.RequireAuthorization(Policies.JwtAuthOnly);

endpoints.MapPost("/api/projects/json", async (string json, IProjectService projectService) =>
{
var projects = JsonSerializer.Deserialize<ProjectDto[]>(json) ?? throw new InvalidOperationException("Invalid JSON");
await projectService.RefreshAsync(projects.Select(p => new Project
{
Id = Guid.NewGuid(),
Title = p.Title,
Description = p.Description,
AuthorHandle = p.AuthorHandle,
Technologies = p.Technologies,
Github = p.Github,
Website = p.Website
}));
return Results.Ok();
})
.RequireAuthorization(Policies.JwtAuthOnly);

return endpoints;
}
}

public record struct ProjectDto(
string Title,
string Description,
string AuthorHandle,
string Technologies,
string? Github,
string? Website);
83 changes: 72 additions & 11 deletions app/Components/Pages/Home.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@page "/"
@using app.business.Services
@using app.domain.Models.ProjectsAggregate
@using app.shared.Utilities
@attribute [StreamRendering]
@inject IProjectService ProjectService

<PageTitle>.NET Cameroon 🇨🇲 | Home</PageTitle>
<HeadContent>
Expand Down Expand Up @@ -113,18 +117,75 @@
<div class="flex flex-wrap gap-8 justify-center items-center mt-8">
<a data-enhance-nav="false" href="/"><img src="@Assets["/assets/sponsors/dotnetfondation.webp"]"
alt=".NET Foundation" class="h-[100px]" /></a>
<a data-enhance-nav="false" href="https://www.packtpub.com/"><img src="@Assets["/assets/sponsors/packt.svg"]" alt="Packt"
class="w-[200px]" /></a>
<a data-enhance-nav="false" href="https://www.linkedin.com/company/communaut%C3%A9-microsoft-powerplatform"><img src="@Assets["/assets/sponsors/Microsoft_PowerPlateform_Cameroun-removebg-preview.png"]"
alt="Microsoft PowerPlateform Cameroun " class="w-[200px]" /></a>

<a data-enhance-nav="false" href="https://examboot.net/"><img src="@Assets["/assets/sponsors/examboot.jpg"]"
alt="Examboot" class="w-[200px]" /></a>
<a data-enhance-nav="false" href="https://itia-consulting.com"><img src="@Assets["/assets/sponsors/itia-removebg-preview.png"]"
alt="Itia" class="w-[200px]" /></a>
<a data-enhance-nav="false" href="https://proditech-digital.com"><img src="@Assets["/assets/sponsors/proditech.png"]"
alt="Proditech" class="h-[100px]" /></a>
<a data-enhance-nav="false" href="https://www.packtpub.com/"><img
src="@Assets["/assets/sponsors/packt.svg"]" alt="Packt" class="w-[200px]" /></a>
<a data-enhance-nav="false"
href="https://www.linkedin.com/company/communaut%C3%A9-microsoft-powerplatform"><img
src="@Assets["/assets/sponsors/Microsoft_PowerPlateform_Cameroun-removebg-preview.png"]"
alt="Microsoft PowerPlateform Cameroun " class="w-[200px]" /></a>

<a data-enhance-nav="false" href="https://examboot.net/"><img
src="@Assets["/assets/sponsors/examboot.jpg"]" alt="Examboot" class="w-[200px]" /></a>
<a data-enhance-nav="false" href="https://itia-consulting.com"><img
src="@Assets["/assets/sponsors/itia-removebg-preview.png"]" alt="Itia" class="w-[200px]" /></a>
<a data-enhance-nav="false" href="https://proditech-digital.com"><img
src="@Assets["/assets/sponsors/proditech.png"]" alt="Proditech" class="h-[100px]" /></a>
</div>
</div>
</section>

<!-- Projects -->
@if(_projects.Length > 0)
{
<section>
<div class="section container mx-auto">
<h1 class="heading heading-1">Our Open Source Projects</h1>
<p class="md:w-2/3">
Explore the innovative open-source projects driving our community forward. Built with passion and
collaboration, these projects reflect our commitment to sharing knowledge and building impactful
solutions together.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mt-8">
@foreach (var project in _projects)
{
<div class="min-h-[200px] flex flex-col items-start hover:shadow-lg transition-all p-4 rounded-lg" target="_blank">
<span class="font-bold">@project.Title</span>
<p class="text-gray-600 line-clamp-2 text-sm">@project.Description</p>

<div class="flex flex-wrap gap-2">
@foreach (var tech in project.Technologies.Split(','))
{
<span class="text-sm m-0 font-semibold text-secondary">#@tech</span>
}
</div>

<div class="flex flex-wrap gap-2 mt-2">
@if(!string.IsNullOrWhiteSpace(project.Github))
{
<a href="@project.Github" title="@project.Title github" class="text-primary text-lg hover:text-primary-accentuation transition-all" target="_blank">
<i class="fa-brands fa-github"></i>
</a>
}
@if(!string.IsNullOrWhiteSpace(project.Website))
{
<a href="@project.Website" title="@project.Title website" class="text-primary text-lg hover:text-primary-accentuation transition-all" target="_blank">
<i class="fa-solid fa-globe"></i>
</a>
}
</div>
</div>
}
</div>
</div>
</section>
}
</div>

@code {
private Project[] _projects = [];
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_projects = await ProjectService.GetAllAsync();
}
}
3 changes: 3 additions & 0 deletions app/Extensions/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static class Extensions
{
private const string SqlServer = "SqlServer";
private const string HangfireSqlite = "HangfireSqlite";
private const string Sqlite = "Sqlite";
public static IServiceCollection AddServices(
this IServiceCollection services,
IConfiguration configuration,
Expand Down Expand Up @@ -57,12 +58,14 @@ public static IServiceCollection AddServices(
services.AddScoped<IFileManager, FileManager>();
services.AddScoped<IEventService, EventService>();
services.AddScoped<ITokenProvider, TokenProvider>();
services.AddScoped<IProjectService, ProjectService>();
services.AddScoped<IFileDownloader, FileDownloader>();
services.AddScoped<IPartnerService, PartnerService>();
services.AddScoped<IIdentityService, IdentityService>();
services.AddScoped<IExternalAppService, ExternalAppService>();
services.AddScoped<IPasswordHasher<Application>>(sp => new PasswordHasher<Application>());
services.AddScoped<DomainEventsInterceptor>();
services.AddSqlite<ProjectDbContext>(configuration.GetConnectionString(Sqlite));
services.AddSqlServer<AppDbContext>(configuration.GetConnectionString(SqlServer));
services.AddScoped<IDbContext>(sp => sp.GetRequiredService<AppDbContext>());
services.AddScoped<DbContext>(sp => sp.GetRequiredService<AppDbContext>());
Expand Down
12 changes: 12 additions & 0 deletions app/Extensions/SqliteExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using app.infrastructure.Persistence;

namespace app.Extensions;
public static class SqliteExtensions
{
public static void EnsureDatabaseCreated(this IApplicationBuilder app)
{
var scope = app.ApplicationServices.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ProjectDbContext>();
dbContext.Database.EnsureCreated();
}
}
2 changes: 2 additions & 0 deletions app/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

var app = builder.Build();

app.EnsureDatabaseCreated();

if (await app.MapSeedCommandsAsync(args))
{
return;
Expand Down
3 changes: 2 additions & 1 deletion app/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"ConnectionStrings": {
"SqlServer": "Server=localhost,1433;Database=Website;User Id=sa;Password=!Passw0rd;TrustServerCertificate=True;Encrypt=false",
"Npgsql": "Host=localhost;Port=5432;Username=admin;Password=P@55w0rd;Database=Website;",
"HangfireSqlite": "hangfire.db"
"HangfireSqlite": "hangfire.db",
"Sqlite": "Data Source=./projects.db;"
},
"CookiesOptions": {
"Issuer": "localhost:8000",
Expand Down
3 changes: 2 additions & 1 deletion app/appsettings.Production.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"ConnectionStrings": {
"SqlServer": "",
"Npgsql": "",
"HangfireSqlite": ""
"HangfireSqlite": "",
"Sqlite": ""
},
"CookiesOptions": {
"Issuer": "",
Expand Down
Loading

0 comments on commit ec62263

Please sign in to comment.