From 0360a5a2ffd808ccc36658f1b26151d415b8cbf7 Mon Sep 17 00:00:00 2001 From: hit25082000 Date: Fri, 1 Nov 2024 17:09:32 -0400 Subject: [PATCH] att review sistem --- src/app/app.component.ts | 10 +- src/app/app.routes.ts | 25 +- src/app/auth/guards/admin.guard.ts | 33 +++ src/app/auth/services/auth.service.ts | 52 ++-- src/app/core/models/game.model.ts | 12 + src/app/core/models/language.model.ts | 9 + src/app/core/models/review.model.ts | 9 + ...vice.spec.ts => firestore.service.spec.ts} | 2 +- src/app/core/services/firestore.service.ts | 65 +++++ src/app/core/services/game.service.spec.ts | 16 ++ src/app/core/services/game.service.ts | 45 ++++ .../core/services/language.service.spec.ts | 16 ++ src/app/core/services/language.service.ts | 43 ++++ src/app/core/services/mock-data.service.ts | 116 +++++++++ src/app/core/services/product.service.ts | 14 -- src/app/core/services/review.service.ts | 35 +++ .../game-management.component.html | 26 ++ .../game-management.component.scss | 86 +++++++ .../game-management.component.ts | 100 ++++++++ .../language-management.component.html | 31 +++ .../language-management.component.scss | 130 ++++++++++ .../language-management.component.ts | 106 ++++++++ src/app/features/login/login.component.ts | 10 +- .../product-list/product-list.component.ts | 2 +- src/app/pages/admin/admin.component.html | 5 + src/app/pages/admin/admin.component.scss | 0 src/app/pages/admin/admin.component.spec.ts | 23 ++ src/app/pages/admin/admin.component.ts | 14 ++ src/app/pages/games/games.component.html | 54 +---- src/app/pages/games/games.component.scss | 72 +++++- src/app/pages/games/games.component.ts | 23 +- .../pages/language/language.component.html | 74 +----- src/app/pages/language/language.component.ts | 26 +- src/app/pages/promo/promo.component.html | 58 +++-- src/app/pages/promo/promo.component.scss | 111 ++++++++- src/app/pages/promo/promo.component.ts | 57 ++++- .../review-list/review-list.component.ts | 111 +++++++++ .../review-modal/review-modal.component.ts | 227 ++++++++++++++++++ src/app/shared/header/header.component.html | 8 +- src/app/shared/header/header.component.ts | 16 +- src/app/shared/navbar/navbar.component.html | 6 + src/app/shared/navbar/navbar.component.scss | 22 ++ .../shared/navbar/navbar.component.spec.ts | 23 ++ src/app/shared/navbar/navbar.component.ts | 13 + .../shared/pipes/timestamp-to-date.pipe.ts | 14 ++ 45 files changed, 1713 insertions(+), 237 deletions(-) create mode 100644 src/app/auth/guards/admin.guard.ts create mode 100644 src/app/core/models/game.model.ts create mode 100644 src/app/core/models/language.model.ts create mode 100644 src/app/core/models/review.model.ts rename src/app/core/services/{product.service.spec.ts => firestore.service.spec.ts} (85%) create mode 100644 src/app/core/services/firestore.service.ts create mode 100644 src/app/core/services/game.service.spec.ts create mode 100644 src/app/core/services/game.service.ts create mode 100644 src/app/core/services/language.service.spec.ts create mode 100644 src/app/core/services/language.service.ts create mode 100644 src/app/core/services/mock-data.service.ts delete mode 100644 src/app/core/services/product.service.ts create mode 100644 src/app/core/services/review.service.ts create mode 100644 src/app/features/game-management/game-management.component.html create mode 100644 src/app/features/game-management/game-management.component.scss create mode 100644 src/app/features/game-management/game-management.component.ts create mode 100644 src/app/features/language-management/language-management.component.html create mode 100644 src/app/features/language-management/language-management.component.scss create mode 100644 src/app/features/language-management/language-management.component.ts create mode 100644 src/app/pages/admin/admin.component.html create mode 100644 src/app/pages/admin/admin.component.scss create mode 100644 src/app/pages/admin/admin.component.spec.ts create mode 100644 src/app/pages/admin/admin.component.ts create mode 100644 src/app/shared/components/review-list/review-list.component.ts create mode 100644 src/app/shared/components/review-modal/review-modal.component.ts create mode 100644 src/app/shared/navbar/navbar.component.html create mode 100644 src/app/shared/navbar/navbar.component.scss create mode 100644 src/app/shared/navbar/navbar.component.spec.ts create mode 100644 src/app/shared/navbar/navbar.component.ts create mode 100644 src/app/shared/pipes/timestamp-to-date.pipe.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3465589..a07a383 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { MockDataInsertion } from './core/services/mock-data.service'; +import { Component, inject, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { AuthService } from './auth/services/auth.service'; import { Router } from '@angular/router'; @@ -13,8 +14,11 @@ import { SpeechToggleComponent } from './features/speech-toggle/speech-toggle.co styleUrl: './app.component.scss' }) -export class AppComponent { - constructor(private auth: AuthService, private router: Router) { +export class AppComponent implements OnInit { + mock = inject(MockDataInsertion) + constructor(private auth: AuthService, private router: Router) {} + ngOnInit() { + //this.mock.insertMockData(); } } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 0bccc42..ce9ccaf 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -7,10 +7,32 @@ import { AboutComponent } from './pages/about/about.component'; import { PromoComponent } from './pages/promo/promo.component'; import { GamesComponent } from './pages/games/games.component'; import { AuthGuard } from './auth/guards/auth.guard'; +import { AdminComponent } from './pages/admin/admin.component'; +import { GameManagementComponent } from './features/game-management/game-management.component'; +import { LanguageManagementComponent } from './features/language-management/language-management.component'; +import { AdminGuard } from './auth/guards/admin.guard'; export const routes: Routes = [ {path: '', component: MainComponent}, + { path: 'admin', component: AdminComponent, + canActivate: [AuthGuard, AdminGuard], + children: [ + { + path: '', + redirectTo: 'games-management', + pathMatch: 'full' + }, + { + path: 'games-management', + component: GameManagementComponent, + }, + { + path: 'language-management', + component: LanguageManagementComponent, + }, + ] + }, {path: 'index', component: MainComponent, children: [ @@ -38,6 +60,5 @@ export const routes: Routes = [ component: LoginComponent}, {path: 'register', component: RegisterComponent}, - //{path: 'unauthorized', component: UnauthorizedComponent}, - //{path: '**', component: NotFoundComponent}, + {path: '**', component: LoginComponent}, ]; diff --git a/src/app/auth/guards/admin.guard.ts b/src/app/auth/guards/admin.guard.ts new file mode 100644 index 0000000..2a19c45 --- /dev/null +++ b/src/app/auth/guards/admin.guard.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { Observable, of } from 'rxjs'; +import { map, take, switchMap } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class AdminGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) {} + + canActivate(): Observable { + return this.authService.user$.pipe( + take(1), + switchMap(user => { + if (!user) { + this.router.navigate(['/login']); + return of(false); + } + return this.authService.isAdmin$; + }), + map(isAdmin => { + if (isAdmin) { + return true; + } else { + this.router.navigate(['/index/games']); + return false; + } + }) + ); + } +} diff --git a/src/app/auth/services/auth.service.ts b/src/app/auth/services/auth.service.ts index ab35965..7a1e7ae 100644 --- a/src/app/auth/services/auth.service.ts +++ b/src/app/auth/services/auth.service.ts @@ -1,57 +1,39 @@ import { inject, Injectable } from '@angular/core'; -import { Auth, createUserWithEmailAndPassword, deleteUser, getAuth, sendEmailVerification, sendPasswordResetEmail, signInWithCredential, signInWithCustomToken, signInWithEmailAndPassword, updateCurrentUser, updateEmail, updatePassword, updateProfile, User, user } from '@angular/fire/auth'; -import { Subscription } from 'rxjs'; +import { Auth, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, user } from '@angular/fire/auth'; +import { BehaviorSubject, Observable, map } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthService { private auth: Auth = inject(Auth); + private isAdminSubject = new BehaviorSubject(false); + isAdmin$ = this.isAdminSubject.asObservable(); user$ = user(this.auth); - userSubscription: Subscription; + isLoggedIn$: Observable; constructor() { - this.userSubscription = this.user$.subscribe((aUser: any | null) => { - console.log(aUser); - }) + this.isLoggedIn$ = this.user$.pipe(map(user => !!user)); + this.user$.subscribe(user => this.checkAdminStatus(user?.email)); } - getCurrentUser(){ - return this.auth.currentUser; - } - - async register(email : string,password : string){ - var userCredential = await createUserWithEmailAndPassword(this.auth, email, password) - - return sendEmailVerification(userCredential.user) + async login(email: string, password: string): Promise { + await signInWithEmailAndPassword(this.auth, email, password); } - login(email : string,password : string){ - return signInWithEmailAndPassword(this.auth,email, password); + async register(email: string, password: string): Promise { + await createUserWithEmailAndPassword(this.auth, email, password); } - logout(){ - this.auth.signOut() + async logout(): Promise { + await signOut(this.auth); } - passwordReset(email : string){ - sendPasswordResetEmail(this.auth, email) - .then(() => { - // Password reset email sent! - // .. - }) - .catch((error) => { - const errorCode = error.code; - const errorMessage = error.message; - // .. - }); + private checkAdminStatus(email: string | null | undefined): void { + this.isAdminSubject.next(email?.toLowerCase() === 'admin@admin.com'); } - isLoggedIn(): boolean { - return !!this.auth.currentUser; - } - - ngOnDestroy() { - this.auth.signOut() + getCurrentUser() { + return this.auth.currentUser; } } diff --git a/src/app/core/models/game.model.ts b/src/app/core/models/game.model.ts new file mode 100644 index 0000000..a3a024f --- /dev/null +++ b/src/app/core/models/game.model.ts @@ -0,0 +1,12 @@ +import { Language } from "./language.model"; + +export interface Game { + id: string; + img: string; + alt: string; + name: string; + genre: string; + description: string; + language: Language; +} + diff --git a/src/app/core/models/language.model.ts b/src/app/core/models/language.model.ts new file mode 100644 index 0000000..f4094df --- /dev/null +++ b/src/app/core/models/language.model.ts @@ -0,0 +1,9 @@ +import { Game } from "./game.model"; + +export interface Language { + id?: string; + name: string; + flag: string; + culturalCuriosities: string[]; + games: Game[]; +} diff --git a/src/app/core/models/review.model.ts b/src/app/core/models/review.model.ts new file mode 100644 index 0000000..bbbe393 --- /dev/null +++ b/src/app/core/models/review.model.ts @@ -0,0 +1,9 @@ +export interface Review { + id?: string; + userId: string; + gameId: string; + rating: number; + comment: string; + userName: string; + createdAt: Date; +} diff --git a/src/app/core/services/product.service.spec.ts b/src/app/core/services/firestore.service.spec.ts similarity index 85% rename from src/app/core/services/product.service.spec.ts rename to src/app/core/services/firestore.service.spec.ts index d5c493e..eef753c 100644 --- a/src/app/core/services/product.service.spec.ts +++ b/src/app/core/services/firestore.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; -import { ProductService } from './product.service'; +import { ProductService } from './mock-data.service'; describe('ProductService', () => { let service: ProductService; diff --git a/src/app/core/services/firestore.service.ts b/src/app/core/services/firestore.service.ts new file mode 100644 index 0000000..b8bf3d6 --- /dev/null +++ b/src/app/core/services/firestore.service.ts @@ -0,0 +1,65 @@ +import { inject, Injectable } from '@angular/core'; +import { collection, collectionData, Firestore, doc, docData, setDoc, deleteDoc } from '@angular/fire/firestore'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class FirestoreService { + firestore: Firestore = inject(Firestore); + + constructor() {} + + getCollection(path: string): Observable { + return collectionData(collection(this.firestore, path), { + idField: 'id', + }) as Observable; + } + + getDocument(path: string, id: string): Observable { + return docData(doc(this.firestore, path, id), { idField: 'id' }) as Observable; + } + + createDocument( + path: string, + id: string, + data: any + ): Observable { // Alterado para retornar um Observable + const docRef = doc(this.firestore, path, id); + return new Observable((observer) => { + setDoc(docRef, { ...data }).then(() => { + observer.next(); // Notifica que a operação foi concluída + observer.complete(); // Completa o Observable + }).catch((error) => observer.error(error)); // Notifica erro, se ocorrer + }); + } + + generateId(path: string, id?: string): Observable { + const taskCollection = collection(this.firestore, path); + const docRef = id ? doc(taskCollection, id) : doc(taskCollection); + return new Observable((observer) => { + observer.next(docRef.id); + observer.complete(); + }); + } + + updateDocument(path: string, id: string, data: Partial): Observable { + const docRef = doc(this.firestore, path, id); + return new Observable((observer) => { + setDoc(docRef, data, { merge: true }).then(() => { + observer.next(); + observer.complete(); + }).catch((error) => observer.error(error)); + }); + } + + deleteDocument(path: string, id: string): Observable { + const docRef = doc(this.firestore, path, id); + return new Observable((observer) => { + deleteDoc(docRef).then(() => { + observer.next(); + observer.complete(); + }).catch((error) => observer.error(error)); + }); + } +} diff --git a/src/app/core/services/game.service.spec.ts b/src/app/core/services/game.service.spec.ts new file mode 100644 index 0000000..fb5f120 --- /dev/null +++ b/src/app/core/services/game.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { GameService } from './game.service'; + +describe('GameService', () => { + let service: GameService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GameService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/game.service.ts b/src/app/core/services/game.service.ts new file mode 100644 index 0000000..ce592e8 --- /dev/null +++ b/src/app/core/services/game.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { FirestoreService } from './firestore.service'; +import { Observable } from 'rxjs'; +import { Game } from '../models/game.model'; + +@Injectable({ + providedIn: 'root' +}) +export class GameService { + private path = 'games'; // Nome da coleção no Firestore + + constructor(private firestoreService: FirestoreService) {} + + getGames(): Observable { + return this.firestoreService.getCollection(this.path); + } + + getGame(id: string): Observable { + return this.firestoreService.getDocument(this.path, id); + } + + createGame(game: Game): Observable { + const id = this.firestoreService.generateId(this.path); + return new Observable((observer) => { + id.subscribe((generatedId) => { + this.firestoreService.createDocument(this.path, generatedId, { ...game, id: generatedId }) + .subscribe({ + next: () => { + observer.next(); + observer.complete(); + }, + error: (error) => observer.error(error) + }); + }); + }); + } + + updateGame(id: string, game: Partial): Observable { + return this.firestoreService.updateDocument(this.path, id, game); + } + + deleteGame(id: string): Observable { + return this.firestoreService.deleteDocument(this.path, id); + } +} diff --git a/src/app/core/services/language.service.spec.ts b/src/app/core/services/language.service.spec.ts new file mode 100644 index 0000000..299455e --- /dev/null +++ b/src/app/core/services/language.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LanguageService } from './language.service'; + +describe('LanguageService', () => { + let service: LanguageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LanguageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/language.service.ts b/src/app/core/services/language.service.ts new file mode 100644 index 0000000..9d4e0f3 --- /dev/null +++ b/src/app/core/services/language.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { FirestoreService } from './firestore.service'; +import { Observable } from 'rxjs'; +import { Language } from '../models/language.model'; + +@Injectable({ + providedIn: 'root' +}) +export class LanguageService { + private path = 'languages'; // Nome da coleção no Firestore + + constructor(private firestoreService: FirestoreService) {} + + getLanguages(): Observable { + return this.firestoreService.getCollection(this.path); + } + + getLanguage(id: string): Observable { + return this.firestoreService.getDocument(this.path, id); + } + + createLanguage(language: Language): Observable { + return new Observable((observer) => { + this.firestoreService.generateId(this.path).subscribe((id) => { + this.firestoreService.createDocument(this.path, id, { ...language, id }).subscribe({ + next: () => { + observer.next(); // Notifica que a operação foi concluída + observer.complete(); // Completa o Observable + }, + error: (error) => observer.error(error) // Notifica erro, se ocorrer + }); + }); + }); + } + + updateLanguage(id: string, language: Partial): Observable { + return this.firestoreService.updateDocument(this.path, id, language); + } + + deleteLanguage(id: string): Observable { + return this.firestoreService.deleteDocument(this.path, id); + } +} diff --git a/src/app/core/services/mock-data.service.ts b/src/app/core/services/mock-data.service.ts new file mode 100644 index 0000000..6cbc8fb --- /dev/null +++ b/src/app/core/services/mock-data.service.ts @@ -0,0 +1,116 @@ +import { inject, Injectable } from '@angular/core'; +import { Language } from '../models/language.model'; +import { FirestoreService } from './firestore.service'; +import { Game } from '../models/game.model'; +import { forkJoin, switchMap } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class MockDataInsertion { + firestoreService = inject(FirestoreService) + constructor() {} + + insertMockData() { + const languages: Language[] = [ + { + name: 'Inglês', + flag: '🇺🇸', + culturalCuriosities: [ + 'O inglês é a língua mais falada no mundo dos negócios', + 'Hollywood é o centro da indústria cinematográfica global' + ], + games: [] + }, + { + name: 'Japonês', + flag: '🇯🇵', + culturalCuriosities: [ + 'O Japão é conhecido pela sua cultura de respeito e honra', + 'A culinária japonesa é considerada Patrimônio Cultural Imaterial pela UNESCO' + ], + games: [] + }, + { + name: 'Polonês', + flag: '🇵🇱', + culturalCuriosities: [ + 'A Polônia tem uma rica história de resistência e resiliência', + 'O pierogi é um dos pratos mais famosos da culinária polonesa' + ], + games: [] + } + ]; + + const games: Game[] = [ + { + id: '', + img: 'https://exemplo.com/gta5.jpg', + alt: 'Imagem de Grand Theft Auto V', + name: 'Grand Theft Auto V', + genre: 'Ação-Aventura', + description: 'Um jogo de mundo aberto ambientado na fictícia Los Santos', + language: {} as Language + }, + { + id: '', + img: 'https://exemplo.com/zelda.jpg', + alt: 'Imagem de The Legend of Zelda: Breath of the Wild', + name: 'The Legend of Zelda: Breath of the Wild', + genre: 'Ação-Aventura', + description: 'Uma aventura épica no vasto reino de Hyrule', + language: {} as Language + }, + { + id: '', + img: 'https://exemplo.com/witcher3.jpg', + alt: 'Imagem de The Witcher 3: Wild Hunt', + name: 'The Witcher 3: Wild Hunt', + genre: 'RPG de Ação', + description: 'Uma jornada épica em um mundo de fantasia sombria', + language: {} as Language + } + ]; + + // Inserir idiomas + const languageInsertions = languages.map(language => + this.firestoreService.generateId('languages').pipe( + switchMap(id => { + language.id = id; + return this.firestoreService.createDocument('languages', id, language); + }) + ) + ); + + forkJoin(languageInsertions).subscribe( + () => { + console.log('Idiomas inseridos com sucesso'); + + // Após inserir os idiomas, inserir os jogos + this.firestoreService.getCollection('languages').subscribe( + insertedLanguages => { + games[0].language = insertedLanguages.find(lang => lang.name === 'Inglês')!; + games[1].language = insertedLanguages.find(lang => lang.name === 'Japonês')!; + games[2].language = insertedLanguages.find(lang => lang.name === 'Polonês')!; + + const gameInsertions = games.map(game => + this.firestoreService.generateId('games').pipe( + switchMap(id => { + game.id = id; + return this.firestoreService.createDocument('games', id, game); + }) + ) + ); + + forkJoin(gameInsertions).subscribe( + () => console.log('Jogos inseridos com sucesso'), + error => console.error('Erro ao inserir jogos:', error) + ); + }, + error => console.error('Erro ao obter idiomas:', error) + ); + }, + error => console.error('Erro ao inserir idiomas:', error) + ); + } +} diff --git a/src/app/core/services/product.service.ts b/src/app/core/services/product.service.ts deleted file mode 100644 index 424e0b6..0000000 --- a/src/app/core/services/product.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from '@angular/core'; -import { AngularFirestore } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class ProductService { - constructor(private firestore: AngularFirestore) {} - - getProducts(): Observable { - return this.firestore.collection('products').valueChanges(); - } -} diff --git a/src/app/core/services/review.service.ts b/src/app/core/services/review.service.ts new file mode 100644 index 0000000..8424962 --- /dev/null +++ b/src/app/core/services/review.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { Firestore, collection, addDoc, query, where, collectionData } from '@angular/fire/firestore'; +import { Review } from '../models/review.model'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class ReviewService { + constructor(private firestore: Firestore) {} + + addReview(review: Omit): Promise { + const reviewsRef = collection(this.firestore, 'reviews'); + return addDoc(reviewsRef, { + ...review, + createdAt: new Date() + }).then(); + } + + getGameReviews(gameId: string): Observable { + const reviewsRef = collection(this.firestore, 'reviews'); + const reviewQuery = query(reviewsRef, where('gameId', '==', gameId)); + + return collectionData(reviewQuery, { idField: 'id' }).pipe( + map(reviews => reviews as Review[]) + ); + } + + calculateAverageRating(reviews: Review[]): number { + if (!reviews.length) return 0; + const sum = reviews.reduce((acc, review) => acc + review.rating, 0); + return sum / reviews.length; + } +} diff --git a/src/app/features/game-management/game-management.component.html b/src/app/features/game-management/game-management.component.html new file mode 100644 index 0000000..1b2f8b1 --- /dev/null +++ b/src/app/features/game-management/game-management.component.html @@ -0,0 +1,26 @@ +
+

Gerenciar Jogos

+
+ + + + + + + + +
+ +
    +
  • + {{ game.name }} - {{ game.genre }} + + +
  • +
+
diff --git a/src/app/features/game-management/game-management.component.scss b/src/app/features/game-management/game-management.component.scss new file mode 100644 index 0000000..03fb7d0 --- /dev/null +++ b/src/app/features/game-management/game-management.component.scss @@ -0,0 +1,86 @@ +.game-management { + padding: 2rem; +} + +h1 { + text-align: center; + margin-bottom: 2rem; + color: #1db954; +} + +form { + background-color: #1e1e1e; + border-radius: 10px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +input, textarea, select { + width: 100%; + padding: 0.5rem; + margin-bottom: 1rem; + border: 1px solid #333; + background-color: #2a2a2a; + color: #ffffff; + border-radius: 5px; + + &:focus { + outline: none; + border-color: #1db954; + } +} + +button { + background-color: #1db954; + color: #ffffff; + border: none; + padding: 0.5rem 1rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: #1ed760; + } + + &:disabled { + background-color: #333; + cursor: not-allowed; + } +} + +ul { + list-style-type: none; + padding: 0; +} + +li { + background-color: #1e1e1e; + border-radius: 10px; + padding: 1rem; + margin-bottom: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + + &:hover { + transform: translateY(-5px); + } +} + +.game-actions { + display: flex; + gap: 0.5rem; +} + +.edit-btn { + background-color: #1db954; +} + +.delete-btn { + background-color: #e74c3c; +} + diff --git a/src/app/features/game-management/game-management.component.ts b/src/app/features/game-management/game-management.component.ts new file mode 100644 index 0000000..ae31971 --- /dev/null +++ b/src/app/features/game-management/game-management.component.ts @@ -0,0 +1,100 @@ +import { Component, OnInit } from '@angular/core'; +import { GameService } from '../../core/services/game.service'; +import { LanguageService } from '../../core/services/language.service'; +import { Observable } from 'rxjs'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { Game } from '../../core/models/game.model'; +import { Language } from '../../core/models/language.model'; + +@Component({ + selector: 'app-game-management', + standalone: true, + imports: [ReactiveFormsModule, CommonModule], + templateUrl: './game-management.component.html', + styleUrls: ['./game-management.component.scss'] +}) +export class GameManagementComponent implements OnInit { + games$!: Observable; + languages$!: Observable; + gameForm: FormGroup; + editMode = false; + currentGameId: string | null = null; + + constructor( + private gameService: GameService, + private languageService: LanguageService, + private fb: FormBuilder + ) { + this.gameForm = this.fb.group({ + name: ['', Validators.required], + genre: ['', Validators.required], + img: ['', Validators.required], + alt: ['', Validators.required], + description: ['', Validators.required], + languageId: [''] + }); + } + + ngOnInit(): void { + this.loadGames(); + this.loadLanguages(); + } + + loadGames(): void { + this.games$ = this.gameService.getGames(); + } + + loadLanguages(): void { + this.languages$ = this.languageService.getLanguages(); + } + + addGame(): void { + if (this.gameForm.valid) { + const newGame: Game = this.gameForm.value; + this.gameService.createGame(newGame).subscribe(() => { + this.loadGames(); + this.gameForm.reset(); + }); + } + } + + updateGame(): void { + if (this.gameForm.valid && this.currentGameId) { + const updatedGame: Partial = this.gameForm.value; + this.gameService.updateGame(this.currentGameId, updatedGame).subscribe(() => { + this.loadGames(); + this.resetForm(); + }); + } + } + + deleteGame(id: string): void { + this.gameService.deleteGame(id).subscribe(() => { + this.loadGames(); + }); + } + + editGame(game: Game): void { + this.editMode = true; + this.currentGameId = game.id; + this.gameForm.patchValue({ + ...game, + languageId: game.language.id + }); + } + + resetForm(): void { + this.editMode = false; + this.currentGameId = null; + this.gameForm.reset(); + } + + onSubmit(): void { + if (this.editMode) { + this.updateGame(); + } else { + this.addGame(); + } + } +} diff --git a/src/app/features/language-management/language-management.component.html b/src/app/features/language-management/language-management.component.html new file mode 100644 index 0000000..45b6c54 --- /dev/null +++ b/src/app/features/language-management/language-management.component.html @@ -0,0 +1,31 @@ +
+

Gerenciar Idiomas

+
+
+ + +
+
+ + +
+
+

Curiosidades Culturais

+
+ + +
+ +
+ + +
+ +
    +
  • + {{ language.name }} + + +
  • +
+
diff --git a/src/app/features/language-management/language-management.component.scss b/src/app/features/language-management/language-management.component.scss new file mode 100644 index 0000000..edd551f --- /dev/null +++ b/src/app/features/language-management/language-management.component.scss @@ -0,0 +1,130 @@ +.language-management { + padding: 2rem; + max-width: 800px; + margin: 0 auto; +} + +h1 { + text-align: center; + margin-bottom: 2rem; + color: #1db954; +} + +form { + background-color: #1e1e1e; + border-radius: 10px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1rem; +} + +label { + display: block; + margin-bottom: 0.5rem; + color: #ffffff; +} + +input, textarea, select { + width: 100%; + padding: 0.5rem; + border: 1px solid #333; + background-color: #2a2a2a; + color: #ffffff; + border-radius: 5px; + margin-bottom: 1rem; + + + &:focus { + outline: none; + border-color: #1db954; + } +} + +.curiosities-container { + margin-top: 1rem; +} + +.curiosity-item { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; + + input { + flex-grow: 1; + } +} + +button { + background-color: #1db954; + color: #ffffff; + border: none; + padding: 0.5rem 1rem; + margin-bottom: 1rem; + margin-right: 1rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: #1ed760; + } + + &:disabled { + background-color: #333; + cursor: not-allowed; + } +} + +.button-group { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +ul { + list-style-type: none; + padding: 0; +} + +li { + background-color: #1e1e1e; + border-radius: 10px; + padding: 1rem; + margin-bottom: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + + &:hover { + transform: translateY(-2px); + } +} + +.language-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.language-actions { + display: flex; + gap: 0.5rem; +} + +.edit-btn { + background-color: #1db954; +} + +.delete-btn { + background-color: #e74c3c; +} + +.cancel-btn { + background-color: #7f8c8d; +} diff --git a/src/app/features/language-management/language-management.component.ts b/src/app/features/language-management/language-management.component.ts new file mode 100644 index 0000000..af0cde7 --- /dev/null +++ b/src/app/features/language-management/language-management.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit } from '@angular/core'; +import { LanguageService } from '../../core/services/language.service'; +import { Observable } from 'rxjs'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { Language } from '../../core/models/language.model'; + +@Component({ + selector: 'app-language-management', + standalone: true, + imports: [ReactiveFormsModule, CommonModule], + templateUrl: './language-management.component.html', + styleUrls: ['./language-management.component.scss'] +}) +export class LanguageManagementComponent implements OnInit { + languages$!: Observable; + languageForm: FormGroup; + editMode = false; + currentLanguageId: string | null = null; + + constructor(private languageService: LanguageService, private fb: FormBuilder) { + this.languageForm = this.fb.group({ + name: ['', Validators.required], + flag: ['', Validators.required], + culturalCuriosities: this.fb.array([]), + games: this.fb.array([]) + }); + } + + ngOnInit(): void { + this.loadLanguages(); + } + + get culturalCuriosities() { + return this.languageForm.get('culturalCuriosities') as FormArray; + } + + addCuriosity() { + this.culturalCuriosities.push(this.fb.control('')); + } + + removeCuriosity(index: number) { + this.culturalCuriosities.removeAt(index); + } + + loadLanguages(): void { + this.languages$ = this.languageService.getLanguages(); + } + + addLanguage(): void { + if (this.languageForm.valid) { + const newLanguage: Language = { + ...this.languageForm.value, + games: [] + }; + this.languageService.createLanguage(newLanguage).subscribe(() => { + this.loadLanguages(); + this.resetForm(); + }); + } + } + + updateLanguage(): void { + if (this.languageForm.valid && this.currentLanguageId) { + const updatedLanguage: Partial = this.languageForm.value; + this.languageService.updateLanguage(this.currentLanguageId, updatedLanguage).subscribe(() => { + this.loadLanguages(); + this.resetForm(); + }); + } + } + + deleteLanguage(id: string): void { + this.languageService.deleteLanguage(id).subscribe(() => { + this.loadLanguages(); + }); + } + + editLanguage(language: Language): void { + this.editMode = true; + this.currentLanguageId = language.id!; + this.languageForm.patchValue({ + name: language.name, + flag: language.flag + }); + this.culturalCuriosities.clear(); + language.culturalCuriosities.forEach(curiosity => { + this.culturalCuriosities.push(this.fb.control(curiosity)); + }); + } + + resetForm(): void { + this.editMode = false; + this.currentLanguageId = null; + this.languageForm.reset(); + this.culturalCuriosities.clear(); + } + + onSubmit(): void { + if (this.editMode) { + this.updateLanguage(); + } else { + this.addLanguage(); + } + } +} diff --git a/src/app/features/login/login.component.ts b/src/app/features/login/login.component.ts index 99adf81..0042b23 100644 --- a/src/app/features/login/login.component.ts +++ b/src/app/features/login/login.component.ts @@ -3,6 +3,7 @@ import { AuthService } from '../../auth/services/auth.service'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; +import { take } from 'rxjs/operators'; @Component({ selector: 'app-login', @@ -36,8 +37,13 @@ export class LoginComponent implements OnInit { this.authService.login(this.f['email'].value, this.f['password'].value) .then(() => { - console.log('Login bem-sucedido'); - this.router.navigate(['/index/games']); + this.authService.isAdmin$.pipe(take(1)).subscribe(isAdmin => { + if (isAdmin) { + this.router.navigate(['/admin']); + } else { + this.router.navigate(['/index/games']); + } + }); }) .catch(error => console.error('Erro no login', error)); } diff --git a/src/app/features/product-list/product-list.component.ts b/src/app/features/product-list/product-list.component.ts index 43a5328..768bd99 100644 --- a/src/app/features/product-list/product-list.component.ts +++ b/src/app/features/product-list/product-list.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ProductService } from '../../core/services/product.service'; +import { ProductService } from '../../core/services/mock-data.service'; @Component({ selector: 'app-product-list', diff --git a/src/app/pages/admin/admin.component.html b/src/app/pages/admin/admin.component.html new file mode 100644 index 0000000..b0d294f --- /dev/null +++ b/src/app/pages/admin/admin.component.html @@ -0,0 +1,5 @@ +
+

Painel de Administração

+ + +
diff --git a/src/app/pages/admin/admin.component.scss b/src/app/pages/admin/admin.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/admin/admin.component.spec.ts b/src/app/pages/admin/admin.component.spec.ts new file mode 100644 index 0000000..93d56d8 --- /dev/null +++ b/src/app/pages/admin/admin.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminComponent } from './admin.component'; + +describe('AdminComponent', () => { + let component: AdminComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/admin/admin.component.ts b/src/app/pages/admin/admin.component.ts new file mode 100644 index 0000000..cfe1a26 --- /dev/null +++ b/src/app/pages/admin/admin.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { NavbarComponent } from '../../shared/navbar/navbar.component'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-admin', + standalone: true, + imports: [NavbarComponent,RouterOutlet], + templateUrl: './admin.component.html', + styleUrl: './admin.component.scss' +}) +export class AdminComponent { + +} diff --git a/src/app/pages/games/games.component.html b/src/app/pages/games/games.component.html index afc9b5f..d371024 100644 --- a/src/app/pages/games/games.component.html +++ b/src/app/pages/games/games.component.html @@ -2,47 +2,19 @@

Catálogo de Jogos Multiplayer para Aprendizado de Idiomas

-
- Fortnite -

Fortnite

-

Gênero: Battle Royale

-

Idioma de Aprendizado: Inglês

-

Como aprender: Use o chat de voz para se comunicar com jogadores internacionais. Aprenda gírias e termos específicos do jogo em inglês. Pratique a compreensão auditiva ouvindo as chamadas de equipe.

-
- -
- Minecraft -

Minecraft

-

Gênero: Sandbox

-

Idioma de Aprendizado: Alemão

-

Como aprender: Entre em servidores alemães e interaja com jogadores nativos. Aprenda vocabulário relacionado a construção e recursos naturais. Use o chat de texto para praticar a escrita em alemão.

-
- -
- Among Us -

Among Us

-

Gênero: Party

-

Idioma de Aprendizado: Espanhol

-

Como aprender: Jogue em servidores latino-americanos. Pratique a argumentação e descrição em espanhol durante as reuniões de emergência. Aprenda vocabulário relacionado a tarefas e acusações.

-
- -
- League of Legends -

League of Legends

-

Gênero: MOBA

-

Idioma de Aprendizado: Coreano

-

Como aprender: Jogue no servidor coreano. Aprenda termos estratégicos e nomes de campeões em coreano. Use o chat de equipe para praticar frases curtas e comandos em coreano.

-
- -
- Animal Crossing: New Horizons -

Animal Crossing: New Horizons

-

Gênero: Simulação Social

-

Idioma de Aprendizado: Japonês

-

Como aprender: Configure o jogo em japonês. Interaja com outros jogadores japoneses online. Aprenda vocabulário relacionado a natureza, decoração e vida cotidiana em japonês.

+
+
+ +
+
+

{{ game.name }}

+

{{ game.genre }}

+

+ {{ game.language.flag }} + {{ game.language.name }} +

+

{{ game.description }}

+
- - - diff --git a/src/app/pages/games/games.component.scss b/src/app/pages/games/games.component.scss index fcfe313..7d0966b 100644 --- a/src/app/pages/games/games.component.scss +++ b/src/app/pages/games/games.component.scss @@ -1,10 +1,15 @@ .games-catalog { padding: 2rem; + background-color: #121212; } h1 { text-align: center; margin-bottom: 2rem; + color: #1db954; + font-size: 2rem; + text-transform: uppercase; + letter-spacing: 2px; } .game-list { @@ -15,35 +20,78 @@ h1 { .game-card { background-color: #1e1e1e; - border-radius: 10px; + border-radius: 15px; overflow: hidden; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.3s ease; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease; + display: flex; + flex-direction: column; + height: 450px; &:hover { transform: translateY(-5px); + box-shadow: 0 15px 30px rgba(29, 185, 84, 0.3); } - img { - width: 100%; + .game-image { height: 200px; - object-fit: cover; + overflow: hidden; + + img { + width: 100%; + object-fit: cover; + transition: transform 0.3s ease; + } + } + + &:hover .game-image img { + transform: scale(1.1); + } + + .game-info { + padding: 1.5rem; + flex-grow: 1; + display: flex; + flex-direction: column; } h2 { - padding: 1rem; margin: 0; font-size: 1.5rem; + color: #1db954; + margin-bottom: 0.5rem; } - p { - padding: 0 1rem; - margin: 0.5rem 0; + .game-genre { font-size: 0.9rem; color: #b3b3b3; + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 1px; + } - &:last-child { - padding-bottom: 1rem; + .game-language { + font-size: 0.9rem; + color: #ffffff; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + + .language-flag { + margin-right: 0.5rem; + font-size: 1.2rem; } } + + .game-description { + font-size: 0.9rem; + color: #b3b3b3; + line-height: 1.4; + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } } diff --git a/src/app/pages/games/games.component.ts b/src/app/pages/games/games.component.ts index 15c8252..b842f8a 100644 --- a/src/app/pages/games/games.component.ts +++ b/src/app/pages/games/games.component.ts @@ -1,17 +1,32 @@ -import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Component, inject, OnInit } from '@angular/core'; import { AccessibilityService } from '../../core/services/accessibility.service'; +import { GameService } from '../../core/services/game.service'; +import { CommonModule } from '@angular/common'; +import { Game } from '../../core/models/game.model'; @Component({ selector: 'app-Games', standalone: true, - imports: [], + imports: [CommonModule], templateUrl: './games.component.html', styleUrl: './games.component.scss' }) -export class GamesComponent { +export class GamesComponent implements OnInit { + gameService = inject(GameService) + games$!: Observable; + constructor(private accessibilityService: AccessibilityService) {} - onButtonClick(text : string) { + ngOnInit(): void { + this.loadGames(); + } + + onButtonClick(text: string) { this.accessibilityService.speak(text); } + + loadGames() { + this.games$ = this.gameService.getGames() + } } diff --git a/src/app/pages/language/language.component.html b/src/app/pages/language/language.component.html index d983da9..12caba9 100644 --- a/src/app/pages/language/language.component.html +++ b/src/app/pages/language/language.component.html @@ -2,78 +2,12 @@

Catálogo de Línguas: Curiosidades Culturais e Referências em Jogos

-
-

Inglês

- Bandeira do Reino Unido +
+

{{ language.name }}

+

Curiosidades Culturais:

    -
  • O chá das cinco é uma tradição britânica real.
  • -
  • "Queuing" (fazer fila) é considerado uma arte no Reino Unido.
  • -
-

Referências em Jogos:

-
    -
  • Assassin's Creed Syndicate: Ambientado em Londres vitoriana.
  • -
  • Forza Horizon 4: Explora o cenário do Reino Unido.
  • -
-
- -
-

Japonês

- Bandeira do Japão -

Curiosidades Culturais:

-
    -
  • É comum tirar os sapatos antes de entrar em casas e alguns restaurantes.
  • -
  • O conceito de "Ikigai" representa o propósito de vida.
  • -
-

Referências em Jogos:

-
    -
  • Persona 5: Retrata a vida escolar e cultura pop japonesa.
  • -
  • Ghost of Tsushima: Ambientado no Japão feudal.
  • -
-
- -
-

Espanhol

- Bandeira da Espanha -

Curiosidades Culturais:

-
    -
  • A sesta é uma tradição de descanso após o almoço.
  • -
  • As touradas são parte controversa da cultura espanhola.
  • -
-

Referências em Jogos:

-
    -
  • Grim Fandango: Inspirado no Dia dos Mortos mexicano.
  • -
  • Uncharted 3: Parte do jogo se passa na Espanha.
  • -
-
- -
-

Alemão

- Bandeira da Alemanha -

Curiosidades Culturais:

-
    -
  • O Oktoberfest é a maior festa popular do mundo.
  • -
  • A pontualidade é altamente valorizada na cultura alemã.
  • -
-

Referências em Jogos:

-
    -
  • Kingdom Come: Deliverance: Ambientado na Boêmia medieval.
  • -
  • Euro Truck Simulator 2: Inclui estradas e cidades alemãs.
  • -
-
- -
-

Mandarim

- Bandeira da China -

Curiosidades Culturais:

-
    -
  • O número 4 é considerado de má sorte devido à sua pronúncia semelhante à palavra "morte".
  • -
  • O Ano Novo Chinês é a festa mais importante do calendário chinês.
  • -
-

Referências em Jogos:

-
    -
  • Shenmue: Ambientado em Hong Kong, com elementos da cultura chinesa.
  • -
  • Sleeping Dogs: Retrata a vida em Hong Kong moderna.
  • +
  • {{ curiosity }}
diff --git a/src/app/pages/language/language.component.ts b/src/app/pages/language/language.component.ts index 35e3e05..a4601b7 100644 --- a/src/app/pages/language/language.component.ts +++ b/src/app/pages/language/language.component.ts @@ -1,12 +1,32 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LanguageService } from '../../core/services/language.service'; +import { Observable } from 'rxjs'; +import { AccessibilityService } from '../../core/services/accessibility.service'; +import { Language } from '../../core/models/language.model' @Component({ selector: 'app-language', standalone: true, - imports: [], + imports: [CommonModule], templateUrl: './language.component.html', styleUrl: './language.component.scss' }) -export class LanguageComponent { +export class LanguageComponent implements OnInit { + languageService = inject(LanguageService); + languages$!: Observable; + constructor(private accessibilityService: AccessibilityService) {} + + ngOnInit() { + this.loadLanguages(); + } + + onButtonClick(text: string) { + this.accessibilityService.speak(text); + } + + loadLanguages() { + this.languages$ = this.languageService.getLanguages(); + } } diff --git a/src/app/pages/promo/promo.component.html b/src/app/pages/promo/promo.component.html index 3a06cea..cd21c7c 100644 --- a/src/app/pages/promo/promo.component.html +++ b/src/app/pages/promo/promo.component.html @@ -1,48 +1,44 @@
-
+
-

Oferta Especial: Pacote de Aprendizado Completo

-

Aprenda inglês jogando Fortnite e Minecraft!

+

Oferta Especial: {{ featuredPromo.name }}

+

Aprenda {{ featuredPromo.language.name }} jogando {{ featuredPromo.name }}!

    -
  • 3 meses de acesso premium aos servidores de aprendizado
  • -
  • Tutoriais exclusivos de vocabulário de jogos
  • +
  • Acesso premium aos servidores de aprendizado
  • +
  • Tutoriais exclusivos de vocabulário de {{ featuredPromo.genre }}
  • Sessões semanais com instrutores nativos
- R$ 299,99 - R$ 199,99 + R$ {{ 100 | number:'1.2-2' }} + R$ {{ featuredPromo.discountedPrice | number:'1.2-2' }}
- Fortnite e Minecraft +

Mais Ofertas de Jogos e Idiomas

diff --git a/src/app/pages/promo/promo.component.scss b/src/app/pages/promo/promo.component.scss index 5c7d891..99dfd85 100644 --- a/src/app/pages/promo/promo.component.scss +++ b/src/app/pages/promo/promo.component.scss @@ -1,14 +1,16 @@ .promo-page { padding: 2rem; + background-color: #121212; + color: #ffffff; } .main-promo { display: flex; background-color: #1e1e1e; - border-radius: 10px; + border-radius: 15px; overflow: hidden; margin-bottom: 3rem; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); } .promo-content { @@ -18,6 +20,7 @@ h1 { color: #1db954; margin-bottom: 1rem; + font-size: 2.5rem; } p { @@ -27,6 +30,21 @@ ul { margin-bottom: 1rem; + list-style-type: none; + padding-left: 0; + + li { + margin-bottom: 0.5rem; + padding-left: 1.5rem; + position: relative; + + &:before { + content: '✓'; + color: #1db954; + position: absolute; + left: 0; + } + } } .promo-price { @@ -39,7 +57,7 @@ } .discounted-price { - font-size: 1.5rem; + font-size: 1.8rem; font-weight: bold; color: #1db954; } @@ -49,14 +67,15 @@ background-color: #1db954; color: #121212; border: none; - padding: 0.75rem 1.5rem; - font-size: 1rem; - border-radius: 25px; + padding: 1rem 2rem; + font-size: 1.2rem; + border-radius: 30px; cursor: pointer; - transition: background-color 0.3s ease; + transition: all 0.3s ease; &:hover { background-color: #1ed760; + transform: translateY(-2px); } } } @@ -124,3 +143,81 @@ color: #1db954; } } + +.language-flag { + width: 30px; + height: 20px; + margin: 1rem; + overflow: hidden; + border-radius: 4px; + + .flag-image { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.review-button { + padding: 8px 16px; + background: #1db954; + color: #ffffff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.review-button:hover { + background: #1ed760; +} + +.promo-details { + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + + .promo-price { + font-size: 1.2rem; + padding: 0; + } + + app-review-list { + width: 100%; + + ::ng-deep .reviews-container { + padding: 10px; + + .reviews-summary { + margin-bottom: 10px; + text-align: center; + } + + .reviews-list { + max-height: 200px; + overflow-y: auto; + padding-right: 5px; + + &::-webkit-scrollbar { + width: 5px; + } + + &::-webkit-scrollbar-track { + background: #2a2a2a; + } + + &::-webkit-scrollbar-thumb { + background: #1db954; + border-radius: 3px; + } + } + } + } + + .review-button { + width: 100%; + margin-top: 10px; + } +} diff --git a/src/app/pages/promo/promo.component.ts b/src/app/pages/promo/promo.component.ts index 5339ff3..1dad058 100644 --- a/src/app/pages/promo/promo.component.ts +++ b/src/app/pages/promo/promo.component.ts @@ -1,12 +1,63 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { GameService } from '../../core/services/game.service'; +import { Observable, map } from 'rxjs'; +import { Game } from '../../core/models/game.model'; +import { ReviewListComponent } from "../../shared/components/review-list/review-list.component"; +import { ReviewModalComponent } from "../../shared/components/review-modal/review-modal.component"; + +interface PromoGame extends Game { + discountedPrice: number; +} @Component({ selector: 'app-promo', standalone: true, - imports: [], + imports: [CommonModule, ReviewListComponent, ReviewModalComponent], templateUrl: './promo.component.html', styleUrl: './promo.component.scss' }) -export class PromoComponent { +export class PromoComponent implements OnInit { + featuredPromo$!: Observable; + otherPromos$!: Observable; + private currentOpenModalId = signal(null); + + constructor( + private gameService: GameService + ) {} + + ngOnInit() { + const games$ = this.gameService.getGames(); + + this.featuredPromo$ = games$.pipe( + map(games => this.createPromoGame(games[0])) + ); + + this.otherPromos$ = games$.pipe( + map(games => games.slice(1, 5).map(game => this.createPromoGame(game))) + ); + } + + private createPromoGame(game: Game): PromoGame { + return { + ...game, + discountedPrice: this.calculateDiscountedPrice(game) + }; + } + + private calculateDiscountedPrice(game: Game): number { + // Simples cálculo de desconto (20% off) + const originalPrice = 100; // Preço base fictício + return originalPrice * 0.8; + } + + isModalOpen = (gameId: string) => this.currentOpenModalId() === gameId; + + openReviewModal(gameId: string): void { + this.currentOpenModalId.set(gameId); + } + closeReviewModal(): void { + this.currentOpenModalId.set(null); + } } diff --git a/src/app/shared/components/review-list/review-list.component.ts b/src/app/shared/components/review-list/review-list.component.ts new file mode 100644 index 0000000..105165c --- /dev/null +++ b/src/app/shared/components/review-list/review-list.component.ts @@ -0,0 +1,111 @@ +import { Component, input, Input, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReviewService } from '../../../core/services/review.service'; +import { Review } from '../../../core/models/review.model'; +import { Observable } from 'rxjs'; +import { TimestampToDatePipe } from '../../pipes/timestamp-to-date.pipe'; + +@Component({ + selector: 'app-review-list', + standalone: true, + imports: [CommonModule, TimestampToDatePipe], + template: ` +
+
+

Avaliações dos Usuários

+
+ {{ calculateAverageRating(reviews) | number:'1.1-1' }} + de 5 ({{ reviews.length }} avaliações) +
+
+ +
+
+
+ {{ review.userName }} + + + + {{ review.createdAt | timestampToDate | date:'dd/MM/yyyy' }} +
+

{{ review.comment }}

+
+
+
+ `, + styles: [` + .reviews-container { + padding: 20px; + } + .reviews-summary { + margin-bottom: 20px; + } + .average-rating { + display: flex; + align-items: center; + gap: 10px; + } + .rating { + font-size: 24px; + font-weight: bold; + } + .total { + font-size: 16px; + color: #666; + } + .reviews-list { + display: flex; + flex-direction: column; + gap: 20px; + } + .review-item { + padding: 15px; + background: #1e1e1e; + border-radius: 8px; + } + .review-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; + } + .user-name { + font-weight: bold; + color: #ffffff; + } + .stars { + display: flex; + gap: 5px; + } + .stars span { + opacity: 0.5; + } + .stars span.active { + opacity: 1; + } + .review-date { + color: #666; + font-size: 14px; + } + .comment { + color: #ffffff; + margin: 0; + line-height: 1.4; + } + `] +}) +export class ReviewListComponent implements OnInit { + gameId = input.required(); + reviews$!: Observable; + + constructor(private reviewService: ReviewService) {} + + ngOnInit() { + this.reviews$ = this.reviewService.getGameReviews(this.gameId()); + } + + calculateAverageRating(reviews: Review[]): number { + return this.reviewService.calculateAverageRating(reviews); + } +} diff --git a/src/app/shared/components/review-modal/review-modal.component.ts b/src/app/shared/components/review-modal/review-modal.component.ts new file mode 100644 index 0000000..2498a7a --- /dev/null +++ b/src/app/shared/components/review-modal/review-modal.component.ts @@ -0,0 +1,227 @@ +import { Component, EventEmitter, input, Output, signal, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ReviewService } from '../../../core/services/review.service'; +import { AuthService } from '../../../auth/services/auth.service'; + +@Component({ + selector: 'app-review-modal', + standalone: true, + imports: [CommonModule, FormsModule, ReactiveFormsModule], + template: ` + + `, + styles: [` + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .modal-content { + background: #1e1e1e; + padding: 20px; + border-radius: 8px; + width: 90%; + max-width: 500px; + color: #ffffff; + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .close-button { + background: none; + border: none; + color: #ffffff; + font-size: 24px; + cursor: pointer; + } + + .rating-container { + margin-bottom: 20px; + } + + .stars { + display: flex; + gap: 5px; + cursor: pointer; + margin-top: 10px; + } + + .stars span { + opacity: 0.5; + transition: 0.3s; + } + + .stars span.active { + opacity: 1; + } + + .form-field { + margin-bottom: 20px; + } + + textarea { + width: 100%; + padding: 10px; + background: #2a2a2a; + border: 1px solid #404040; + color: #ffffff; + border-radius: 4px; + } + + .error { + color: #ff4444; + font-size: 12px; + } + + .modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + } + + .btn-primary, .btn-secondary { + padding: 8px 16px; + border-radius: 4px; + border: none; + cursor: pointer; + } + + .btn-primary { + background: #1db954; + color: #ffffff; + } + + .btn-secondary { + background: #404040; + color: #ffffff; + } + + .btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + } + `] +}) +export class ReviewModalComponent { + isOpen = signal(false); + modalGameName = input.required(); + modalGameId = input.required(); + @Output() closeModal = new EventEmitter(); + + rating = signal(0); + reviewForm = signal(this.fb.group({ + rating: [0, [Validators.required, Validators.min(1), Validators.max(5)]], + comment: ['', [Validators.required, Validators.minLength(10)]] + })); + + constructor( + private fb: FormBuilder, + private reviewService: ReviewService, + private authService: AuthService + ) {} + + setRating(value: number): void { + this.rating.set(value); + this.reviewForm().patchValue({ rating: value }); + } + + close(): void { + this.isOpen.set(false); + } + + onSubmit(): void { + if (this.reviewForm().valid) { + const user = this.authService.getCurrentUser(); + + if (!user) { + console.error('Usuário não está logado'); + return; + } + + const review = { + userId: user.uid, + userName: user.displayName || 'Anônimo', + gameId: this.modalGameId(), + rating: this.reviewForm().value.rating, + comment: this.reviewForm().value.comment, + createdAt: new Date() + }; + + this.reviewService.addReview(review) + .then(() => { + this.close(); + this.reviewForm().reset(); + this.rating.set(0); + }) + .catch(error => console.error('Erro ao salvar review:', error)); + } + } + + toggle(): void { + this.isOpen.update(currentValue => !currentValue); + } +} diff --git a/src/app/shared/header/header.component.html b/src/app/shared/header/header.component.html index d6c0571..d74a9d8 100644 --- a/src/app/shared/header/header.component.html +++ b/src/app/shared/header/header.component.html @@ -5,14 +5,18 @@ Arcadius Language
- + diff --git a/src/app/shared/header/header.component.ts b/src/app/shared/header/header.component.ts index fdadcfe..3405e4d 100644 --- a/src/app/shared/header/header.component.ts +++ b/src/app/shared/header/header.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink, RouterModule,RouterOutlet } from '@angular/router'; import { AuthService } from '../../auth/services/auth.service'; +import { map, Observable } from 'rxjs'; @Component({ selector: 'app-header', @@ -12,15 +13,18 @@ import { AuthService } from '../../auth/services/auth.service'; styleUrls: ['./header.component.scss'] }) export class HeaderComponent implements OnInit { - isLoggedIn: boolean = false; + isLoggedIn$: Observable; + isAdmin$: Observable; - constructor(private authService: AuthService, private accessibilityService : AccessibilityService) {} + constructor(private authService: AuthService, private accessibilityService : AccessibilityService) { + this.isLoggedIn$ = this.authService.isLoggedIn$; + this.isAdmin$ = this.authService.isAdmin$; + } ngOnInit(): void { - this.authService.user$.subscribe(user => { - this.isLoggedIn = !!user; - }); - + this.isLoggedIn$ = this.authService.user$.pipe( + map(user => !!user) + ); } speach(text : string) { diff --git a/src/app/shared/navbar/navbar.component.html b/src/app/shared/navbar/navbar.component.html new file mode 100644 index 0000000..e86378d --- /dev/null +++ b/src/app/shared/navbar/navbar.component.html @@ -0,0 +1,6 @@ + diff --git a/src/app/shared/navbar/navbar.component.scss b/src/app/shared/navbar/navbar.component.scss new file mode 100644 index 0000000..84ac61c --- /dev/null +++ b/src/app/shared/navbar/navbar.component.scss @@ -0,0 +1,22 @@ +nav { + background-color: #1e1e1e; + padding: 1rem; + + ul { + list-style: none; + display: flex; + gap: 1rem; + + li { + a { + color: #ffffff; + text-decoration: none; + transition: color 0.3s; + cursor: pointer; + &:hover { + color: #1db954; + } + } + } + } +} diff --git a/src/app/shared/navbar/navbar.component.spec.ts b/src/app/shared/navbar/navbar.component.spec.ts new file mode 100644 index 0000000..78867a6 --- /dev/null +++ b/src/app/shared/navbar/navbar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NavbarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/navbar/navbar.component.ts b/src/app/shared/navbar/navbar.component.ts new file mode 100644 index 0000000..b75e562 --- /dev/null +++ b/src/app/shared/navbar/navbar.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-navbar', + standalone: true, + imports: [RouterLink], + templateUrl: './navbar.component.html', + styleUrl: './navbar.component.scss' +}) +export class NavbarComponent { + +} diff --git a/src/app/shared/pipes/timestamp-to-date.pipe.ts b/src/app/shared/pipes/timestamp-to-date.pipe.ts new file mode 100644 index 0000000..c26bab4 --- /dev/null +++ b/src/app/shared/pipes/timestamp-to-date.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'timestampToDate', + standalone: true +}) +export class TimestampToDatePipe implements PipeTransform { + transform(timestamp: any): Date { + if (timestamp?.seconds) { + return new Date(timestamp.seconds * 1000); + } + return new Date(timestamp); + } +}