This commit is contained in:
2025-08-09 03:38:22 +02:00
parent b677b5f711
commit 679427a6f9
280 changed files with 12578 additions and 47 deletions

View File

@@ -0,0 +1,9 @@
export class dataApi {
static readonly Photos = {
getAll: '/photos',
getById: (id: string) => `/photos/${id}`,
create: '/photos',
update: (id: string) => `/photos/${id}`,
delete: (id: string) => `/photos/${id}`,
};
}

View File

@@ -0,0 +1,12 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
]
};

View File

@@ -0,0 +1,2 @@
<custom-header></custom-header>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,23 @@
import { Routes } from '@angular/router';
import { HomeView } from './views/home-view/home-view';
import { LoginView } from './views/login-view/login-view';
import { UserProfileView } from './views/user-profile-view/user-profile-view';
import { AdminView } from './views/admin-view/admin-view';
import { ContentManagerView } from './views/content-manager-view/content-manager-view';
import { ServicesView } from './views/services-view/services-view';
import { UserGalleriesView } from './views/user-galleries-view/user-galleries-view';
import { TagsView } from './views/tags-view/tags-view';
import { EventsView } from './views/events-view/events-view';
export const routes: Routes = [
{ path: '', component: HomeView },
{ path: 'login', component: LoginView },
{ path: 'tags', component: TagsView },
{ path: 'events', component: EventsView },
{ path: 'services', component: ServicesView },
{ path: 'my-profile', component: UserProfileView },
{ path: 'my-galleries', component: UserGalleriesView },
{ path: 'content-management', component: ContentManagerView },
{ path: 'admin', component: AdminView },
{ path: '**', redirectTo: '' },
];

View File

View File

@@ -0,0 +1,23 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, v2');
});
});

14
front/v2/src/app/app.ts Normal file
View File

@@ -0,0 +1,14 @@
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],
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App {
protected isHomeView = true;
}

View File

@@ -0,0 +1,3 @@
<div class="admin-panel-link">
<a href="/admin">Admin</a>
</div>

View File

@@ -0,0 +1,79 @@
@use "../../../styles/variables" as *;
.admin-panel-link {
a {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
text-decoration: none;
color: $text-dark;
font-size: 0.95rem;
font-weight: 500;
letter-spacing: 0.5px;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
// Hover effect with special admin color
&:hover {
color: $warning;
background: rgba($warning, 0.08);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba($warning, 0.15);
}
// Active state
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba($warning, 0.2);
}
// Focus state for accessibility
&:focus {
outline: 2px solid $warning;
outline-offset: 2px;
}
// Subtle animation on hover with admin color
&::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba($warning, 0.1),
transparent
);
transition: left 0.5s ease;
}
&:hover::before {
left: 100%;
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.admin-panel-link {
a {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
}
}
}
@media (max-width: 480px) {
.admin-panel-link {
a {
padding: 0.5rem 1rem;
font-size: 0.85rem;
letter-spacing: 0.25px;
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminPanelLink } from './admin-panel-link';
describe('AdminPanelLink', () => {
let component: AdminPanelLink;
let fixture: ComponentFixture<AdminPanelLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminPanelLink]
})
.compileComponents();
fixture = TestBed.createComponent(AdminPanelLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'admin-panel-link',
imports: [],
templateUrl: './admin-panel-link.html',
styleUrl: './admin-panel-link.scss'
})
export class AdminPanelLink {
}

View File

@@ -0,0 +1,3 @@
<div class="content-manager-panel-link">
<a href="/content-manager">Contenido</a>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentManagerPanelLink } from './content-manager-panel-link';
describe('ContentManagerPanelLink', () => {
let component: ContentManagerPanelLink;
let fixture: ComponentFixture<ContentManagerPanelLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContentManagerPanelLink]
})
.compileComponents();
fixture = TestBed.createComponent(ContentManagerPanelLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'content-manager-panel-link',
imports: [],
templateUrl: './content-manager-panel-link.html',
styleUrl: './content-manager-panel-link.scss'
})
export class ContentManagerPanelLink {
}

