front y back minimos

This commit is contained in:
2025-08-07 19:19:30 +02:00
parent 11825d7be5
commit b677b5f711
52 changed files with 230 additions and 104 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.db
back/data/
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##

View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.8",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

4
back/Constants.cs Normal file
View File

@@ -0,0 +1,4 @@
public static class Constants
{
public const string Data = "data";
}

View File

@@ -0,0 +1,12 @@
namespace back.DTO;
public class PhotoFormModel
{
public required string Title { get; set; }
public string? Description { 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; }
}

View File

@@ -9,13 +9,22 @@ 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=photos.db"));
builder.Services.AddDbContext<PhotoContext>(options => options.UseSqlite($"Data Source={Constants.Data}/photos.db"));
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddSwaggerGen();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -29,6 +38,7 @@ public class Program
app.UseAuthorization();
app.UseCors("AllowAll");
app.MapControllers();

View File

@@ -1,24 +1,14 @@
using back.ApiService.context;
using back.ApiService.models;
using back.DTO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace back.controllers;
public class PhotoFormModel
{
public required string Title { get; set; }
public string? Description { 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; }
}
[Route("api/[controller]")]
[ApiController]
public class PhotoController(PhotoContext photoContext) : ControllerBase
public class PhotosController(PhotoContext photoContext) : ControllerBase
{
private readonly PhotoContext _photoContext = photoContext;
@@ -42,13 +32,28 @@ public class PhotoController(PhotoContext photoContext) : ControllerBase
}
// GET api/<PhotoController>/5
[HttpGet("{id}")]
public async Task<ActionResult<Photo>> Get(Guid id)
[HttpGet("{id}/{res}")]
public async Task<IActionResult> Get(Guid id, string res = "low")
{
var photo = await _photoContext.Photos.FindAsync(id);
if (photo == null)
return NotFound();
return photo;
string? filePath = res.ToLower() switch
{
"low" => photo.LowResUrl,
"mid" => photo.MidResUrl,
"high" => photo.HighResUrl,
_ => null
};
if (filePath == null || !System.IO.File.Exists(Path.Combine(Constants.Data, filePath)))
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);
}
// POST api/<PhotoController>

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
@@ -10,29 +11,33 @@ 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 fileName = $"{Guid.NewGuid()}{Path.GetExtension(image.FileName)}";
var photo = new Photo(title, description, tags, personsIn, fileName);
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(Photo.LowResFolder);
Directory.CreateDirectory(Photo.MidResFolder);
Directory.CreateDirectory(Photo.HighResFolder);
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(photo.LowResUrl);
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(photo.MidResUrl);
img.Save(Path.Join(Constants.Data, photo.MidResUrl));
// Original
stream.Position = 0;
using var original = Image.Load(stream);
original.Save(photo.HighResUrl);
original.Save(Path.Join(Constants.Data, photo.HighResUrl));
return photo;
}
@@ -45,7 +50,7 @@ public class Photo
public const string MidResFolder = "imgs/mid";
public const string HighResFolder = "imgs/high";
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }

BIN
back/photos.db-shm Normal file

Binary file not shown.

0
back/photos.db-wal Normal file
View File

View File

