back photo + event tags and persons
This commit is contained in:
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -12,6 +12,14 @@
|
||||
"webRoot": "${workspaceFolder}/front/v2",
|
||||
"preLaunchTask": "Start Node server with nvs latest"
|
||||
},
|
||||
{
|
||||
"name": "(legacy) FRONT: DEBUG(Edge)",
|
||||
"request": "launch",
|
||||
"type": "msedge",
|
||||
"url": "http://localhost:4200",
|
||||
"webRoot": "${workspaceFolder}/front/v1",
|
||||
"preLaunchTask": "(legacy) Start Node server with nvs latest"
|
||||
},
|
||||
{
|
||||
"name": "Attach Edge",
|
||||
"type": "msedge",
|
||||
|
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -1,6 +1,22 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "(legacy) Start Node server with nvs latest",
|
||||
"type": "shell",
|
||||
"command": "nvs use latest && npm run start",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/front/v1"
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start Node server with nvs latest",
|
||||
"type": "shell",
|
||||
|
@@ -1,4 +0,0 @@
|
||||
public static class Constants
|
||||
{
|
||||
public const string Data = "data";
|
||||
}
|
@@ -2,11 +2,13 @@
|
||||
|
||||
public class PhotoFormModel
|
||||
{
|
||||
public required string UserId { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Tags { get; set; }
|
||||
public string? People { get; set; }
|
||||
public string[]? Tags { get; set; }
|
||||
public string[]? People { get; set; }
|
||||
public IFormFile? Image { get; set; }
|
||||
public string? Ubicacion { get; set; }
|
||||
public string? Evento { get; set; }
|
||||
public bool IsPublic { get; set; }
|
||||
}
|
||||
|
52
back/DataModels/EventModel.cs
Normal file
52
back/DataModels/EventModel.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Events")]
|
||||
public class EventModel
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
[Required, MaxLength(50)]
|
||||
public string? Title { get; set; }
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public List<TagModel> RelatedTags { get; set; } = [];
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
public EventModel(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public EventModel(
|
||||
string id,
|
||||
string title,
|
||||
string description,
|
||||
DateTime date,
|
||||
string location,
|
||||
List<TagModel>? relatedTags = null,
|
||||
DateTime? createdAt = null,
|
||||
DateTime? updatedAt = null,
|
||||
string? createdBy = null,
|
||||
string? updatedBy = null)
|
||||
{
|
||||
Id = id;
|
||||
Title = title;
|
||||
Description = description;
|
||||
Date = date;
|
||||
Location = location;
|
||||
RelatedTags = relatedTags ?? [];
|
||||
CreatedAt = createdAt ?? DateTime.UtcNow;
|
||||
UpdatedAt = updatedAt ?? DateTime.UtcNow;
|
||||
CreatedBy = createdBy ?? "";
|
||||
UpdatedBy = updatedBy;
|
||||
}
|
||||
}
|
||||
|
27
back/DataModels/GalleryModel.cs
Normal file
27
back/DataModels/GalleryModel.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Galleries")]
|
||||
public class GalleryModel
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
[MaxLength(100)]
|
||||
public string? Title { get; set; }
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public string? UpdatedBy { get; set; }
|
||||
public List<PhotoModel> Photos { get; set; } = new();
|
||||
public bool? IsPublic { get; set; } = true;
|
||||
public bool? IsArchived { get; set; } = false;
|
||||
public bool? IsFavorite { get; set; } = false;
|
||||
public EventModel? Event { get; set; } = null;
|
||||
public List<TagModel>? Tags { get; set; } = null;
|
||||
public List<PersonModel>? PersonsInvolved { get; set; } = null;
|
||||
public List<PersonModel>? UsersWhoCanSee { get; set; } = null;
|
||||
}
|
34
back/DataModels/PermissionModel.cs
Normal file
34
back/DataModels/PermissionModel.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Permissions")]
|
||||
public class PermissionModel
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
[Required, MaxLength(100)]
|
||||
public string Name { get; set; }
|
||||
[MaxLength(255)]
|
||||
public string Description { get; set; }
|
||||
|
||||
public PermissionModel(string id, string name, string description)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
// Static permissions
|
||||
public static readonly PermissionModel ViewContentPermission = new("1", "VIEW_CONTENT", "Permission to view content");
|
||||
public static readonly PermissionModel LikeContentPermission = new("2", "LIKE_CONTENT", "Permission to like content");
|
||||
public static readonly PermissionModel EditContentPermission = new("3", "EDIT_CONTENT", "Permission to edit content");
|
||||
public static readonly PermissionModel DeleteContentPermission = new("4", "DELETE_CONTENT", "Permission to delete content");
|
||||
public static readonly PermissionModel CreateContentPermission = new("5", "CREATE_CONTENT", "Permission to create new content");
|
||||
public static readonly PermissionModel EditUserPermission = new("6", "EDIT_USER", "Permission to edit user");
|
||||
public static readonly PermissionModel DeleteUserPermission = new("7", "DELETE_USER", "Permission to delete user");
|
||||
public static readonly PermissionModel DisableUserPermission = new("8", "DISABLE_USER", "Permission to disable user");
|
||||
public static readonly PermissionModel CreateUserPermission = new("9", "CREATE_USER", "Permission to create new user");
|
||||
public static readonly PermissionModel EditWebConfigPermission = new("10", "EDIT_WEB_CONFIG", "Permission to edit web configuration");
|
||||
}
|
49
back/DataModels/PersonModel.cs
Normal file
49
back/DataModels/PersonModel.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Persons")]
|
||||
public class PersonModel
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
[Required, MaxLength(100)]
|
||||
public string? Name { get; set; }
|
||||
public string? ProfilePicture { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
public SocialMedia? SocialMedia { get; set; }
|
||||
[MaxLength(250)]
|
||||
public string? Bio { get; set; } // Optional field for a short biography or description
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public PersonModel(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public PersonModel(string id, string name, string? profilePicture = null, string? avatar = null, SocialMedia? socialMedia = null)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
ProfilePicture = profilePicture;
|
||||
Avatar = avatar;
|
||||
SocialMedia = socialMedia;
|
||||
}
|
||||
}
|
||||
|
||||
[Table("SocialMediaLinks")]
|
||||
public class SocialMedia
|
||||
{
|
||||
public string? Facebook { get; set; }
|
||||
public string? Instagram { get; set; }
|
||||
public string? Twitter { get; set; }
|
||||
public string? BlueSky { get; set; }
|
||||
public string? Tiktok { get; set; }
|
||||
public string? Linkedin { get; set; }
|
||||
public string? Pinterest { get; set; }
|
||||
public string? Discord { get; set; }
|
||||
public string? Reddit { get; set; }
|
||||
public string? Other { get; set; }
|
||||
}
|
68
back/DataModels/PhotoModel.cs
Normal file
68
back/DataModels/PhotoModel.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Photo")]
|
||||
public class PhotoModel
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
[Required, MaxLength(100), MinLength(1)]
|
||||
public string Title { get; set; }
|
||||
[MaxLength(500)]
|
||||
public string Description { get; set; }
|
||||
public string Extension { get; set; }
|
||||
public string LowResUrl { get; set; }
|
||||
public string MidResUrl { get; set; }
|
||||
public string HighResUrl { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public string CreatedBy { get; set; }
|
||||
public string UpdatedBy { get; set; }
|
||||
public EventModel? Event { get; set; } = null;
|
||||
public List<TagModel> Tags { get; set; } = [];
|
||||
public RankingModel Ranking { get; set; } = new RankingModel(0);
|
||||
public bool IsFavorite { get; set; } = false;
|
||||
public bool IsPublic { get; set; } = true;
|
||||
public bool IsArchived { get; set; } = false;
|
||||
public List<PersonModel>? Persons { get; set; }
|
||||
|
||||
public PhotoModel(
|
||||
string id,
|
||||
string title,
|
||||
string description,
|
||||
string lowResUrl,
|
||||
string midResUrl,
|
||||
string highResUrl,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
string createdBy,
|
||||
string updatedBy,
|
||||
EventModel? @event = null,
|
||||
List<TagModel>? tags = null,
|
||||
RankingModel? ranking = null,
|
||||
bool isFavorite = false,
|
||||
bool isPublic = true,
|
||||
bool isArchived = false,
|
||||
List<PersonModel>? persons = null)
|
||||
{
|
||||
Id = id;
|
||||
Title = title;
|
||||
Description = description;
|
||||
LowResUrl = lowResUrl;
|
||||
MidResUrl = midResUrl;
|
||||
HighResUrl = highResUrl;
|
||||
CreatedAt = createdAt;
|
||||
UpdatedAt = updatedAt;
|
||||
CreatedBy = createdBy;
|
||||
UpdatedBy = updatedBy;
|
||||
Event = @event ?? Event;
|
||||
Tags = tags ?? Tags;
|
||||
Ranking = ranking ?? Ranking;
|
||||
IsFavorite = isFavorite;
|
||||
IsPublic = isPublic;
|
||||
IsArchived = isArchived;
|
||||
Persons = persons ?? Persons;
|
||||
}
|
||||
}
|
36
back/DataModels/RankingModel.cs
Normal file
36
back/DataModels/RankingModel.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace back.DataModels;
|
||||
|
||||
public class RankingModel
|
||||
{
|
||||
private int totalVotes;
|
||||
private int upVotes;
|
||||
private int downVotes;
|
||||
|
||||
public RankingModel(int totalVotes, int upVotes = 0, int downVotes = 0)
|
||||
{
|
||||
this.totalVotes = totalVotes;
|
||||
this.upVotes = upVotes;
|
||||
this.downVotes = downVotes;
|
||||
}
|
||||
|
||||
public void DownVote()
|
||||
{
|
||||
downVotes++;
|
||||
totalVotes++;
|
||||
}
|
||||
|
||||
public void UpVote()
|
||||
{
|
||||
upVotes++;
|
||||
totalVotes++;
|
||||
}
|
||||
|
||||
public double Score
|
||||
{
|
||||
get
|
||||
{
|
||||
if (totalVotes == 0) return 0;
|
||||
return (double)(upVotes - downVotes) / totalVotes;
|
||||
}
|
||||
}
|
||||
}
|
69
back/DataModels/RoleModel.cs
Normal file
69
back/DataModels/RoleModel.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Tags("Roles")]
|
||||
public class RoleModel
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
[Required, MaxLength(100)]
|
||||
public string Name { get; set; }
|
||||
[MaxLength(250)]
|
||||
public string Description { get; set; }
|
||||
public List<PermissionModel> Permissions { get; set; }
|
||||
public RoleModel? BaseRoleModel { get; set; }
|
||||
|
||||
public RoleModel(string id, string name, string description, List<PermissionModel>? permissions = null, RoleModel? baseRoleModel = null)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Description = description;
|
||||
Permissions = permissions ?? new List<PermissionModel>();
|
||||
BaseRoleModel = baseRoleModel;
|
||||
if (baseRoleModel != null)
|
||||
{
|
||||
Permissions.AddRange(baseRoleModel.Permissions);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAdmin => Id == AdminRole.Id;
|
||||
public bool IsContentManager => Id == ContentManagerRole.Id;
|
||||
public bool IsUser => Id == UserRole.Id;
|
||||
|
||||
public bool HasPermission(PermissionModel permission)
|
||||
{
|
||||
return Permissions.Exists(p => p.Id == permission.Id);
|
||||
}
|
||||
|
||||
public static readonly RoleModel UserRole = new(
|
||||
"1", "User", "Role for regular users",
|
||||
new List<PermissionModel> {
|
||||
PermissionModel.ViewContentPermission,
|
||||
PermissionModel.LikeContentPermission
|
||||
}
|
||||
);
|
||||
|
||||
public static readonly RoleModel ContentManagerRole = new(
|
||||
"2", "Content Manager", "Role for managing content",
|
||||
new List<PermissionModel> {
|
||||
PermissionModel.CreateUserPermission,
|
||||
PermissionModel.DisableUserPermission,
|
||||
PermissionModel.CreateContentPermission,
|
||||
PermissionModel.EditContentPermission,
|
||||
PermissionModel.DeleteContentPermission
|
||||
},
|
||||
UserRole
|
||||
);
|
||||
|
||||
public static readonly RoleModel AdminRole = new(
|
||||
"3", "Admin", "Administrator role with full permissions",
|
||||
new List<PermissionModel> {
|
||||
PermissionModel.CreateUserPermission,
|
||||
PermissionModel.EditUserPermission,
|
||||
PermissionModel.DeleteUserPermission,
|
||||
PermissionModel.EditWebConfigPermission
|
||||
},
|
||||
ContentManagerRole
|
||||
);
|
||||
}
|
14
back/DataModels/TagModel.cs
Normal file
14
back/DataModels/TagModel.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Tags")]
|
||||
public class TagModel(string id, string? name = null)
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; init; } = id;
|
||||
[Required, MaxLength(25)]
|
||||
public string? Name { get; init; } = name;
|
||||
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
|
||||
}
|
38
back/DataModels/UserModel.cs
Normal file
38
back/DataModels/UserModel.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace back.DataModels;
|
||||
|
||||
[Table("Users")]
|
||||
public class UserModel : PersonModel
|
||||
{
|
||||
[Required]
|
||||
public string Email { get; set; }
|
||||
[Required, MinLength(8)]
|
||||
public string Password { get; set; }
|
||||
public List<RoleModel> Role { get; set; }
|
||||
|
||||
public UserModel(string id, string email, string password, string name, List<RoleModel> role, DateTime createdAt, DateTime updatedAt)
|
||||
: base(id, name)
|
||||
{
|
||||
Email = email;
|
||||
Password = password;
|
||||
Role = role;
|
||||
CreatedAt = createdAt;
|
||||
UpdatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public bool IsAdmin => Role.Exists(r => r.IsAdmin);
|
||||
public bool IsContentManager => Role.Exists(r => r.IsContentManager);
|
||||
public bool IsUser => Role.Exists(r => r.IsUser);
|
||||
|
||||
public static readonly UserModel DefaultUser = new(
|
||||
"0",
|
||||
"default@example.com",
|
||||
string.Empty,
|
||||
"Default User",
|
||||
[RoleModel.UserRole],
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow
|
||||
);
|
||||
}
|
14
back/Options/DatabaseConfig.cs
Normal file
14
back/Options/DatabaseConfig.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace back.Options;
|
||||
|
||||
public sealed class DatabaseConfig
|
||||
{
|
||||
public const string BlobStorage = "Databases:Blob";
|
||||
public const string DataStorage = "Databases:Data";
|
||||
public required string Provider { get; set; }
|
||||
public string? DatabaseName { get; set; }
|
||||
public string? AccountKey { get; set; }
|
||||
public string? TokenCredential { get; set; }
|
||||
public string? BaseUrl { get; set; }
|
||||
public string? ConnectionString { get; set; }
|
||||
public string? SystemContainer { get; set; }
|
||||
}
|
6
back/Options/Databases.cs
Normal file
6
back/Options/Databases.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace back.Options;
|
||||
|
||||
public sealed class Databases
|
||||
{
|
||||
public string? BaseDirectory { get; set; }
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
using back.ApiService.context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using back.ServicesExtensions;
|
||||
|
||||
namespace back;
|
||||
|
||||
@@ -9,9 +8,8 @@ public class Program
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
Directory.CreateDirectory(Constants.Data);
|
||||
// Add services to the container.
|
||||
builder.Services.AddDbContext<PhotoContext>(options => options.UseSqlite($"Data Source={Constants.Data}/photos.db"));
|
||||
builder.Services.UseExtensions();
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
|
37
back/ServicesExtensions/DatabaseContexts.cs
Normal file
37
back/ServicesExtensions/DatabaseContexts.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using back.context;
|
||||
using back.Options;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace back.ServicesExtensions;
|
||||
|
||||
public static partial class ServicesExtensions
|
||||
{
|
||||
private static IServiceCollection AddDatabaseContexts(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddContext<EventContext>()
|
||||
.AddContext<GalleryContext>()
|
||||
.AddContext<PersonContext>()
|
||||
.AddContext<PhotoContext>()
|
||||
.AddContext<TagContext>()
|
||||
.AddContext<UserContext>()
|
||||
;
|
||||
|
||||
return services;
|
||||
}
|
||||
private static IServiceCollection AddContext<T>(this IServiceCollection services)
|
||||
where T : DbContext
|
||||
{
|
||||
var config = services
|
||||
.BuildServiceProvider()
|
||||
.GetRequiredService<IOptionsSnapshot<DatabaseConfig>>()
|
||||
.Get(DatabaseConfig.DataStorage);
|
||||
|
||||
services.AddDbContext<T>(options =>
|
||||
{
|
||||
options.UseDatabaseConfig(config);
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
78
back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs
Normal file
78
back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using back.Options;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.ServicesExtensions;
|
||||
|
||||
public enum DatabaseProvider
|
||||
{
|
||||
/* -- Relational databases supported by EF Core -- */
|
||||
SUPPORTED, // Placeholder for supported databases.
|
||||
InMemory,
|
||||
Sqlite,
|
||||
PostgreSQL,
|
||||
CockroachDB, // CockroachDB is compatible with PostgreSQL.
|
||||
SQLServer,
|
||||
MariaDB,
|
||||
MySQL,
|
||||
Oracle, // Oracle is supported by EF Core but requires a separate package.
|
||||
|
||||
/* -- NoSQL are not supported by EF -- */
|
||||
|
||||
NOT_SUPPORTED, // Placeholder for unsupported databases.
|
||||
Firebird, // Firebird is supported by EF Core but requires a separate package.
|
||||
Db2, // Db2 is supported by EF Core but requires a separate package.
|
||||
SAPHana, // SAP HANA is supported by EF Core but requires a separate package.
|
||||
Sybase, // Sybase is supported by EF Core but requires a separate package.
|
||||
Cosmos, // Cosmos DB is database supported by EF Core.
|
||||
MongoDB,
|
||||
InfluxDB,
|
||||
Redis,
|
||||
Cassandra,
|
||||
ElasticSearch,
|
||||
CouchDB,
|
||||
RavenDB,
|
||||
Neo4j,
|
||||
OrientDB,
|
||||
ArangoDB,
|
||||
ClickHouse,
|
||||
Druid,
|
||||
TimescaleDB,
|
||||
}
|
||||
|
||||
public static partial class DbContextOptionsBuilderExtensions
|
||||
{
|
||||
private static string SupportedDbs()
|
||||
=> string.Join(", ", Enum.GetValues<DatabaseProvider>()
|
||||
.Where(db => db > DatabaseProvider.SUPPORTED && db < DatabaseProvider.NOT_SUPPORTED)
|
||||
.OrderBy(db => db)
|
||||
.Select(db => db.ToString()));
|
||||
|
||||
public static void UseDatabaseConfig(this DbContextOptionsBuilder options, DatabaseConfig config)
|
||||
{
|
||||
if(!Enum.TryParse(Enum.GetName(typeof(DatabaseProvider), config.Provider)?.ToLowerInvariant(), out DatabaseProvider provider))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported database provider: {config.Provider} -- Supported providers are: {SupportedDbs()}");
|
||||
}
|
||||
|
||||
switch (provider)
|
||||
{
|
||||
case DatabaseProvider.Sqlite:
|
||||
options.UseSqlite(config.ConnectionString);
|
||||
break;
|
||||
case DatabaseProvider.InMemory:
|
||||
options.UseInMemoryDatabase(config.ConnectionString);
|
||||
break;
|
||||
case DatabaseProvider.PostgreSQL or DatabaseProvider.CockroachDB:
|
||||
options.UseNpgsql(config.ConnectionString);
|
||||
break;
|
||||
case DatabaseProvider.SQLServer:
|
||||
options.UseSqlServer(config.ConnectionString);
|
||||
break;
|
||||
case DatabaseProvider.MySQL or DatabaseProvider.MariaDB:
|
||||
options.UseMySql(config.ConnectionString, ServerVersion.AutoDetect(config.ConnectionString));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported database provider: {config.Provider}");
|
||||
}
|
||||
}
|
||||
}
|
72
back/ServicesExtensions/Options.cs
Normal file
72
back/ServicesExtensions/Options.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using back.Options;
|
||||
|
||||
namespace back.ServicesExtensions;
|
||||
|
||||
public static partial class ServicesExtensions
|
||||
{
|
||||
private static IConfiguration ConfigureOptions(this IServiceCollection services)
|
||||
{
|
||||
IConfiguration config = services.BuildServiceProvider().GetRequiredService<IConfiguration>();
|
||||
string? baseDirectory = null;
|
||||
|
||||
services.Configure<Databases>(config.GetSection(nameof(Databases)));
|
||||
services.Configure<DatabaseConfig>(DatabaseConfig.DataStorage, config.GetSection(DatabaseConfig.DataStorage));
|
||||
services.Configure<DatabaseConfig>(DatabaseConfig.BlobStorage, config.GetSection(DatabaseConfig.BlobStorage));
|
||||
|
||||
services.PostConfigure<Databases>(databases =>
|
||||
{
|
||||
if (databases.BaseDirectory != null && !Directory.Exists(databases.BaseDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(databases.BaseDirectory);
|
||||
Console.WriteLine($"Base directory created at: {databases.BaseDirectory}");
|
||||
baseDirectory = databases.BaseDirectory;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create base directory at {databases.BaseDirectory}. " +
|
||||
"Please ensure the path is valid and accessible.", ex
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
services.PostConfigure<DatabaseConfig>(DatabaseConfig.DataStorage, config =>
|
||||
{
|
||||
PostConfigureDatabaseConfig(config, baseDirectory);
|
||||
});
|
||||
|
||||
services.PostConfigure<DatabaseConfig>(DatabaseConfig.BlobStorage, config =>
|
||||
{
|
||||
PostConfigureDatabaseConfig(config, baseDirectory);
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static void PostConfigureDatabaseConfig(DatabaseConfig config, string? baseDirectory)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(config.SystemContainer))
|
||||
{
|
||||
var path = config.SystemContainer;
|
||||
if (!string.IsNullOrEmpty(baseDirectory))
|
||||
{
|
||||
path = Path.Combine(baseDirectory, path);
|
||||
}
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
Console.WriteLine($"System container for {config.Provider} created at: {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create system container at {path}. " +
|
||||
"Please ensure the path is valid and accessible.", ex
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
back/ServicesExtensions/ServicesExtensions.cs
Normal file
21
back/ServicesExtensions/ServicesExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using back.persistance.blob;
|
||||
using back.services.ImageResizer;
|
||||
|
||||
namespace back.ServicesExtensions;
|
||||
|
||||
public static partial class ServicesExtensions
|
||||
{
|
||||
public static IServiceCollection UseExtensions(this IServiceCollection services)
|
||||
{
|
||||
var config = services.ConfigureOptions();
|
||||
|
||||
services.AddDatabaseContexts();
|
||||
|
||||
// TODO: Move and configure for using S3, Azure Blob Storage, etc.
|
||||
services.AddSingleton<IBlobStorageService, FileSystemImageStorageService>();
|
||||
|
||||
services.AddSingleton<IImageResizer, ImageResizer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Databases": {
|
||||
"BaseDirectory": "data",
|
||||
"Data": {
|
||||
"Provider": "sqlite",
|
||||
"ConnectionString": "Data Source=data/app.db;Cache=Shared"
|
||||
},
|
||||
"Blob": {
|
||||
"Provider": "system",
|
||||
"baseUrl": "https://localhost:7273/api/photo/{id}/{res}",
|
||||
"SystemContainer": "imgs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
back/appsettings.Production.json
Normal file
18
back/appsettings.Production.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Databases": {
|
||||
"Data": {
|
||||
"Provider": "sqlite",
|
||||
"ConnectionString": "Data Source=data/app.db;Cache=Shared"
|
||||
},
|
||||
"Blob": {
|
||||
"Provider": "system",
|
||||
"baseUrl": "https://back.mmorales.photo/api/photo/{id}/{res}"
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,12 +7,42 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" Version="1.14.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.UnitOfWork" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.90" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
||||
<PackageReference Include="System.Diagnostics.EventLog" Version="9.0.8" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="persistance\data\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
52
back/context/EventContext.cs
Normal file
52
back/context/EventContext.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using back.DataModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.context;
|
||||
|
||||
public class EventContext : DbContext
|
||||
{
|
||||
private DbSet<EventModel> Events { get; set; }
|
||||
public EventContext(DbContextOptions<EventContext> options)
|
||||
{
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public async Task<EventModel?> GetById(string id)
|
||||
{
|
||||
return await GetById(Guid.Parse(id));
|
||||
}
|
||||
|
||||
public async Task<EventModel?> GetById(Guid id)
|
||||
{
|
||||
return await Events.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalItems()
|
||||
{
|
||||
return await Events.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EventModel>> GetPage(int page = 1, int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
return await Events
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(EventModel? photo)
|
||||
{
|
||||
if (photo == null) return false;
|
||||
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||
return await Events.AnyAsync(p => p.Id == photo.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string id)
|
||||
{
|
||||
return await Events.AnyAsync(p => p.Id == id);
|
||||
}
|
||||
}
|
48
back/context/GalleryContext.cs
Normal file
48
back/context/GalleryContext.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using back.DataModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.context;
|
||||
|
||||
public class GalleryContext : DbContext
|
||||
{
|
||||
public DbSet<GalleryModel> Galleries { get; set; }
|
||||
public GalleryContext(DbContextOptions<GalleryContext> options) : base(options)
|
||||
{
|
||||
// Ensure database is created
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public async Task<GalleryModel?> GetById(Guid id)
|
||||
{
|
||||
return await Galleries.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalItems()
|
||||
{
|
||||
return await Galleries.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<GalleryModel>> GetPage(int page = 1, int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
return await Galleries
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(GalleryModel? photo)
|
||||
{
|
||||
if (photo == null) return false;
|
||||
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||
return await Galleries.AnyAsync(p => p.Id == photo.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string id)
|
||||
{
|
||||
return await Galleries.AnyAsync(p => p.Id == id);
|
||||
}
|
||||
}
|
52
back/context/PersonContext.cs
Normal file
52
back/context/PersonContext.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using back.DataModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.context;
|
||||
|
||||
public class PersonContext : DbContext
|
||||
{
|
||||
private DbSet<PersonModel> Persons { get; set; }
|
||||
public PersonContext(DbContextOptions<PersonContext> options)
|
||||
{
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public async Task<PersonModel?> GetById(string id)
|
||||
{
|
||||
return await GetById(Guid.Parse(id));
|
||||
}
|
||||
|
||||
public async Task<PersonModel?> GetById(Guid id)
|
||||
{
|
||||
return await Persons.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalItems()
|
||||
{
|
||||
return await Persons.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PersonModel>> GetPage(int page = 1, int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
return await Persons
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(PersonModel? photo)
|
||||
{
|
||||
if (photo == null) return false;
|
||||
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||
return await Persons.AnyAsync(p => p.Id == photo.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string id)
|
||||
{
|
||||
return await Persons.AnyAsync(p => p.Id == id);
|
||||
}
|
||||
}
|
@@ -1,29 +1,235 @@
|
||||
using back.ApiService.models;
|
||||
using back.DataModels;
|
||||
using back.DTO;
|
||||
using back.persistance.blob;
|
||||
using back.services.ImageResizer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.ApiService.context;
|
||||
namespace back.context;
|
||||
|
||||
public class PhotoContext : DbContext
|
||||
{
|
||||
public DbSet<Photo> Photos { get; set; }
|
||||
private DbSet<PhotoModel> Photos { get; set; }
|
||||
private readonly IImageResizer _Resizer;
|
||||
private readonly IBlobStorageService _BlobStorage;
|
||||
|
||||
public PhotoContext(DbContextOptions<PhotoContext> options) : base(options)
|
||||
private readonly TagContext _tagContext;
|
||||
private readonly EventContext _eventContext;
|
||||
private readonly PersonContext _personContext;
|
||||
|
||||
public PhotoContext(DbContextOptions<PhotoContext> options,
|
||||
IImageResizer resizer,
|
||||
IBlobStorageService blobStorage,
|
||||
TagContext tags,
|
||||
EventContext events,
|
||||
PersonContext persons
|
||||
) : base(options)
|
||||
{
|
||||
// Ensure database is created
|
||||
Database.EnsureCreated();
|
||||
_Resizer = resizer;
|
||||
_BlobStorage = blobStorage;
|
||||
_tagContext = tags;
|
||||
_eventContext = events;
|
||||
_personContext = persons;
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
public async Task CreateNew(PhotoFormModel? form)
|
||||
{
|
||||
modelBuilder.Entity<Photo>()
|
||||
.Property(p => p.Tags)
|
||||
.HasConversion(
|
||||
v => string.Join(',', v),
|
||||
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
|
||||
modelBuilder.Entity<Photo>()
|
||||
.Property(p => p.PersonsIn)
|
||||
.HasConversion(
|
||||
v => string.Join(',', v),
|
||||
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());
|
||||
if (form == null) { return; }
|
||||
|
||||
var photo = new PhotoModel(
|
||||
Guid.NewGuid().ToString(),
|
||||
form.Title,
|
||||
form.Description ?? string.Empty,
|
||||
string.Empty, // LowResUrl will be set later
|
||||
string.Empty, // MidResUrl will be set later
|
||||
string.Empty, // HighResUrl will be set later
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
form.UserId,
|
||||
form.UserId
|
||||
)
|
||||
{
|
||||
IsPublic = form.IsPublic
|
||||
};
|
||||
|
||||
List<Task> tasks = [
|
||||
SaveBlob(photo, form),
|
||||
LinkTags(photo, form.Tags ?? [], form.UserId),
|
||||
LinkEvent(photo, form.Evento ?? "", form.UserId),
|
||||
LinkPersons(photo, form.People ?? [], form.UserId),
|
||||
];
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Photos.AddAsync(photo);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task LinkPersons(PhotoModel photo, string[] personas, string updatedBy = "SYSTEM")
|
||||
{
|
||||
if (photo == null || personas == null || personas.Length == 0) return;
|
||||
foreach (var personId in personas)
|
||||
{
|
||||
var person = await _personContext.GetById(personId);
|
||||
if (person != null)
|
||||
{
|
||||
await LinkPersons(photo, person, updatedBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LinkPersons(PhotoModel photo, PersonModel tag, string updatedBy = "SYSTEM")
|
||||
{
|
||||
if (tag == null) return;
|
||||
// Ensure the tag exists
|
||||
if (await _personContext.Exists(tag.Id))
|
||||
{
|
||||
photo.Persons ??= [];
|
||||
photo.Persons.Add(tag);
|
||||
photo.UpdatedAt = DateTime.UtcNow;
|
||||
photo.UpdatedBy = updatedBy; // or use a more appropriate value
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LinkTags(PhotoModel photo, string[] tags, string updatedBy = "SYSTEM")
|
||||
{
|
||||
if (photo == null || tags == null || tags.Length == 0) return;
|
||||
foreach (var tagId in tags)
|
||||
{
|
||||
var tag = await _tagContext.GetById(tagId);
|
||||
if (tag != null)
|
||||
{
|
||||
await LinkTag(photo, tag, updatedBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LinkTag(PhotoModel photo, TagModel tag, string updatedBy = "SYSTEM")
|
||||
{
|
||||
if (tag == null) return;
|
||||
// Ensure the tag exists
|
||||
if (await _tagContext.Exists(tag.Id))
|
||||
{
|
||||
photo.Tags.Add(tag);
|
||||
photo.UpdatedAt = DateTime.UtcNow;
|
||||
photo.UpdatedBy = updatedBy; // or use a more appropriate value
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LinkEvent(PhotoModel photo, string eventId, string updatedBy = "SYSTEM")
|
||||
{
|
||||
if (string.IsNullOrEmpty(eventId)) return;
|
||||
var evento = await _eventContext.GetById(eventId);
|
||||
if (evento != null)
|
||||
{
|
||||
await LinkEvent(photo, evento, updatedBy);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LinkEvent(PhotoModel photo, EventModel? evento, string updatedBy = "SYSTEM")
|
||||
{
|
||||
if (evento == null) return;
|
||||
// Ensure the event exists
|
||||
if (await _eventContext.Exists(evento.Id))
|
||||
{
|
||||
photo.Event = evento;
|
||||
photo.UpdatedAt = DateTime.UtcNow;
|
||||
photo.UpdatedBy = updatedBy;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveBlob(PhotoModel photo, PhotoFormModel form)
|
||||
{
|
||||
if (form.Image != null && form.Image.Length > 0)
|
||||
{
|
||||
var lowRes = await _Resizer.ResizeImage(form.Image, 480);
|
||||
var midRes = await _Resizer.ResizeImage(form.Image, 720);
|
||||
// Upload images to blob storage
|
||||
photo.Extension = form.Image.FileName.Split('.').Last();
|
||||
photo.LowResUrl = $"low/{photo.Id}.webp";
|
||||
photo.MidResUrl = $"mid/{photo.Id}.webp";
|
||||
photo.HighResUrl = $"high/{photo.Id}.{photo.Extension}";
|
||||
await _BlobStorage.SaveAsync(lowRes, photo.LowResUrl);
|
||||
await _BlobStorage.SaveAsync(midRes, photo.MidResUrl);
|
||||
await _BlobStorage.SaveAsync(form.Image.OpenReadStream(), photo.HighResUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PhotoModel?> GetById(string id)
|
||||
{
|
||||
return await GetById(Guid.Parse(id));
|
||||
}
|
||||
|
||||
public async Task<PhotoModel?> GetById(Guid id)
|
||||
{
|
||||
return await Photos.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalItems()
|
||||
{
|
||||
return await Photos.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PhotoModel>> GetPage(int page = 1, int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
return await Photos
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(PhotoModel? photo)
|
||||
{
|
||||
if (photo == null) return false;
|
||||
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||
return await Photos.AnyAsync(p => p.Id == photo.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string id)
|
||||
{
|
||||
return await Photos.AnyAsync(p => p.Id == id);
|
||||
}
|
||||
|
||||
public async Task Delete(PhotoModel photo)
|
||||
{
|
||||
if (photo == null) return;
|
||||
if (await Exists(photo))
|
||||
{
|
||||
// Delete the photo from blob storage
|
||||
if (!string.IsNullOrEmpty(photo.LowResUrl))
|
||||
await _BlobStorage.DeleteAsync(photo.LowResUrl);
|
||||
if (!string.IsNullOrEmpty(photo.MidResUrl))
|
||||
await _BlobStorage.DeleteAsync(photo.MidResUrl);
|
||||
if (!string.IsNullOrEmpty(photo.HighResUrl))
|
||||
await _BlobStorage.DeleteAsync(photo.HighResUrl);
|
||||
Photos.Remove(photo);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Update(PhotoModel photo)
|
||||
{
|
||||
if (photo == null) return;
|
||||
if (await Exists(photo))
|
||||
{
|
||||
var evento = photo.Event;
|
||||
photo.Event = null;
|
||||
await LinkEvent(photo, evento, photo.UpdatedBy);
|
||||
|
||||
var tags = photo.Tags.Select(t => t.Id);
|
||||
photo.Tags.Clear();
|
||||
await LinkTags(photo, [.. tags], photo.UpdatedBy);
|
||||
|
||||
var persons = photo.Persons?.Select(t => t.Id) ?? [];
|
||||
photo.Persons = null;
|
||||
await LinkPersons(photo, [.. persons], photo.UpdatedBy);
|
||||
|
||||
Photos.Update(photo);
|
||||
await SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
52
back/context/TagContext.cs
Normal file
52
back/context/TagContext.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using back.DataModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.context;
|
||||
|
||||
public class TagContext : DbContext
|
||||
{
|
||||
private DbSet<TagModel> Tags { get; set; }
|
||||
public TagContext(DbContextOptions<TagContext> options)
|
||||
{
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public async Task<TagModel?> GetById(string id)
|
||||
{
|
||||
return await GetById(Guid.Parse(id));
|
||||
}
|
||||
|
||||
public async Task<TagModel?> GetById(Guid id)
|
||||
{
|
||||
return await Tags.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalItems()
|
||||
{
|
||||
return await Tags.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TagModel>> GetPage(int page = 1, int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
return await Tags
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(TagModel? photo)
|
||||
{
|
||||
if (photo == null) return false;
|
||||
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||
return await Tags.AnyAsync(p => p.Id == photo.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string id)
|
||||
{
|
||||
return await Tags.AnyAsync(p => p.Id == id);
|
||||
}
|
||||
}
|
48
back/context/UserContext.cs
Normal file
48
back/context/UserContext.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using back.DataModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.context;
|
||||
|
||||
public class UserContext : DbContext
|
||||
{
|
||||
public DbSet<UserModel> Users { get; set; }
|
||||
public UserContext(DbContextOptions<UserContext> options) : base(options)
|
||||
{
|
||||
// Ensure database is created
|
||||
Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public async Task<UserModel?> GetById(Guid id)
|
||||
{
|
||||
return await Users.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalItems()
|
||||
{
|
||||
return await Users.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UserModel>> GetPage(int page = 1, int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
return await Users
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(UserModel? photo)
|
||||
{
|
||||
if (photo == null) return false;
|
||||
if (string.IsNullOrEmpty(photo.Id)) return false;
|
||||
return await Users.AnyAsync(p => p.Id == photo.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string id)
|
||||
{
|
||||
return await Users.AnyAsync(p => p.Id == id);
|
||||
}
|
||||
}
|
@@ -1,112 +1,98 @@
|
||||
using back.ApiService.context;
|
||||
using back.ApiService.models;
|
||||
using back.context;
|
||||
using back.DataModels;
|
||||
using back.DTO;
|
||||
using back.persistance.blob;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace back.controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class PhotosController(PhotoContext photoContext) : ControllerBase
|
||||
public class PhotosController(PhotoContext photoContext, IBlobStorageService blobStorage) : ControllerBase
|
||||
{
|
||||
private readonly PhotoContext _photoContext = photoContext;
|
||||
|
||||
// GET: api/<PhotoController>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Photo>>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
public async Task<ActionResult<IEnumerable<PhotoModel>>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
var totalItems = await _photoContext.Photos.CountAsync();
|
||||
var photos = await _photoContext.Photos
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
var photos = await _photoContext.GetPage(page, pageSize);
|
||||
var totalItems = await _photoContext.GetTotalItems();
|
||||
|
||||
Response.Headers.Append("X-Total-Count", totalItems.ToString());
|
||||
|
||||
return Ok(photos);
|
||||
}
|
||||
|
||||
// GET api/<PhotoController>/5
|
||||
[HttpGet("{id}/{res}")]
|
||||
public async Task<IActionResult> Get(Guid id, string res = "low")
|
||||
[HttpGet("{res}/{id}")]
|
||||
public async Task<IActionResult> Get(string res, Guid id)
|
||||
{
|
||||
var photo = await _photoContext.Photos.FindAsync(id);
|
||||
var photo = await _photoContext.GetById(id);
|
||||
if (photo == null)
|
||||
return NotFound();
|
||||
|
||||
string? filePath = res.ToLower() switch
|
||||
{
|
||||
"low" => photo.LowResUrl,
|
||||
"mid" => photo.MidResUrl,
|
||||
"high" => photo.HighResUrl,
|
||||
_ => null
|
||||
"mid" => photo.MidResUrl,
|
||||
"low" or _ => photo.LowResUrl
|
||||
};
|
||||
|
||||
if (filePath == null || !System.IO.File.Exists(Path.Combine(Constants.Data, filePath)))
|
||||
string? mediaType = res.ToLower() switch
|
||||
{
|
||||
"high" => $"image/{photo.Extension}",
|
||||
"mid" or "low" or _ => "image/webp",
|
||||
};
|
||||
|
||||
if (filePath == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var file = await blobStorage.GetBytesAsync(filePath);
|
||||
if (file == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var fileBytes = await System.IO.File.ReadAllBytesAsync(Path.Combine(Constants.Data, filePath));
|
||||
var contentType = "image/jpeg"; // Cambia si usas otro formato
|
||||
|
||||
return File(fileBytes, contentType);
|
||||
return File(file, mediaType);
|
||||
}
|
||||
|
||||
// POST api/<PhotoController>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Post([FromForm] PhotoFormModel form)
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
if (form.Image == null || form.Image.Length == 0)
|
||||
return BadRequest("No image uploaded.");
|
||||
|
||||
var photo = PhotoBuilder.Build(
|
||||
form.Title,
|
||||
form.Description,
|
||||
form.Tags?.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
form.People?.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
form.Image
|
||||
);
|
||||
|
||||
// Guarda la información en la base de datos
|
||||
_photoContext.Photos.Add(photo);
|
||||
await _photoContext.SaveChangesAsync();
|
||||
await _photoContext.CreateNew(form);
|
||||
|
||||
return Created();
|
||||
}
|
||||
catch
|
||||
catch
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
}
|
||||
|
||||
//// PUT api/<PhotoController>/5
|
||||
//[HttpPut("{id}")]
|
||||
//public async Task<IActionResult> Put(Guid id, [FromBody] Photo photo)
|
||||
//{
|
||||
// if (id != photo.Id)
|
||||
// return BadRequest();
|
||||
|
||||
// _photoContext.Entry(photo).State = EntityState.Modified;
|
||||
// await _photoContext.SaveChangesAsync();
|
||||
// return NoContent();
|
||||
//}
|
||||
//// PUT api/<PhotoController>
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> Put([FromBody] PhotoModel photo)
|
||||
{
|
||||
await _photoContext.Update(photo);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// DELETE api/<PhotoController>/5
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var photo = await _photoContext.Photos.FindAsync(id);
|
||||
var photo = await _photoContext.GetById(id);
|
||||
if (photo == null)
|
||||
return NotFound();
|
||||
|
||||
_photoContext.Photos.Remove(photo);
|
||||
await _photoContext.SaveChangesAsync();
|
||||
await _photoContext.Delete(photo);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
@@ -1,96 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace back.ApiService.models;
|
||||
|
||||
public class PhotoBuilder
|
||||
{
|
||||
public static Photo Build(string? title, string? description, List<string>? tags, List<string>? personsIn, IFormFile image)
|
||||
{
|
||||
// Genera un nombre de archivo único
|
||||
var id = Guid.NewGuid();
|
||||
var fileName = $"{id}{Path.GetExtension(image.FileName)}";
|
||||
var photo = new Photo(title, description, tags, personsIn, fileName)
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
|
||||
// Asegura que los directorios existen
|
||||
Directory.CreateDirectory(Path.Join(Constants.Data, Photo.LowResFolder));
|
||||
Directory.CreateDirectory(Path.Join(Constants.Data, Photo.MidResFolder));
|
||||
Directory.CreateDirectory(Path.Join(Constants.Data, Photo.HighResFolder));
|
||||
|
||||
// Procesa y guarda las imágenes
|
||||
using var stream = image.OpenReadStream();
|
||||
using var img = Image.Load(stream);
|
||||
// Baja resolución (480px)
|
||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(480, 0), Mode = ResizeMode.Max }));
|
||||
img.Save(Path.Join(Constants.Data, photo.LowResUrl));
|
||||
|
||||
// Media resolución (720px)
|
||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(720, 0), Mode = ResizeMode.Max }));
|
||||
img.Save(Path.Join(Constants.Data, photo.MidResUrl));
|
||||
|
||||
// Original
|
||||
stream.Position = 0;
|
||||
using var original = Image.Load(stream);
|
||||
original.Save(Path.Join(Constants.Data, photo.HighResUrl));
|
||||
|
||||
return photo;
|
||||
}
|
||||
}
|
||||
|
||||
[Table("Photo")]
|
||||
public class Photo
|
||||
{
|
||||
public const string LowResFolder = "imgs/low";
|
||||
public const string MidResFolder = "imgs/mid";
|
||||
public const string HighResFolder = "imgs/high";
|
||||
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Ubicación { get; set; }
|
||||
public string? Evento { get; set; }
|
||||
public List<string>? Tags { get; set; }
|
||||
public List<string>? PersonsIn { get; set; }
|
||||
public string? FileName { get; set; }
|
||||
public string LowResUrl { get => Path.Join(LowResFolder, FileName); }
|
||||
public string MidResUrl { get => Path.Join(MidResFolder, FileName); }
|
||||
public string HighResUrl { get => Path.Join(HighResFolder, FileName); }
|
||||
public float? Ranking { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
private Photo()
|
||||
{
|
||||
//Id = Guid.NewGuid();
|
||||
Tags = [];
|
||||
PersonsIn = [];
|
||||
CreatedAt = DateTime.Now;
|
||||
UpdatedAt = DateTime.Now;
|
||||
CreatedBy = "system";
|
||||
UpdatedBy = "system";
|
||||
Ranking = 0.0f;
|
||||
}
|
||||
|
||||
public Photo(string? title, string? description, List<string>? tags, List<string>? personsIn, string? fileName)
|
||||
: this()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
throw new ArgumentException("FileName cannot be null or empty.", nameof(fileName));
|
||||
}
|
||||
Title = title;
|
||||
Description = description;
|
||||
Tags = tags ?? [];
|
||||
PersonsIn = personsIn ?? [];
|
||||
FileName = fileName;
|
||||
}
|
||||
}
|
104
back/persistance/blob/FileSystemImageStorageService.cs
Normal file
104
back/persistance/blob/FileSystemImageStorageService.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using back.Options;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace back.persistance.blob;
|
||||
|
||||
public class FileSystemImageStorageService(
|
||||
IOptions<Databases> systemOptions,
|
||||
IOptionsMonitor<DatabaseConfig> options,
|
||||
IMemoryCache memoryCache
|
||||
) : IBlobStorageService
|
||||
{
|
||||
private readonly string RootPath = systemOptions.Value.BaseDirectory ?? "";
|
||||
private readonly DatabaseConfig config = options.Get(DatabaseConfig.BlobStorage);
|
||||
private readonly IMemoryCache cache = memoryCache;
|
||||
|
||||
private string GetFullPath(string fileName)
|
||||
{
|
||||
// Ensure the directory exists
|
||||
var directory = Path.Join(RootPath, config.SystemContainer, fileName);
|
||||
if (directory != null && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string fileName)
|
||||
{
|
||||
var path = GetFullPath(fileName);
|
||||
if (cache.TryGetValue(path, out Stream cachedStream))
|
||||
{
|
||||
cachedStream.Dispose(); // Dispose the cached stream if it exists
|
||||
cache.Remove(path); // Remove from cache
|
||||
}
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return; // No file to delete
|
||||
}
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Error deleting file {fileName}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Stream?> GetStreamAsync(string fileName)
|
||||
{
|
||||
var path = GetFullPath(fileName);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (cache.TryGetValue(path, out Stream? cachedStream))
|
||||
{
|
||||
return Task.FromResult<Stream?>(cachedStream);
|
||||
}
|
||||
// open the file stream for multiple reads and cache it for performance
|
||||
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
|
||||
cache.CreateEntry(path)
|
||||
.SetValue(fileStream)
|
||||
.SetSlidingExpiration(TimeSpan.FromMinutes(30)); // Cache for 30 minutes
|
||||
|
||||
return Task.FromResult<Stream?>(fileStream);
|
||||
}
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
public async Task<byte[]?> GetBytesAsync(string fileName)
|
||||
{
|
||||
var stream = await GetStreamAsync(fileName);
|
||||
if (stream != null)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Stream blobStream, string fileName)
|
||||
{
|
||||
var path = GetFullPath(fileName);
|
||||
if (cache.TryGetValue(path, out Stream? _) || File.Exists(path))
|
||||
{
|
||||
throw new InvalidOperationException($"File {fileName} already exists. Use Update for updating file info.");
|
||||
}
|
||||
using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
|
||||
await blobStream.CopyToAsync(fileStream);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Stream blobStream, string fileName)
|
||||
{
|
||||
var path = GetFullPath(fileName);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
await DeleteAsync(fileName);
|
||||
}
|
||||
await SaveAsync(blobStream, fileName);
|
||||
}
|
||||
}
|
||||
|
11
back/persistance/blob/IBlobStorageService.cs
Normal file
11
back/persistance/blob/IBlobStorageService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace back.persistance.blob;
|
||||
|
||||
public interface IBlobStorageService
|
||||
{
|
||||
Task SaveAsync(Stream blobStream, string fileName);
|
||||
Task<Stream?> GetStreamAsync(string fileName);
|
||||
Task<byte[]?> GetBytesAsync(string fileName);
|
||||
Task DeleteAsync(string fileName);
|
||||
Task UpdateAsync(Stream blobStream, string fileName);
|
||||
}
|
||||
|
Binary file not shown.
6
back/services/ImageResizer/IImageResizer.cs
Normal file
6
back/services/ImageResizer/IImageResizer.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace back.services.ImageResizer;
|
||||
|
||||
public interface IImageResizer
|
||||
{
|
||||
Task<Stream> ResizeImage(IFormFile image, int v);
|
||||
}
|
23
back/services/ImageResizer/ImageResizer.cs
Normal file
23
back/services/ImageResizer/ImageResizer.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace back.services.ImageResizer;
|
||||
|
||||
public sealed class ImageResizer : IImageResizer
|
||||
{
|
||||
public async Task<Stream> ResizeImage(IFormFile image, int maxRes)
|
||||
{
|
||||
if (image == null || image.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Invalid image file.");
|
||||
}
|
||||
using var inputStream = image.OpenReadStream();
|
||||
using var outputStream = new MemoryStream();
|
||||
using var img = Image.Load(inputStream);
|
||||
|
||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(maxRes, 0), Mode = ResizeMode.Max }));
|
||||
await img.SaveAsWebpAsync(outputStream);
|
||||
outputStream.Position = 0;
|
||||
return outputStream;
|
||||
}
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
nvs latest
|
||||
node .\_fix-svg-stroke.js
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "config.schema.json",
|
||||
"version": "1.0.0",
|
||||
"baseApiUrl": "https://back.mmorales.photo/api",
|
||||
"baseApiUrl": "https://localhost:7273/api",
|
||||
"defaultTheme": "light",
|
||||
"logoAlt": "MMORALES PHOTO",
|
||||
"title": "MMORALES PHOTO",
|
||||
|
@@ -11,6 +11,7 @@ import { EventsView } from './views/events-view/events-view';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeView },
|
||||
{ path: 'home', component: HomeView },
|
||||
{ path: 'login', component: LoginView },
|
||||
{ path: 'tags', component: TagsView },
|
||||
{ path: 'events', component: EventsView },
|
||||
|
@@ -1,14 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { Header } from './global-components/header/header';
|
||||
import { HomeView } from './views/home-view/home-view';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [Header, HomeView, RouterOutlet],
|
||||
imports: [Header, RouterOutlet],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss',
|
||||
})
|
||||
export class App {
|
||||
protected isHomeView = true;
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ header {
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
gap: 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
logo {
|
||||
width: 30%;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ul>
|
||||
@if (gallery.photos) { @for (item of gallery.photos; track $index) {
|
||||
@if (gallery().photos) { @for (item of gallery().photos; track $index) {
|
||||
<li>
|
||||
<img [src]="item.lowResUrl" [alt]="item.title" />
|
||||
</li>
|
||||
|
@@ -0,0 +1,49 @@
|
||||
$gap-size: 20px;
|
||||
|
||||
.low-res-image-list {
|
||||
columns: 4;
|
||||
column-gap: $gap-size;
|
||||
list-style: none;
|
||||
margin-top: 3%;
|
||||
min-height: 100lvh;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Mobile - 1 column */
|
||||
@media (max-width: 639px) {
|
||||
.low-res-image-list {
|
||||
columns: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets in portrait - 3 columns for better balance */
|
||||
@media (min-width: 640px) and (max-width: 1023px) and (orientation: portrait) {
|
||||
.low-res-image-list {
|
||||
columns: 3;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets in landscape - 1 column */
|
||||
@media (min-width: 640px) and (max-width: 1023px) and (orientation: landscape) {
|
||||
.low-res-image-list {
|
||||
columns: 1;
|
||||
max-width: 90%;
|
||||
min-height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop - mosaic with margins */
|
||||
@media (min-width: 1024px) {
|
||||
.low-res-image-list {
|
||||
columns: 4;
|
||||
margin-left: 17svw;
|
||||
margin-right: 17svw;
|
||||
max-width: calc(100vw - 34svw);
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { homeGallery } from '../../../models/gallery/homeGallery';
|
||||
|
||||
@Component({
|
||||
@@ -8,7 +8,7 @@ import { homeGallery } from '../../../models/gallery/homeGallery';
|
||||
styleUrl: './home-view.scss',
|
||||
})
|
||||
export class HomeView implements OnInit {
|
||||
public gallery: homeGallery = new homeGallery();
|
||||
public readonly gallery = signal<homeGallery>(new homeGallery());
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
@@ -26,15 +26,30 @@ export class paginator<T> {
|
||||
private params: Record<string, any> = {};
|
||||
|
||||
private page: number = 1;
|
||||
private totalItems: number = 0;
|
||||
private totalItems: number = 20;
|
||||
private isLoading: boolean = false;
|
||||
private alreadyLoadedPages: Record<number, T[]> = {};
|
||||
|
||||
get totalPages(): number {
|
||||
return Math.ceil(this.totalItems / this.maxItemPerPage);
|
||||
}
|
||||
|
||||
get hasNextPage(): boolean {
|
||||
return this.page < this.totalPages;
|
||||
return this.page <= this.totalPages;
|
||||
}
|
||||
|
||||
public async getPage(number: number): Promise<T[] | null> {
|
||||
if (number < 1) {
|
||||
return this.alreadyLoadedPages[1] || null;
|
||||
}
|
||||
if (number > this.totalPages) {
|
||||
if (this.hasNextPage) {
|
||||
this.loadNextPage();
|
||||
return this.alreadyLoadedPages[number] || null;
|
||||
}
|
||||
return this.alreadyLoadedPages[this.totalPages] || null;
|
||||
}
|
||||
return this.alreadyLoadedPages[number] || null;
|
||||
}
|
||||
|
||||
public async loadNextPage(): Promise<T[] | null> {
|
||||
@@ -54,8 +69,10 @@ export class paginator<T> {
|
||||
this.totalItems = response.headers['x-total-count']
|
||||
? parseInt(response.headers['x-total-count'], 10)
|
||||
: 0;
|
||||
const data = response.data as T[];
|
||||
this.alreadyLoadedPages[this.page] = data;
|
||||
this.page++;
|
||||
return response.data as T[];
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading photos:', error);
|
||||
} finally {
|
||||
|
Reference in New Issue
Block a user