View File

@@ -0,0 +1,6 @@
<svg-button
label="Eventos"
routerLink="/events"
textAtRight="Eventos"
icon="assets/icons/calendar-day-svgrepo-com.svg"
></svg-button>

After

Width:  |  Height:  |  Size: 143 B

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EventsLink } from './events-link';
describe('EventsLink', () => {
let component: EventsLink;
let fixture: ComponentFixture<EventsLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EventsLink]
})
.compileComponents();
fixture = TestBed.createComponent(EventsLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'events-link',
imports: [SvgButton],
templateUrl: './events-link.html',
styleUrl: './events-link.scss',
})
export class EventsLink {}

View File

@@ -0,0 +1,17 @@
<header>
<logo></logo>
<events-link></events-link>
<tags-link></tags-link>
<services-link></services-link>
@if (user.isLoggedIn) {
<user-galleries-link></user-galleries-link>
} @if (user.isContentManager) {
<content-manager-panel-link></content-manager-panel-link>
} @if (user.isAdmin) {
<admin-panel-link></admin-panel-link>
} @if (user.isLoggedIn) {
<user-profile-link></user-profile-link>
} @else {
<login-link></login-link>
}
</header>

View File

@@ -0,0 +1,25 @@
header {
position: sticky;
top: 0;
z-index: 1000;
background: transparent;
margin: 0;
padding: 0;
width: 100%;
height: 7%;
display: flex;
flex-direction: row;
align-items: baseline;
align-content: center;
justify-content: space-evenly;
gap: 0;
logo {
width: 10%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
order: 0;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Header } from './header';
describe('Header', () => {
let component: Header;
let fixture: ComponentFixture<Header>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Header]
})
.compileComponents();
fixture = TestBed.createComponent(Header);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,45 @@
import { Component } from '@angular/core';
import { Logo } from '../logo/logo';
import { EventsLink } from '../events-link/events-link';
import { UserGalleriesLink } from '../user-galleries-link/user-galleries-link';
import { ContentManagerPanelLink } from '../content-manager-panel-link/content-manager-panel-link';
import { AdminPanelLink } from '../admin-panel-link/admin-panel-link';
import { UserProfileLink } from '../user-profile-link/user-profile-link';
import { LoginLink } from '../login-link/login-link';
import { TagsLink } from '../tags-link/tags-link';
import { userService } from '../../services/userService/userService';
import { OnInit } from '@angular/core';
import { userModel } from '../../../models/userModel';
import { ServicesLink } from '../services-link/services-link';
@Component({
selector: 'custom-header',
imports: [
Logo,
EventsLink,
UserGalleriesLink,
ContentManagerPanelLink,
AdminPanelLink,
UserProfileLink,
LoginLink,
TagsLink,
ServicesLink,
],
templateUrl: './header.html',
styleUrl: './header.scss',
})
export class Header implements OnInit {
constructor(private userService: userService) {}
private currentUser: userModel = userModel.DefaultUser;
ngOnInit() {
this.userService.getUser().subscribe((user) => {
this.currentUser = user;
});
}
get user() {
return this.currentUser;
}
}

View File

@@ -0,0 +1,7 @@
<svg-button
label="login-link"
class="login-button"
routerLink="/login"
textAtRight="Login"
icon="assets/icons/door-open-svgrepo-com.svg"
></svg-button>

After

Width:  |  Height:  |  Size: 163 B

View File