@@ -0,0 +1,3 @@
{
"bff": "https://localhost:7273"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 MiB

View File

@@ -1,7 +1,7 @@
<header class="main-header">
<div class="header-container">
<!-- Logo/Marca -->
<div class="logo">
<div class="logo" (click)="navigateTo('home')" style="cursor:pointer;">
<h1>{{ brandName }}</h1>
</div>

View File

@@ -8,11 +8,11 @@
left: 0;
right: 0;
z-index: 1000;
background: rgba(variables.$primary-white, 0.95);
background: variables.$primary-white;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
border-bottom: 1px solid variables.$border-grey;
box-shadow: 0 4px 20px rgba(42, 41, 38, 0.1);
border-bottom: 2px solid variables.$border-grey;
box-shadow: 0 6px 10px rgba(42, 41, 38, 0.15);
width: 100%;
.header-container {
@@ -133,11 +133,12 @@
}
.main-content {
// margin-top: 0;
// margin-top: calc(
// 80px + (gallery.$gap-size * 2)
// ); // Space for fixed header + 2rem
min-height: calc(100vh - 80px);
padding: 0;
margin: 0;
width: 100%;
overflow-x: hidden; /* Sin scroll horizontal */
overflow-y: scroll; /* Permite scroll vertical */
}
// Responsive design
@@ -175,8 +176,11 @@
}
.main-content {
margin-top: calc(70px + 2rem);
margin-top: 0;
padding: 0;
min-height: calc(100vh - 70px);
width: 100%;
overflow-x: hidden;
}
}
@@ -205,5 +209,7 @@
.main-content {
margin-top: calc(120px + 2rem);
min-height: calc(100vh - 120px);
width: 100%;
overflow-x: hidden;
}
}

View File

@@ -5,6 +5,7 @@ import { Gallery } from './gallery/gallery';
import { Button } from './button/button';
import { SvgLoader } from './svg/svg';
import { UploadImageForm } from './upload-image-form/upload-image-form';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
@@ -29,7 +30,11 @@ export class App implements OnInit {
protected searchQuery = '';
protected isMenuOpen = false;
constructor(private titleService: Title, private metaService: Meta) {}
constructor(
private titleService: Title,
private metaService: Meta,
private router: Router
) {}
ngOnInit(): void {
// Set page meta information
@@ -85,8 +90,10 @@ export class App implements OnInit {
navigateTo(section: string): void {
this.isMenuOpen = false; // Close menu after navigation
// TODO: Implement navigation logic
console.log('Navigating to:', section);
this.showUploadForm = false;
if (section === 'home') {
this.router.navigate(['/']);
}
}
// Close menu when clicking outside

View File

@@ -5,8 +5,10 @@ $gap-size: 20px;
column-gap: $gap-size;
list-style: none;
margin-top: 3%;
min-height: 100vh;
min-height: 100lvh;
box-sizing: border-box;
width: 100%;
padding: 0;
}
/* Mobile - 1 column */
@@ -39,7 +41,9 @@ $gap-size: 20px;
@media (min-width: 1024px) {
.low-res-image-list {
columns: 4;
margin-left: 17lvw;
margin-right: 17lvw;
margin-left: 17svw;
margin-right: 17svw;
max-width: calc(100vw - 34svw);
overflow: visible;
}
}

View File

@@ -2,15 +2,16 @@ import { Component, computed } from '@angular/core';
import { LowResImage } from '../low-res-image/low-res-image';
import { Observable } from 'rxjs';
import { MidResImage } from '../mid-res-image/mid-res-image';
import axios from 'axios';
@Component({
selector: 'gallery',
imports: [LowResImage, MidResImage],
templateUrl: './gallery.html',
styleUrl: './gallery.scss'
styleUrl: './gallery.scss',
})
export class Gallery {
protected images : LowResImage[] = [];
protected images: LowResImage[] = [];
thereIsFullscreenImage: boolean = false;
midResImage: MidResImage | null = null;
isAnimatingToFullscreen: boolean = false;
@@ -35,7 +36,10 @@ export class Gallery {
}, 100);
}
onClickedImageWithEvent(data: {midResImage: MidResImage, event: MouseEvent}): void {
onClickedImageWithEvent(data: {
midResImage: MidResImage;
event: MouseEvent;
}): void {
this.onClickedImage(data.midResImage, data.event);
}
@@ -46,13 +50,13 @@ export class Gallery {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
width: rect.width,
height: rect.height
height: rect.height,
};
// Guardar información de transición en el document para uso del mid-res-image
(document as any).__imageTransition = {
...initialPosition,
timestamp: Date.now()
timestamp: Date.now(),
};
}
@@ -64,17 +68,37 @@ export class Gallery {
public loadImages(): void {
// when backend is online ask for photos // return this.http.get<string[]>('/api/images');
// meanwhile, load examples from assets/fotosPrueba
this.images = this.getImages();
this.getImages();
}
private getImages(): LowResImage[] {
var exampleImages : LowResImage[] = []
for (let i = 1; i <= 33; i++) {
const img = new LowResImage();
img.src = `assets/fotosPrueba/${i}.jpg`;
img.alt = `Image ${i}`;
exampleImages.push(img);
}
return exampleImages;
private getImages(): void {
// hacer un get /api/photos para coger la primera página de fotos
axios.get('/photos').then((response) => {
const photos = response.data;
this.images = photos.map((photo: PhotoDTO) => {
let p = new LowResImage();
p.id = photo.id;
p.src = axios.defaults.baseURL + '/photos/' + photo.id + '/low';
p.alt = photo.description;
return p;
});
});
}
}
class PhotoDTO {
public id: string = '';
public src: string = '';
public alt: string = '';
public description: string = '';
public title: string = '';
public tags: string = '';
public people: string = '';
public createdAt: Date | null = null;
public updatedAt: Date | null = null;
public createdBy: Date | null = null;
public updatedBy: Date | null = null;
public evento: string | null = null;
public ubicacion: string | null = null;
public ranking: number | null = null;
}

View File

@@ -5,20 +5,23 @@ import { MidResImage } from '../mid-res-image/mid-res-image';
selector: 'low-res-image',
imports: [],
templateUrl: './low-res-image.html',
styleUrl: './low-res-image.scss'
styleUrl: './low-res-image.scss',
})
export class LowResImage {
@Input({ required: true }) src: string = '';
@Input({ required: true }) alt: string = '';
public id : string = 'low-res-image-' + Math.random().toString(36).substring(2, 15);
public id: string | null = null;
@Output() clicked = new EventEmitter<{midResImage: MidResImage, event: MouseEvent}>();
@Output() clicked = new EventEmitter<{
midResImage: MidResImage;
event: MouseEvent;
}>();
openFullscreen(event: MouseEvent): void {
event.stopPropagation();
const midResImage = new MidResImage();
midResImage.src = this.src.replace('low', 'mid');
midResImage.alt = this.alt;
this.clicked.emit({midResImage, event});
this.clicked.emit({ midResImage, event });
}
}