@@ -0,0 +1,68 @@
.login-button {
// Variables para el lenguaje visual (SCSS)
$primary-white: #fefcf8; // Blanco ahuevado gentil
$border-grey: #e0ddd8; // Gris suave para bordes
$text-dark: #2a2926; // Texto principal
$standard-button-icon: #8d8d8d; // Texto principal
$standard-button-border: #b8b8b891; // Texto principal
$standard-button-hovered: #494949; // Texto principal
$text-muted: #706b63; // Texto secundario
svg-button {
button {
background: $primary-white;
border: 2px solid $standard-button-border;
color: $standard-button-icon;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
min-width: 3.2rem;
height: 3.2rem;
margin: 0;
padding: 0;
flex-shrink: 1;
display: flex;
flex-wrap: nowrap;
align-items: center;
align-content: center;
justify-content: space-evenly;
padding: 5%;
label {
text-align: center;
flex-grow: 1;
}
&:hover {
color: $standard-button-hovered;
transform: scale(1.07);
box-shadow: 0 6px 16px rgba(42, 41, 38, 0.1);
}
&:disabled {
background: $primary-white;
color: $text-muted;
}
svg-loader {
box-sizing: border-box;
pointer-events: none;
display: block;
height: 80%;
width: 80%;
margin: 0;
padding: 0;
}
&:hover {
svg-loader {
fill: $primary-white;
}
label {
color: $primary-white;
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginLink } from './login-link';
describe('LoginLink', () => {
let component: LoginLink;
let fixture: ComponentFixture<LoginLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginLink]
})
.compileComponents();
fixture = TestBed.createComponent(LoginLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'login-link',
imports: [SvgButton],
templateUrl: './login-link.html',
styleUrl: './login-link.scss',
})
export class LoginLink {}

View File

@@ -0,0 +1,3 @@
<a routerLink="/">
<img [src]="logoSrc" alt="Logo" />
</a>

View File

@@ -0,0 +1,6 @@
a {
img {
width: 100%;
height: auto;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logo } from './logo';
describe('Logo', () => {
let component: Logo;
let fixture: ComponentFixture<Logo>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Logo]
})
.compileComponents();
fixture = TestBed.createComponent(Logo);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { config } from '../../../models/config';
@Component({
selector: 'logo',
imports: [],
templateUrl: './logo.html',
styleUrl: './logo.scss',
})
export class Logo {
protected logoSrc = config.get().logoSrc;
}

View File

@@ -0,0 +1,6 @@
<svg-button
label="Servicios"
routerLink="/services"
textAtRight="Servicios"
icon="assets/icons/book-section-svgrepo-com.svg"
></svg-button>

After

Width:  |  Height:  |  Size: 149 B

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ServicesLink } from './services-link';
describe('ServicesLink', () => {
let component: ServicesLink;
let fixture: ComponentFixture<ServicesLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ServicesLink]
})
.compileComponents();
fixture = TestBed.createComponent(ServicesLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'services-link',
imports: [SvgButton],
templateUrl: './services-link.html',
styleUrl: './services-link.scss',
})
export class ServicesLink {}

View File

@@ -0,0 +1,6 @@
<svg-button
label="tags-link"
routerLink="/tags"
textAtRight="Etiquetas"
icon="assets/icons/tags-svgrepo-com.svg"
></svg-button>

After

Width:  |  Height:  |  Size: 137 B

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TagsLink } from './tags-link';
describe('TagsLink', () => {
let component: TagsLink;
let fixture: ComponentFixture<TagsLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TagsLink]
})
.compileComponents();
fixture = TestBed.createComponent(TagsLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'tags-link',
imports: [SvgButton],
templateUrl: './tags-link.html',
styleUrl: './tags-link.scss',
})
export class TagsLink {}

View File

@@ -0,0 +1,3 @@
<div class="user-galleries-link">
<a href="/galeria">Galería</a>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserGalleriesLink } from './user-galleries-link';
describe('UserGalleriesLink', () => {
let component: UserGalleriesLink;
let fixture: ComponentFixture<UserGalleriesLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserGalleriesLink]
})
.compileComponents();
fixture = TestBed.createComponent(UserGalleriesLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'user-galleries-link',
imports: [],
templateUrl: './user-galleries-link.html',
styleUrl: './user-galleries-link.scss'
})
export class UserGalleriesLink {
}

View File

@@ -0,0 +1,3 @@
<div class="user-profile-link">
<a href="/perfil">Perfil</a>
</div>

View File

@@ -0,0 +1,79 @@
@use "../../../styles/variables" as *;
.user-profile-link {
a {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
text-decoration: none;
color: $text-dark;
font-size: 0.95rem;
font-weight: 500;
letter-spacing: 0.5px;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
// Hover effect
&:hover {
color: $primary-accent;
background: rgba($primary-accent, 0.08);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba($primary-accent, 0.15);
}
// Active state
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba($primary-accent, 0.2);
}
// Focus state for accessibility
&:focus {
outline: 2px solid $primary-accent;
outline-offset: 2px;
}
// Subtle animation on hover
&::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba($primary-accent, 0.1),
transparent
);
transition: left 0.5s ease;
}
&:hover::before {
left: 100%;
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.user-profile-link {
a {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
}
}
}
@media (max-width: 480px) {
.user-profile-link {
a {
padding: 0.5rem 1rem;
font-size: 0.85rem;
letter-spacing: 0.25px;
}
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserProfileLink } from './user-profile-link';
describe('UserProfileLink', () => {
let component: UserProfileLink;
let fixture: ComponentFixture<UserProfileLink>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserProfileLink]
})
.compileComponents();
fixture = TestBed.createComponent(UserProfileLink);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'user-profile-link',
imports: [],
templateUrl: './user-profile-link.html',
styleUrl: './user-profile-link.scss'
})
export class UserProfileLink {
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { Theme } from './theme';
describe('Theme', () => {
let service: Theme;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(Theme);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class Theme {
private isDarkTheme: boolean = false;
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.applyTheme();
}
isDark() {
return this.isDarkTheme;
}
private applyTheme() {
const theme = this.isDarkTheme ? 'dark' : 'light';
document.body.classList.remove('light-theme', 'dark-theme');
document.body.classList.add(`${theme}-theme`);
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { userService } from './userService';
describe('userService', () => {
let service: userService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(userService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { userModel } from '../../../models/userModel';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class userService {
private currentUser: userModel = userModel.DefaultUser;
private userSubject = new BehaviorSubject<userModel>(this.currentUser);
setUser(user: userModel) {
this.currentUser = user;
this.userSubject.next(user);
}
getUser() {
return this.userSubject.asObservable();
}
}

View File

@@ -0,0 +1 @@
<p>admin-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminView } from './admin-view';
describe('AdminView', () => {
let component: AdminView;
let fixture: ComponentFixture<AdminView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminView]
})
.compileComponents();
fixture = TestBed.createComponent(AdminView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'admin-view',
imports: [],
templateUrl: './admin-view.html',
styleUrl: './admin-view.scss'
})
export class AdminView {
}

View File

@@ -0,0 +1 @@
<p>content-manager-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentManagerView } from './content-manager-view';
describe('ContentManagerView', () => {
let component: ContentManagerView;
let fixture: ComponentFixture<ContentManagerView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContentManagerView]
})
.compileComponents();
fixture = TestBed.createComponent(ContentManagerView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'content-manager-view',
imports: [],
templateUrl: './content-manager-view.html',
styleUrl: './content-manager-view.scss'
})
export class ContentManagerView {
}

View File

@@ -0,0 +1 @@
<p>events-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EventsView } from './events-view';
describe('EventsView', () => {
let component: EventsView;
let fixture: ComponentFixture<EventsView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EventsView]
})
.compileComponents();
fixture = TestBed.createComponent(EventsView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'events-view',
imports: [],
templateUrl: './events-view.html',
styleUrl: './events-view.scss'
})
export class EventsView {
}

View File

@@ -0,0 +1,7 @@
<ul>
@if (gallery.photos) { @for (item of gallery.photos; track $index) {
<li>
<img [src]="item.lowResUrl" [alt]="item.title" />
</li>
} }
</ul>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeView } from './home-view';
describe('HomeView', () => {
let component: HomeView;
let fixture: ComponentFixture<HomeView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeView]
})
.compileComponents();
fixture = TestBed.createComponent(HomeView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { homeGallery } from '../../../models/gallery/homeGallery';
@Component({
selector: 'home-view',
imports: [],
templateUrl: './home-view.html',
styleUrl: './home-view.scss',
})
export class HomeView implements OnInit {
public gallery: homeGallery = new homeGallery();
ngOnInit() {}
}

View File

@@ -0,0 +1 @@
<p>login-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginView } from './login-view';
describe('LoginView', () => {
let component: LoginView;
let fixture: ComponentFixture<LoginView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginView]
})
.compileComponents();
fixture = TestBed.createComponent(LoginView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'login-view',
imports: [],
templateUrl: './login-view.html',
styleUrl: './login-view.scss'
})
export class LoginView {
}