View File

@@ -31,18 +31,12 @@ export class UploadImageForm {
// Here you would typically send the formData to your backend API
axios
.post('https://localhost:7273/api/photo', formData, {
.post('/photos', formData, {
headers: {},
validateStatus: (status) => status == 201,
})
.then((response) => {
console.log('Upload successful:', response.data);
})
.catch((error) => {
console.error('Upload failed:', error);
});
.then(() => {
this.close.emit();
});
}
}

View File

@@ -1,6 +1,12 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import axios from 'axios';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
fetch('/assets/config.json')
.then((response) => response.json())
.then((config) => {
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
axios.defaults.baseURL = config.bff + '/api'; // Configurar base URL del BFF desde el archivo de configuración
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
});

View File

@@ -1,4 +1,9 @@
/* You can add global styles to this file, and also import other style files */
::-webkit-scrollbar {
width: 0px;
background: transparent;
}
html,
body {
width: 100%;
@@ -6,5 +11,29 @@ body {
margin: 0;
padding: 0;
overflow-x: hidden; /* Elimina scroll horizontal */
overflow-y: scroll; /* Permite scroll vertical */
scroll-behavior: smooth;
box-sizing: border-box;
// /* Estilos personalizados para la barra de scroll */
// ::-webkit-scrollbar-track {
// background: #f1f1f1;
// border-radius: 4px;
// }
// ::-webkit-scrollbar-thumb {
// background: #c1c1c1;
// border-radius: 4px;
// transition: background 0.3s ease;
// }
// ::-webkit-scrollbar-thumb:hover {
// background: #a8a8a8;
// }
// /* Para Firefox */
// body {
// scrollbar-width: thin;
// scrollbar-color: #c1c1c1 #f1f1f1;
// }
}