View File

@@ -0,0 +1 @@
<p>services-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ServicesView } from './services-view';
describe('ServicesView', () => {
let component: ServicesView;
let fixture: ComponentFixture<ServicesView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ServicesView]
})
.compileComponents();
fixture = TestBed.createComponent(ServicesView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'services-view',
imports: [],
templateUrl: './services-view.html',
styleUrl: './services-view.scss'
})
export class ServicesView {
}

View File

@@ -0,0 +1 @@
<p>tags-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TagsView } from './tags-view';
describe('TagsView', () => {
let component: TagsView;
let fixture: ComponentFixture<TagsView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TagsView]
})
.compileComponents();
fixture = TestBed.createComponent(TagsView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'tags-view',
imports: [],
templateUrl: './tags-view.html',
styleUrl: './tags-view.scss'
})
export class TagsView {
}

View File

@@ -0,0 +1 @@
<p>user-galleries-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserGalleriesView } from './user-galleries-view';
describe('UserGalleriesView', () => {
let component: UserGalleriesView;
let fixture: ComponentFixture<UserGalleriesView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserGalleriesView]
})
.compileComponents();
fixture = TestBed.createComponent(UserGalleriesView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'user-galleries-view',
imports: [],
templateUrl: './user-galleries-view.html',
styleUrl: './user-galleries-view.scss'
})
export class UserGalleriesView {
}

View File

@@ -0,0 +1 @@
<p>user-profile-view works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserProfileView } from './user-profile-view';
describe('UserProfileView', () => {
let component: UserProfileView;
let fixture: ComponentFixture<UserProfileView>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserProfileView]
})
.compileComponents();
fixture = TestBed.createComponent(UserProfileView);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'user-profile-view',
imports: [],
templateUrl: './user-profile-view.html',
styleUrl: './user-profile-view.scss'
})
export class UserProfileView {
}

11
front/v2/src/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="es" translate="no">
<head>
<meta charset="utf-8" />
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<app-root></app-root>
</body>
</html>

17
front/v2/src/main.ts Normal file
View File

@@ -0,0 +1,17 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import { config } from './models/config';
import axios from 'axios';
async function main() {
await config.load();
const publicConfig: config = config.get();
publicConfig.configure();
// update axios base url
axios.defaults.baseURL = publicConfig.baseApiUrl || undefined;
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
}
main();

View File

@@ -0,0 +1,130 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class config {
private constructor() {} // Evita instanciación
private static instance: config | null = null;
private dnsPrefecthBackend() {
const link = document.createElement('link');
link.rel = 'dns-prefetch';
link.href = config.instance!.baseApiUrl || 'http://localhost:3000/api';
document.head.appendChild(link);
}
private setIcon() {
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/x-icon';
link.href = config.instance!.faviconSrc || 'default.ico';
document.head.appendChild(link);
}
private setMetaTags() {
if (config.instance!.metaTags) {
for (const [key, value] of Object.entries(config.instance!.metaTags!)) {
const meta = document.createElement('meta');
meta.name = key;
meta.content = value || '';
document.head.appendChild(meta);
}
}
}
private setFonts() {
// update font
config.instance!.fonts.preconnect.forEach((font) => {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = font.href;
if (font.crossorigin) {
link.crossOrigin = 'anonymous';
}
document.head.appendChild(link);
});
config.instance!.fonts.stylesheets.forEach((stylesheet) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = stylesheet;
document.head.appendChild(link);
});
}
public configure() {
document.title = config.instance!.title || 'Default Title';
this.dnsPrefecthBackend();
this.setIcon();
this.setMetaTags();
this.setFonts();
}
static get(): config {
if (config.instance) {
return config.instance!;
}
throw new Error('Config not loaded. Call config.load() first.');
}
static async load(): Promise<config | null> {
if (config.instance === null) {
config.instance = new config();
const response = await fetch('/config.json');
const data = await response.json();
config.instance.version = data.version || '1.0.0';
config.instance.baseApiUrl =
data.baseApiUrl || 'http://localhost:3000/api';
config.instance.defaultTheme = data.defaultTheme || 'light';
config.instance.logoSrc = data.logoSrc || '/assets/logo.png';
config.instance.logoAlt = data.logoAlt || 'Logo';
config.instance.faviconSrc = data.faviconSrc || '/assets/favicon.ico';
config.instance.title = data.title || 'My Application';
config.instance.metaTags = {
description: data.metaTags?.description || 'Default description',
keywords: data.metaTags?.keywords || 'default,keywords',
author: data.metaTags?.author || 'Default Author',
};
config.instance.socialLinks = {
facebook: data.socialLinks?.facebook || null,
twitter: data.socialLinks?.twitter || null,
instagram: data.socialLinks?.instagram || null,
};
config.instance.contactEmail = data.contactEmail || null;
config.instance.fonts = {
preconnect: data.fonts?.preconnect || [],
stylesheets: data.fonts?.stylesheets || [],
};
}
return config.instance;
}
public fonts: {
preconnect: {
href: string;
crossorigin?: boolean;
type?: string;
}[];
stylesheets: string[];
} = {
preconnect: [],
stylesheets: [],
};
public version: string | null = null;
public baseApiUrl: string | null = null;
public defaultTheme: string | null = null;
public logoSrc: string | null = null;
public logoAlt: string | null = null;
public faviconSrc: string | null = null;
public title: string | null = null;
public metaTags: {
description: string | null;
keywords: string | null;
author: string | null;
} | null = null;
public socialLinks: {
facebook: string | null;
twitter: string | null;
instagram: string | null;
} | null = null;
public contactEmail: string | null = null;
}

View File

@@ -0,0 +1,16 @@
import { tagModel } from './tagModel';
export class eventModel {
constructor(
public id: string,
public title: string,
public description: string,
public date: Date,
public location: string,
public relatedTags: tagModel[] = [],
public createdAt: Date,
public updatedAt: Date,
public createdBy: string,
public updatedBy: string
) {}
}

View File

@@ -0,0 +1,67 @@
import { galleryModel } from './galleryModel';
import { homeGallery } from './homeGallery';
import { photoModel } from '../photoModel';
import { eventModel } from '../eventModel';
import { tagModel } from '../tagModel';
import { personModel } from '../personModel';
import { v4 as uuidv4 } from 'uuid';
type galleryArgs = {
title?: string | null;
description?: string | null;
createdBy?: string | null;
photos?: photoModel[];
isPublic?: boolean;
isArchived?: boolean;
isFavorite?: boolean;
event?: eventModel | null;
tags?: tagModel[] | null;
personsInvolved?: personModel[] | null;
usersWhoCanSee?: personModel[] | null;
};
export class galleryFactory {
public static createEmptyGallery(): galleryModel {
return this.createGallery({});
}
public static getHomeGallery(): galleryModel {
return new homeGallery();
}
public static createGallery(
{
title = null,
description = null,
createdBy = null,
photos = [],
isPublic = true,
isArchived = false,
isFavorite = false,
event = null,
tags = null,
personsInvolved = null,
usersWhoCanSee = null,
} = {} as galleryArgs
): galleryModel {
const now = new Date();
return new galleryModel({
id: uuidv4(),
title,
description,
createdAt: now,
updatedAt: now,
createdBy,
updatedBy: createdBy,
photos,
isPublic,
isArchived,
isFavorite,
event,
tags,
personsInvolved,
usersWhoCanSee,
});
}
}

View File

@@ -0,0 +1,98 @@
import { dataApi } from '../../apiContracts/dataApi';
import { paginator } from '../../utils/paginator';
import { eventModel } from '../eventModel';
import { personModel } from '../personModel';
import { photoModel } from '../photoModel';
import { tagModel } from '../tagModel';
type galleryArgs = {
id: string;
title?: string | null;
description?: string | null;
createdAt?: Date | null;
updatedAt?: Date | null;
createdBy?: string | null;
updatedBy?: string | null;
photos?: photoModel[];
isPublic?: boolean;
isArchived?: boolean;
isFavorite?: boolean;
event?: eventModel | null;
tags?: tagModel[] | null;
personsInvolved?: personModel[] | null;
usersWhoCanSee?: personModel[] | null;
};
export class galleryModel {
constructor({
id,
title,
description,
createdAt,
updatedAt,
createdBy,
updatedBy,
photos,
isPublic,
isArchived,
isFavorite,
event,
tags,
personsInvolved,
usersWhoCanSee,
}: galleryArgs) {
this.id = id;
this.title = title;
this.description = description;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.createdBy = createdBy;
this.updatedBy = updatedBy;
this.photos = photos || [];
this.isPublic = isPublic;
this.isArchived = isArchived;
this.isFavorite = isFavorite;
this.event = event;
this.tags = tags;
this.personsInvolved = personsInvolved;
this.usersWhoCanSee = usersWhoCanSee;
this.loadNextPage();
}
public id: string;
public title?: string | null = null;
public description?: string | null = null;
public createdAt?: Date | null = null;
public updatedAt?: Date | null = null;
public createdBy?: string | null = null;
public updatedBy?: string | null = null;
public photos: photoModel[] = [];
public isPublic?: boolean = true;
public isArchived?: boolean = false;
public isFavorite?: boolean = false;
public event?: eventModel | null = null;
public tags?: tagModel[] | null = null;
public personsInvolved?: personModel[] | null = null;
public usersWhoCanSee?: personModel[] | null = null;
private paginator = new paginator<photoModel>({
url: dataApi.Photos.getAll,
});
public addPhotos(photos: photoModel[]) {
if (!this.photos) {
this.photos = [];
}
this.photos.push(...photos);
}
public loadNextPage() {
const nextPage = this.paginator.loadNextPage();
nextPage.then((photos) => {
if (photos) {
this.addPhotos(photos);
}
});
}
}

View File

@@ -0,0 +1,12 @@
import { galleryModel } from './galleryModel';
import { v4 as uuidv4 } from 'uuid';
export class homeGallery extends galleryModel {
constructor() {
super({
id: uuidv4(),
title: 'Home Gallery',
description: 'This is the home gallery.',
});
}
}

View File

@@ -0,0 +1,67 @@
export class permissionModel {
constructor(
public id: string,
public name: string,
public description: string
) {}
static ViewContentPermission = new permissionModel(
'1',
'VIEW_CONTENT',
'Permission to view content'
);
static LikeContentPermission = new permissionModel(
'2',
'LIKE_CONTENT',
'Permission to like content'
);
static EditContentPermission = new permissionModel(
'3',
'EDIT_CONTENT',
'Permission to edit content'
);
static DeleteContentPermission = new permissionModel(
'4',
'DELETE_CONTENT',
'Permission to delete content'
);
static CreateContentPermission = new permissionModel(
'5',
'CREATE_CONTENT',
'Permission to create new content'
);
static EditUserPermission = new permissionModel(
'6',
'EDIT_USER',
'Permission to edit user'
);
static DeleteUserPermission = new permissionModel(
'7',
'DELETE_USER',
'Permission to delete user'
);
static DisableUserPermission = new permissionModel(
'8',
'DISABLE_USER',
'Permission to disable user'
);
static CreateUserPermission = new permissionModel(
'9',
'CREATE_USER',
'Permission to create new user'
);
static EditWebConfigPermission = new permissionModel(
'10',
'EDIT_WEB_CONFIG',
'Permission to edit web configuration'
);
}

View File

@@ -0,0 +1,64 @@
type socialMediaType = {
facebook: string | null;
instagram: string | null;
twitter: string | null;
blueSky: string | null;
tiktok: string | null;
linkedin: string | null;
pinterest: string | null;
discord: string | null;
reddit: string | null;
other: string | null;
};
type personModelType = {
id: string;
name: string;
profilePicture?: string | null;
avatar?: string | null;
socialMedia?: socialMediaType | null;
};
export class personModel {
constructor({
id,
name,
profilePicture,
avatar,
socialMedia,
}: personModelType) {
this.id = id;
this.name = name;
this.profilePicture = profilePicture || null;
this.avatar = avatar || null;
this.socialMedia = socialMedia || null;
}
public id: string;
public name: string;
public profilePicture: string | null = null;
public avatar: string | null = null;
public socialMedia: {
facebook: string | null;
instagram: string | null;
twitter: string | null;
blueSky: string | null;
tiktok: string | null;
linkedin: string | null;
pinterest: string | null;
discord: string | null;
reddit: string | null;
other: string | null;
} | null = {
facebook: null,
instagram: null,
twitter: null,
blueSky: null,
tiktok: null,
linkedin: null,
pinterest: null,
discord: null,
reddit: null,
other: null,
};
}

View File

@@ -0,0 +1,26 @@
import { eventModel } from './eventModel';
import { personModel } from './personModel';
import { rankingModel } from './rankingModel';
import { tagModel } from './tagModel';
export class photoModel {
constructor(
public id: string,
public title: string,
public description: string,
public lowResUrl: string,
public midResUrl: string,
public highResUrl: string,
public createdAt: Date,
public updatedAt: Date,
public createdBy: string,
public updatedBy: string,
public event: eventModel | null = null,
public tags: tagModel[] = [],
public ranking: rankingModel = new rankingModel(0),
public isFavorite: boolean = false,
public isPublic: boolean = true,
public isArchived: boolean = false,
public persons: personModel[] = []
) {}
}

View File

@@ -0,0 +1,25 @@
export class rankingModel {
constructor(
private totalVotes: number,
private upVotes: number = 0,
private downVotes: number = 0
) {}
downVote(): void {
this.downVotes++;
this.totalVotes++;
}
upVote(): void {
this.upVotes++;
this.totalVotes++;
}
get score(): number {
if (this.totalVotes === 0) {
return 0;
} else {
return (this.upVotes - this.downVotes) / this.totalVotes;
}
}
}

View File

@@ -0,0 +1,63 @@
import { permissionModel } from './permissionModel';
export class roleModel {
constructor(
public id: string,
public name: string,
public description: string,
public permissions: permissionModel[] = [],
public baseRoleModel: roleModel | null = null
) {
if (baseRoleModel) {
this.permissions = baseRoleModel.permissions.concat(this.permissions);
}
}
get isAdmin(): boolean {
return this.id === roleModel.AdminRole.id;
}
get isContentManager(): boolean {
return this.id === roleModel.ContentManagerRole.id;
}
get isUser(): boolean {
return this.id === roleModel.UserRole.id;
}
hasPermission(permission: permissionModel): boolean {
return this.permissions.some((p) => p.id === permission.id);
}
static UserRole = new roleModel('1', 'User', 'Role for regular users', [
permissionModel.ViewContentPermission,
permissionModel.LikeContentPermission,
]);
static ContentManagerRole = new roleModel(
'2',
'Content Manager',
'Role for managing content',
[
permissionModel.CreateUserPermission,
permissionModel.DisableUserPermission,
permissionModel.CreateContentPermission,
permissionModel.EditContentPermission,
permissionModel.DeleteContentPermission,
],
this.UserRole
);
static AdminRole = new roleModel(
'3',
'Admin',
'Administrator role with full permissions',
[
permissionModel.CreateUserPermission,
permissionModel.EditUserPermission,
permissionModel.DeleteUserPermission,
permissionModel.EditWebConfigPermission,
],
this.ContentManagerRole
);
}

View File

@@ -0,0 +1,3 @@
export class tagModel {
constructor(public id: string, public name: string) {}
}

Some files were not shown because too many files have changed in this diff Show More