diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6c77982..45f8a21 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -347,6 +347,7 @@ src/ 9. Use debouncing for search inputs (use-debounce) 10. Handle loading and error states consistently 11. **Native UI Development**: When the "use-gluestack-components" MCP server is connected and you're working on React Native UI, always consult it first for component selection and implementation patterns. Prefer Gluestack UI components for accessibility and cross-platform consistency. +12. **NEVER use barrel exports** (index.ts/index.js files that re-export from other files). Always import directly from the source file (e.g., `import { Box } from "../layout/box/box"` instead of `import { Box } from "../layout/box"`). Barrel exports cause performance issues, circular dependency problems, and make it harder to track imports. ## Useful Commands ```bash @@ -393,6 +394,7 @@ yarn build-android # EAS build Android - **API**: Uses tsconfig paths (e.g., `@/modules/...`) - **Web**: `@/` → `src/` - **Native**: Relative imports for modules, absolute for ui +- **Important**: Always import directly from source files, NOT from index.ts barrel exports (e.g., use `from "./box/box"` not `from "./box"`) ## Dependencies Management - Use exact versions for critical dependencies diff --git a/apps/api/package.json b/apps/api/package.json index 441fd6d..eacefa0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -29,6 +29,7 @@ "@nestjs/apollo": "^12.0.11", "@nestjs/axios": "^3.0.1", "@nestjs/bull": "^10.0.1", + "@nestjs/cache-manager": "^3.1.0", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/cqrs": "11.0.2", @@ -43,6 +44,7 @@ "@types/user-agents": "^1.0.4", "axios": "^1.6.2", "bull": "^4.11.5", + "cache-manager": "^7.2.8", "cheerio": "^1.0.0-rc.12", "cloudinary": "^1.41.2", "dayjs": "^1.11.10", diff --git a/apps/api/plan-cqrsHexagonalRefactor.prompt.md b/apps/api/plan-cqrsHexagonalRefactor.prompt.md new file mode 100644 index 0000000..272b02f --- /dev/null +++ b/apps/api/plan-cqrsHexagonalRefactor.prompt.md @@ -0,0 +1,74 @@ +## Plan: Refaktoryzacja API pod CQRS i Architekturę Heksagonalną + +Ten plan ma na celu przekształcenie obecnej architektury "N-Tier" w pełnoprawną Architekturę Heksagonalną (Ports & Adapters) z CQRS, aby odseparować logikę biznesową od frameworka i bazy danych. + +### Główne Założenia +* **Domain Layer (Jądro)**: Czyste klasy TypeScript (Encje, Value Objects). Zero zależności od NestJS, Prisma czy zewnętrznych bibliotek. +* **Application Layer**: Obsługa Use Cases (CQRS Handlers). Definiuje interfejsy (Porty) dla komunikacji ze światem zewnętrznym. +* **Infrastructure Layer**: Implementacje interfejsów (Adaptery). Tu żyje Prisma, Resolvery GraphQL, Kontrolery REST i konfiguracja modułów NestJS. + +### Struktura folderów (Nowy schemat) +Dla każdego modułu (np. `games`, `users`) zastosujemy strukturę: +``` +modules// +├── domain/ # WARSTWA DOMENY +│ ├── models/ # Encje (np. Game, User) i Value Objects +│ └── ports/ # Interfejsy repozytoriów i serwisów zewnętrznych +├── application/ # WARSTWA APLIKACJI +│ ├── commands/ # Komendy i ich Handlery (WRITE model) +│ └── queries/ # Zapytania i ich Handlery (READ model) +└── infrastructure/ # WARSTWA INFRASTRUKTURY + ├── adapters/ # Implementacje portów (np. PrismaGameRepository) + ├── graphql/ # Resolvery i DTO (Inputs/Args) + └── .module.ts # Konfiguracja modułu (DI) +``` + +### Steps + +#### Faza 1: Przygotowanie Fundamentów +1. **Utwórz współdzielone abstrakcje w `libs/ddd` (lub `shared`)** + * Stwórz klasy bazowe `AggregateRoot` i `Entity` dla domeny. + * Zdefiniuj typy generyczne dla `RepositoryPort`. + +#### Faza 2: Migracja Modułu `Games` (Jako Pilot) +To najbardziej złożony moduł, idealny do przetestowania wzorca. + +1. **Wyodrębnij Domenę (`domain/`)** + * Stwórz encję `Game` w `domain/models/game.model.ts`. Przenieś tam logikę biznesową (np. walidacje stanu, metody operujące na danych), która obecnie jest rozproszona w serwisach. + * Zdefiniuj interfejs `GameRepositoryPort` w `domain/ports/`. Powinien operować na encjach domeny, nie na typach Prisma. + +2. **Przenieś Warstwę Aplikacji (`application/`)** + * Przenieś obecne `commands/` i `queries/` do folderu `application/`. + * W `UpdateGameDataHandler`: + * Zamiast `PrismaService`, wstrzyknij `GameRepositoryPort`. + * Zamiast bezpośrednich operacji na DB: pobierz encję -> wykonaj metodę na encji -> zapisz encję. + +3. **Zbuduj Warstwę Infrastruktury (`infrastructure/`)** + * Stwórz `PrismaGameRepository` w `adapters/`, który implementuje `GameRepositoryPort`. + * To tutaj przenieś logikę mapowania z obiektów Prisma na Encje Domenowe (Mappery). + * Przenieś `GamesResolver` do `infrastructure/graphql/`. + +4. **Skonfiguruj Dependency Injection** + * W `GamesModule` zarejestruj implementację portu: `provide: GameRepositoryPort, useClass: PrismaGameRepository`. + +#### Faza 3: Migracja Modułu `Users` i `Profiles` +Powtórz proces dla zarządzania użytkownikami, co uporządkuje zależności Auth0 i seedowania. + +1. **Separacja Modeli** + * Stwórz domenowy model `User`, który posiada metody biznesowe (np. `updateProfile`, `addFriend`). + * Oddziel model bazy danych (Prisma User) od modelu domeny. + +2. **Adaptery Zewnętrzne** + * Dla integracji z Auth0 (User Management), stwórz port `IdentityServicePort` w domenie i adapter w infrastrukturze. + +#### Faza 4: CQRS - Rozdzielenie Modelu Odczytu (Read Model) +Dla zapytań (Queries), pełna heksagonalna "czystość" bywa uciążliwa (overhead przy mapowaniu). + +1. **Optymalizacja Queries** + * Zezwól Handlerom Query (`application/queries/`) na bezpośredni dostęp do `PrismaService` lub dedykowanych widoków (Raw SQL), pomijając Encje Domenowe na rzecz szybkich DTO (Read Models). + * Dla Command (zapis) **zawsze** używaj Repozytoriów i Encji. + +### Further Considerations +1. **Mappery**: Będziesz potrzebował mapperów (np. `GameMapper`), które konwertują `PrismaGame` <-> `DomainGame`. To wymaga napisania dodatkowego kodu, ale daje niezależność. +2. **Komunikacja między modułami**: Moduł `Games` nie powinien importować `UsersService`. Zamiast tego `Games` powinien zdefiniować port `OwnerServicePort`, a moduł `Users` powinien go zaimplementować (lub użyć QueryBus/CommandBus do komunikacji). +3. **Transakcje**: W architekturze heksagonalnej transakcje są trudniejsze. Rozważ użycie `UnitOfWork` lub mechanizmu transakcji NestJS (`ClsService`) w warstwie aplikacji. diff --git a/apps/api/src/modules/igdb/igdb_auth/dtos/igdb.dto.ts b/apps/api/src/infrastructure/igdb/igdb.dto.ts similarity index 100% rename from apps/api/src/modules/igdb/igdb_auth/dtos/igdb.dto.ts rename to apps/api/src/infrastructure/igdb/igdb.dto.ts diff --git a/apps/api/src/infrastructure/igdb/igdb.service.ts b/apps/api/src/infrastructure/igdb/igdb.service.ts new file mode 100644 index 0000000..e82ab91 --- /dev/null +++ b/apps/api/src/infrastructure/igdb/igdb.service.ts @@ -0,0 +1,132 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { ExternalGameDTO } from '../../modules/games/games.dto'; +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse, isAxiosError } from 'axios'; +import { IGamesProvider } from '../../modules/games/interfaces/games-provider.interface'; +import { OAuthTokenDto } from './igdb.dto'; +import { timestampToMs } from '../../modules/date_and_time/time/timestamp_to_ms'; + +@Injectable() +export class IgdbService implements IGamesProvider { + private readonly logger = new Logger(IgdbService.name); + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) {} + + private accessToken: string | null = null; + private tokenExpiration: number = 0; + + async getUpcomingGames(limit: number): Promise { + const now = Math.floor(Date.now() / 1000); + + const query = ` + fields name, cover.url, screenshots.url, first_release_date, platforms.name, category; + + where + first_release_date > ${now} & + platforms = (48,167,49,169,130) & + cover != null & + game_type = (0, 2, 4, 8, 9) & + version_parent = null; + + sort first_release_date asc; + limit ${limit}; +`; + + const token = await this.getTokenFromOAuth(); + + try { + const { data } = await firstValueFrom( + this.httpService.post('https://api.igdb.com/v4/games', query, { + headers: { + 'Client-ID': this.configService.get('IGDB_CLIENT_ID'), + Authorization: `Bearer ${token}`, + }, + }), + ); + return data.map(this.mapToDto); + } catch (e) { + this.logger.error('Error fetching games from IGDB', e); + return []; + } + } + + async getTokenFromOAuth(): Promise { + const now = Date.now(); + + if (this.accessToken && this.tokenExpiration > now + 3600 * 1000) { + return this.accessToken; + } + this.logger.log('Refreshing IGDB Access Token...'); + try { + const { data } = await firstValueFrom>( + this.httpService.post('https://id.twitch.tv/oauth2/token', null, { + params: { + client_id: this.configService.get('IGDB_CLIENT_ID'), + client_secret: this.configService.get('IGDB_CLIENT_SECRET'), + grant_type: 'client_credentials', + }, + }), + ); + this.accessToken = data.access_token; + this.tokenExpiration = now + timestampToMs(data.expires_in); + return this.accessToken; + } catch (e) { + if (isAxiosError(e)) { + this.logger.error( + 'Failed to authenticate with Twitch/IGDB', + e.response?.data || e.message, + ); + } + throw e; + } + } + + private mapToDto(game: IgdbGame): ExternalGameDTO { + return { + id: game.id.toString(), + name: game.name, + coverUrl: game.cover?.url + ? `https:${game.cover.url.replace('t_thumb', 't_cover_big')}` + : '', + backgroundUrl: game.screenshots?.[0]?.url + ? `https:${game.screenshots[0].url.replace( + 't_thumb', + 't_screenshot_big', + )}` + : '', + releaseDate: new Date(game.first_release_date * 1000), + platforms: + game.platforms?.map((p: any) => ({ + id: p.id.toString(), + name: p.name, + })) || [], + }; + } +} + +type IgdbGame = { + id: string; + cover: { + id: string; + url: string; + }; + first_release_date: number; + name: string; + platforms: IgdbGamePlatform[]; + screenshots: IgdbGameScreenshots[]; +}; + +type IgdbGamePlatform = { + id: string; + name: string; +}; + +type IgdbGameScreenshots = { + id: string; + url: string; +}; diff --git a/apps/api/src/libs/ddd/aggregate-root.base.ts b/apps/api/src/libs/ddd/aggregate-root.base.ts new file mode 100644 index 0000000..5e9040e --- /dev/null +++ b/apps/api/src/libs/ddd/aggregate-root.base.ts @@ -0,0 +1,3 @@ +import { Entity } from './entity.base'; + +export abstract class AggregateRoot extends Entity {} diff --git a/apps/api/src/libs/ddd/entity.base.ts b/apps/api/src/libs/ddd/entity.base.ts new file mode 100644 index 0000000..442614b --- /dev/null +++ b/apps/api/src/libs/ddd/entity.base.ts @@ -0,0 +1,17 @@ +export abstract class Entity { + protected readonly _id: number; + protected props: Props; + + constructor(props: Props, id?: number) { + this._id = id ? id : 0; + this.props = props; + } + + get id(): number { + return this._id; + } + + public getProps(): Props { + return { ...this.props }; + } +} diff --git a/apps/api/src/libs/ddd/repository.port.ts b/apps/api/src/libs/ddd/repository.port.ts new file mode 100644 index 0000000..698eb33 --- /dev/null +++ b/apps/api/src/libs/ddd/repository.port.ts @@ -0,0 +1,3 @@ +export interface RepositoryPort { + save(entity: Entity): Promise; +} diff --git a/apps/api/src/modules/auth/commands/create_user/create_user.command.ts b/apps/api/src/modules/auth/application/commands/create_user/create_user.command.ts similarity index 100% rename from apps/api/src/modules/auth/commands/create_user/create_user.command.ts rename to apps/api/src/modules/auth/application/commands/create_user/create_user.command.ts diff --git a/apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts b/apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts new file mode 100644 index 0000000..d7a1842 --- /dev/null +++ b/apps/api/src/modules/auth/application/commands/create_user/create_user.handler.ts @@ -0,0 +1,32 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { CreateUserCommand } from './create_user.command'; +import { Inject } from '@nestjs/common'; +import { + AUTH_REPOSITORY, + AuthRepositoryPort, +} from '../../../domain/ports/auth.repository.port'; +import { AuthUser, UserRole } from '../../../domain/models/auth-user.model'; + +@CommandHandler(CreateUserCommand) +export class CreateUserCommandHandler + implements ICommandHandler +{ + constructor( + @Inject(AUTH_REPOSITORY) + private readonly authRepository: AuthRepositoryPort, + ) {} + + async execute(command: CreateUserCommand) { + const newUser = AuthUser.create({ + oauthId: command.oauthId, + role: UserRole.USER, + }); + + const savedUser = await this.authRepository.save(newUser); + + return { + id: savedUser.id, + role: savedUser.role, + }; + } +} diff --git a/apps/api/src/modules/auth/queries/get_user_role/get_user_role.handler.ts b/apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.handler.ts similarity index 91% rename from apps/api/src/modules/auth/queries/get_user_role/get_user_role.handler.ts rename to apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.handler.ts index e952255..f873903 100644 --- a/apps/api/src/modules/auth/queries/get_user_role/get_user_role.handler.ts +++ b/apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.handler.ts @@ -1,6 +1,6 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; import { GetUserRoleQuery } from './get_user_role.query'; -import { PrismaService } from '../../../database/prisma.service'; +import { PrismaService } from '../../../../database/prisma.service'; @QueryHandler(GetUserRoleQuery) export class GetUserRoleQueryHandler diff --git a/apps/api/src/modules/auth/queries/get_user_role/get_user_role.query.ts b/apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.query.ts similarity index 100% rename from apps/api/src/modules/auth/queries/get_user_role/get_user_role.query.ts rename to apps/api/src/modules/auth/application/queries/get_user_role/get_user_role.query.ts diff --git a/apps/api/src/modules/auth/auth.module.ts b/apps/api/src/modules/auth/auth.module.ts index 9de4918..671445e 100644 --- a/apps/api/src/modules/auth/auth.module.ts +++ b/apps/api/src/modules/auth/auth.module.ts @@ -1,15 +1,17 @@ import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; -import { OAuthJwtStrategy } from './strategies/o-auth-jwt-strategy.service'; -import { AuthResolver } from './auth.resolver'; +import { OAuthJwtStrategy } from './infrastructure/strategies/o-auth-jwt-strategy.service'; +import { AuthResolver } from './infrastructure/graphql/auth.resolver'; import { ConfigService } from '@nestjs/config'; -import { AuthService } from './auth.service'; +import { AuthService } from './infrastructure/services/auth.service'; import { DatabaseModule } from '../database/database.module'; import { BullModule } from '@nestjs/bull'; import { HttpModule } from '@nestjs/axios'; import { CqrsModule } from '@nestjs/cqrs'; -import { GetUserRoleQueryHandler } from './queries/get_user_role/get_user_role.handler'; -import { CreateUserCommandHandler } from './commands/create_user/create_user.handler'; +import { GetUserRoleQueryHandler } from './application/queries/get_user_role/get_user_role.handler'; +import { CreateUserCommandHandler } from './application/commands/create_user/create_user.handler'; +import { AUTH_REPOSITORY } from './domain/ports/auth.repository.port'; +import { PrismaAuthRepository } from './infrastructure/adapters/prisma-auth.repository'; const handlers = [GetUserRoleQueryHandler, CreateUserCommandHandler]; @@ -26,7 +28,16 @@ const handlers = [GetUserRoleQueryHandler, CreateUserCommandHandler]; inject: [ConfigService], }), ], - providers: [OAuthJwtStrategy, AuthResolver, AuthService, ...handlers], + providers: [ + OAuthJwtStrategy, + AuthResolver, + AuthService, + ...handlers, + { + provide: AUTH_REPOSITORY, + useClass: PrismaAuthRepository, + }, + ], exports: [PassportModule, AuthService], }) export class AuthModule {} diff --git a/apps/api/src/modules/auth/commands/create_user/create_user.handler.ts b/apps/api/src/modules/auth/commands/create_user/create_user.handler.ts deleted file mode 100644 index 7a0eb15..0000000 --- a/apps/api/src/modules/auth/commands/create_user/create_user.handler.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { CreateUserCommand } from './create_user.command'; -import { RoleEnum } from '@prisma/client'; -import { PrismaService } from '../../../database/prisma.service'; - -@CommandHandler(CreateUserCommand) -export class CreateUserCommandHandler - implements ICommandHandler -{ - constructor(private readonly prismaService: PrismaService) {} - - async execute(command: CreateUserCommand) { - const { id, role } = await this.createUser(command.oauthId); - return { - id, - role, - }; - } - - async createUser(oauthId: string) { - return this.prismaService.$transaction(async (prisma) => { - const userRole = await prisma.role.findFirst({ - where: { - name: RoleEnum.USER, - }, - }); - if (!userRole) { - throw new Error('Role not found'); - } - const user = await prisma.user.create({ - data: { - oauthId, - role: { - connectOrCreate: { - where: { - oauthId, - }, - create: { - roleId: userRole.id, - }, - }, - }, - }, - }); - return { - ...user, - role: userRole.name, - }; - }); - } -} diff --git a/apps/api/src/modules/auth/domain/models/auth-user.model.ts b/apps/api/src/modules/auth/domain/models/auth-user.model.ts new file mode 100644 index 0000000..f09e95a --- /dev/null +++ b/apps/api/src/modules/auth/domain/models/auth-user.model.ts @@ -0,0 +1,23 @@ +import { AggregateRoot } from '../../../../libs/ddd/aggregate-root.base'; +import { RoleEnum } from '@prisma/client'; + +export interface AuthUserProps { + oauthId: string; + role: RoleEnum; +} + +export class AuthUser extends AggregateRoot { + get oauthId(): string { + return this.props.oauthId; + } + + get role(): RoleEnum { + return this.props.role; + } + + static create(props: AuthUserProps): AuthUser { + return new AuthUser(props); + } +} + +export { RoleEnum as UserRole }; diff --git a/apps/api/src/modules/auth/domain/ports/auth.repository.port.ts b/apps/api/src/modules/auth/domain/ports/auth.repository.port.ts new file mode 100644 index 0000000..9011bdb --- /dev/null +++ b/apps/api/src/modules/auth/domain/ports/auth.repository.port.ts @@ -0,0 +1,8 @@ +import { RepositoryPort } from '../../../../libs/ddd/repository.port'; +import { AuthUser } from '../models/auth-user.model'; + +export const AUTH_REPOSITORY = Symbol('AUTH_REPOSITORY'); + +export interface AuthRepositoryPort extends RepositoryPort { + findUserByOauthId(oauthId: string): Promise; +} diff --git a/apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts b/apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts new file mode 100644 index 0000000..4e8496c --- /dev/null +++ b/apps/api/src/modules/auth/infrastructure/adapters/prisma-auth.repository.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../../database/prisma.service'; +import { AuthRepositoryPort } from '../../domain/ports/auth.repository.port'; +import { AuthUser, UserRole } from '../../domain/models/auth-user.model'; +import { RoleEnum } from '@prisma/client'; + +@Injectable() +export class PrismaAuthRepository implements AuthRepositoryPort { + constructor(private readonly prismaService: PrismaService) {} + + async save(user: AuthUser): Promise { + const roleName = + user.role === UserRole.ADMIN ? RoleEnum.ADMIN : RoleEnum.USER; + + const saved = await this.prismaService.$transaction(async (prisma) => { + const userRole = await prisma.role.findFirst({ + where: { name: roleName }, + }); + + if (!userRole) { + throw new Error('Role not found'); + } + + const newUser = await prisma.user.create({ + data: { + oauthId: user.oauthId, + role: { + connectOrCreate: { + where: { + oauthId: user.oauthId, + }, + create: { + roleId: userRole.id, + }, + }, + }, + }, + }); + return { ...newUser, roleName: userRole.name }; + }); + + const role = + saved.roleName === RoleEnum.ADMIN ? UserRole.ADMIN : UserRole.USER; + return new AuthUser({ oauthId: saved.oauthId, role }, saved.id); + } + + async findUserByOauthId(oauthId: string): Promise { + const user = await this.prismaService.user.findUnique({ + where: { oauthId }, + include: { role: { include: { role: true } } }, + }); + + if (!user || !user.role) return null; + + const roleName = user.role.role.name; + const role = roleName === RoleEnum.ADMIN ? UserRole.ADMIN : UserRole.USER; + return new AuthUser({ oauthId: user.oauthId, role }, user.id); + } +} diff --git a/apps/api/src/modules/auth/auth.decorators.ts b/apps/api/src/modules/auth/infrastructure/decorators/auth.decorators.ts similarity index 100% rename from apps/api/src/modules/auth/auth.decorators.ts rename to apps/api/src/modules/auth/infrastructure/decorators/auth.decorators.ts diff --git a/apps/api/src/modules/auth/auth.dto.ts b/apps/api/src/modules/auth/infrastructure/graphql/auth.dto.ts similarity index 100% rename from apps/api/src/modules/auth/auth.dto.ts rename to apps/api/src/modules/auth/infrastructure/graphql/auth.dto.ts diff --git a/apps/api/src/modules/auth/auth.model.ts b/apps/api/src/modules/auth/infrastructure/graphql/auth.model.ts similarity index 100% rename from apps/api/src/modules/auth/auth.model.ts rename to apps/api/src/modules/auth/infrastructure/graphql/auth.model.ts diff --git a/apps/api/src/modules/auth/auth.resolver.ts b/apps/api/src/modules/auth/infrastructure/graphql/auth.resolver.ts similarity index 59% rename from apps/api/src/modules/auth/auth.resolver.ts rename to apps/api/src/modules/auth/infrastructure/graphql/auth.resolver.ts index 500642b..175d4f4 100644 --- a/apps/api/src/modules/auth/auth.resolver.ts +++ b/apps/api/src/modules/auth/infrastructure/graphql/auth.resolver.ts @@ -1,15 +1,22 @@ import { Context, Query, Resolver } from '@nestjs/graphql'; -import { AuthService } from './auth.service'; +import { AuthService } from '../services/auth.service'; import { Prisma } from '@prisma/client'; import { UseGuards } from '@nestjs/common'; import { AuthUserVerification } from './auth.model'; -import { JwtAuthGuard } from './guards/auth-jwt.guard'; -import { User } from './auth.decorators'; +import { JwtAuthGuard } from '../guards/auth-jwt.guard'; +import { User } from '../decorators/auth.decorators'; import { UserAuthDTO } from './auth.dto'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; +import { CreateUserCommand } from '../../application/commands/create_user/create_user.command'; +import { GetUserRoleQuery } from '../../application/queries/get_user_role/get_user_role.query'; @Resolver() export class AuthResolver { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly commandBus: CommandBus, + private readonly queryBus: QueryBus, + ) {} @UseGuards(JwtAuthGuard) @Query(() => AuthUserVerification) async verify( @@ -25,7 +32,9 @@ export class AuthResolver { } const userInfo = await this.authService.getUserInfoFromAuth0(authorizationHeader); - const { id, role } = await this.authService.createUser(user.sub); + const { id, role } = await this.commandBus.execute( + new CreateUserCommand(user.sub), + ); await this.authService.addUserCreatedEvent(id, userInfo.nickname); return { authorized: true, @@ -36,7 +45,9 @@ export class AuthResolver { e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002' ) { - const userRole = await this.authService.getUserRole(user.sub); + const userRole = await this.queryBus.execute( + new GetUserRoleQuery(user.sub), + ); return { authorized: true, role: userRole?.role || null, diff --git a/apps/api/src/modules/auth/guards/admin-user.guard.ts b/apps/api/src/modules/auth/infrastructure/guards/admin-user.guard.ts similarity index 100% rename from apps/api/src/modules/auth/guards/admin-user.guard.ts rename to apps/api/src/modules/auth/infrastructure/guards/admin-user.guard.ts diff --git a/apps/api/src/modules/auth/guards/auth-jwt.guard.ts b/apps/api/src/modules/auth/infrastructure/guards/auth-jwt.guard.ts similarity index 72% rename from apps/api/src/modules/auth/guards/auth-jwt.guard.ts rename to apps/api/src/modules/auth/infrastructure/guards/auth-jwt.guard.ts index ea6ea88..6c05a88 100644 --- a/apps/api/src/modules/auth/guards/auth-jwt.guard.ts +++ b/apps/api/src/modules/auth/infrastructure/guards/auth-jwt.guard.ts @@ -1,11 +1,12 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { GqlExecutionContext } from '@nestjs/graphql'; -import { AuthService } from '../auth.service'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetUserRoleQuery } from '../../application/queries/get_user_role/get_user_role.query'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { - constructor(private readonly authService: AuthService) { + constructor(private readonly queryBus: QueryBus) { super(); } @@ -16,7 +17,9 @@ export class JwtAuthGuard extends AuthGuard('jwt') { } const request = this.getRequest(context); const user = request.user; - const userWithRole = await this.authService.getUserRole(user.sub); + const userWithRole = await this.queryBus.execute( + new GetUserRoleQuery(user.sub), + ); if (userWithRole) { request.user = { ...user, diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/infrastructure/services/auth.service.ts similarity index 58% rename from apps/api/src/modules/auth/auth.service.ts rename to apps/api/src/modules/auth/infrastructure/services/auth.service.ts index 084ff15..d1ecc21 100644 --- a/apps/api/src/modules/auth/auth.service.ts +++ b/apps/api/src/modules/auth/infrastructure/services/auth.service.ts @@ -3,24 +3,15 @@ import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; -import { UserAuth0InfoDTO } from './auth.dto'; -import { CommandBus, QueryBus } from '@nestjs/cqrs'; -import { CreateUserCommand } from './commands/create_user/create_user.command'; -import { GetUserRoleQuery } from './queries/get_user_role/get_user_role.query'; +import { UserAuth0InfoDTO } from '../graphql/auth.dto'; @Injectable() export class AuthService { constructor( private readonly httpService: HttpService, - private readonly commandBus: CommandBus, - private readonly queryBus: QueryBus, @InjectQueue('user_created') private userCreatedQueue: Queue, ) {} - createUser(oauthId: string) { - return this.commandBus.execute(new CreateUserCommand(oauthId)); - } - async addUserCreatedEvent(userId: number, username: string) { await this.userCreatedQueue.add('userCreated', { userId, username }); } @@ -35,8 +26,4 @@ export class AuthService { ); return data; } - - async getUserRole(oauthId: string) { - return this.queryBus.execute(new GetUserRoleQuery(oauthId)); - } } diff --git a/apps/api/src/modules/auth/strategies/o-auth-jwt-strategy.service.ts b/apps/api/src/modules/auth/infrastructure/strategies/o-auth-jwt-strategy.service.ts similarity index 95% rename from apps/api/src/modules/auth/strategies/o-auth-jwt-strategy.service.ts rename to apps/api/src/modules/auth/infrastructure/strategies/o-auth-jwt-strategy.service.ts index b6dd218..8ced461 100644 --- a/apps/api/src/modules/auth/strategies/o-auth-jwt-strategy.service.ts +++ b/apps/api/src/modules/auth/infrastructure/strategies/o-auth-jwt-strategy.service.ts @@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { passportJwtSecret } from 'jwks-rsa'; -import { UserAuthDTO } from '../auth.dto'; +import { UserAuthDTO } from '../graphql/auth.dto'; type OAuthPayload = { sub: string; diff --git a/apps/api/src/modules/collections/collection.resolver.ts b/apps/api/src/modules/collections/collection.resolver.ts index c6541d4..a37ff57 100644 --- a/apps/api/src/modules/collections/collection.resolver.ts +++ b/apps/api/src/modules/collections/collection.resolver.ts @@ -1,6 +1,6 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { HttpException, HttpStatus, UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { AddGameToCollectionDTO, CollectionDTO, @@ -10,8 +10,8 @@ import { RemoveCollectionArgsDTO, RemovedCollectionResponseDTO, } from './collections.dto'; -import { UserAuthDTO } from '../auth/auth.dto'; -import { User } from '../auth/auth.decorators'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; import { ProfilesService } from '../profiles/profiles.service'; import { CollectionsService } from './collections.service'; diff --git a/apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts b/apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts new file mode 100644 index 0000000..2937027 --- /dev/null +++ b/apps/api/src/modules/date_and_time/time/timestamp_to_ms.ts @@ -0,0 +1,3 @@ +export const timestampToMs = (timestamp: number): number => { + return timestamp * 1000; +}; diff --git a/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts b/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts index 51eeb2e..c036031 100644 --- a/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts +++ b/apps/api/src/modules/friends/friends_activity/friends_activity.resolver.ts @@ -1,8 +1,8 @@ import { Injectable, NotFoundException, UseGuards } from '@nestjs/common'; import { FriendsActivityService } from './friends_activity.service'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; import { FriendsActivityDTO } from './friends_activity.dto'; import { Query } from '@nestjs/graphql'; diff --git a/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts b/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts index 7e07e5e..ad6dc33 100644 --- a/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts +++ b/apps/api/src/modules/friends/friends_list/friends_list.resolver.ts @@ -1,9 +1,9 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Query } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; import { FriendsListService } from './friends_list.service'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; import { FriendsListDTO } from './friends_list.dto'; @Injectable() diff --git a/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts b/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts index 9725f04..9d71a36 100644 --- a/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts +++ b/apps/api/src/modules/friends/friends_requests/friends_requests.resolver.ts @@ -1,13 +1,13 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; import { FriendRequestResponseDTO, GetFriendRequestsResponseDTO, } from './friends_requests.dto'; -import { User } from '../../auth/auth.decorators'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; import { FriendsRequestsService } from './friends_requests.service'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; @Injectable() export class FriendsRequestsResolver { diff --git a/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts b/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts index a9d9a36..14c20a7 100644 --- a/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts +++ b/apps/api/src/modules/friends/friends_search/friends_search.resolver.ts @@ -1,8 +1,8 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Args, Query } from '@nestjs/graphql'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; import { UserSearchResultDTO } from './friends_resolver.dto'; import { FriendsSearchService } from './friends_search.service'; diff --git a/apps/api/src/modules/games/games.dto.ts b/apps/api/src/modules/games/games.dto.ts index 94d1b41..da06d32 100644 --- a/apps/api/src/modules/games/games.dto.ts +++ b/apps/api/src/modules/games/games.dto.ts @@ -87,3 +87,33 @@ export class UpdateGameDataDTO { @Field(() => String) message: string; } + +@ObjectType() +export class ExternalGamePlatformDTO { + @Field() + id: string; + + @Field() + name: string; +} + +@ObjectType() +export class ExternalGameDTO { + @Field() + id: string; + + @Field() + name: string; + + @Field({ nullable: true }) + coverUrl?: string; + + @Field({ nullable: true }) + backgroundUrl?: string; // Np. screenshot + + @Field() + releaseDate: Date; + + @Field(() => [ExternalGamePlatformDTO]) + platforms: ExternalGamePlatformDTO[]; +} diff --git a/apps/api/src/modules/games/games.module.ts b/apps/api/src/modules/games/games.module.ts index b879e45..10b6c32 100644 --- a/apps/api/src/modules/games/games.module.ts +++ b/apps/api/src/modules/games/games.module.ts @@ -10,8 +10,15 @@ import { AuthModule } from '../auth/auth.module'; import { CommandHandlerType, CqrsModule, QueryHandlerType } from '@nestjs/cqrs'; import { GetGamesQueryHandler } from './queries/get_games/get_games.handler'; import { UpdateGameDataHandler } from './commands/update_game_data/update_game_data.handler'; +import { IgdbService } from '../../infrastructure/igdb/igdb.service'; +import { HttpModule } from '@nestjs/axios'; +import { GetUpcomingGamesHandler } from './queries/get_upcoming_games/get_upcoming_games.handler'; +import { CacheModule } from '@nestjs/cache-manager'; -const QueryHandlers: QueryHandlerType[] = [GetGamesQueryHandler]; +const QueryHandlers: QueryHandlerType[] = [ + GetGamesQueryHandler, + GetUpcomingGamesHandler, +]; const CommandHandlers: CommandHandlerType[] = [UpdateGameDataHandler]; @Module({ @@ -21,6 +28,8 @@ const CommandHandlers: CommandHandlerType[] = [UpdateGameDataHandler]; HowLongToBeatParserModule, AuthModule, CqrsModule, + HttpModule, + CacheModule.register(), ], providers: [ GamesService, @@ -29,6 +38,10 @@ const CommandHandlers: CommandHandlerType[] = [UpdateGameDataHandler]; GamesResolver, ...QueryHandlers, ...CommandHandlers, + { + provide: 'GAMES_PROVIDER', + useClass: IgdbService, + }, ], exports: [GamesService], }) diff --git a/apps/api/src/modules/games/games.resolver.ts b/apps/api/src/modules/games/games.resolver.ts index 3fe66ff..a973089 100644 --- a/apps/api/src/modules/games/games.resolver.ts +++ b/apps/api/src/modules/games/games.resolver.ts @@ -1,18 +1,24 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { GamesService } from './games.service'; import { + ExternalGameDTO, GameWithAllDataDTO, GetPaginatedGamesArgs, PaginatedGamesDTO, UpdateGameDataDTO, } from './games.dto'; import { UseGuards } from '@nestjs/common'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetUpcomingGamesQuery } from './queries/get_upcoming_games/get_upcoming_games.query'; @Resolver() export class GamesResolver { - constructor(private readonly gamesService: GamesService) {} + constructor( + private readonly gamesService: GamesService, + private readonly queryBus: QueryBus, + ) {} @Query(() => GameWithAllDataDTO, { name: 'game' }) async getGameById(@Args('hltbId') hltbId: number) { @@ -37,4 +43,13 @@ export class GamesResolver { ): Promise { return this.gamesService.updateGameData(hltbId); } + + @Query(() => [ExternalGameDTO], { name: 'upcomingGames' }) + async getUpcomingGames( + @Args('limit') limit: number, + ): Promise { + return this.queryBus.execute( + new GetUpcomingGamesQuery(limit), + ); + } } diff --git a/apps/api/src/modules/games/interfaces/games-provider.interface.ts b/apps/api/src/modules/games/interfaces/games-provider.interface.ts new file mode 100644 index 0000000..f4b2adb --- /dev/null +++ b/apps/api/src/modules/games/interfaces/games-provider.interface.ts @@ -0,0 +1,7 @@ +import { ExternalGameDTO } from '../games.dto'; + +export interface IGamesProvider { + getUpcomingGames(limit: number): Promise; +} + +export const GAMES_PROVIDER = 'GAMES_PROVIDER'; diff --git a/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts new file mode 100644 index 0000000..0c46b58 --- /dev/null +++ b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.handler.ts @@ -0,0 +1,33 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { GetUpcomingGamesQuery } from './get_upcoming_games.query'; +import { Inject } from '@nestjs/common'; +import { + GAMES_PROVIDER, + IGamesProvider, +} from '../../interfaces/games-provider.interface'; +import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager'; + +const CACHE_TTL_SECONDS = 3600; // 1 hour + +@QueryHandler(GetUpcomingGamesQuery) +export class GetUpcomingGamesHandler + implements IQueryHandler +{ + constructor( + @Inject(GAMES_PROVIDER) private readonly gamesProvider: IGamesProvider, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) {} + + async execute({ limit }: GetUpcomingGamesQuery) { + const cacheKey = `upcoming_games_${limit}`; + const cachedData = await this.cacheManager.get(cacheKey); + + if (cachedData) { + return cachedData; + } + + const games = await this.gamesProvider.getUpcomingGames(limit); + await this.cacheManager.set(cacheKey, games, CACHE_TTL_SECONDS); + return games; + } +} diff --git a/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts new file mode 100644 index 0000000..f8fe04e --- /dev/null +++ b/apps/api/src/modules/games/queries/get_upcoming_games/get_upcoming_games.query.ts @@ -0,0 +1,9 @@ +import { Query } from '@nestjs/cqrs'; + +export class GetUpcomingGamesQuery extends Query { + constructor(public readonly limit: number) { + super(); + } +} + +export type GetUpcomingGamesQueryResponse = any; diff --git a/apps/api/src/modules/games_status/games_status.dto.ts b/apps/api/src/modules/games_status/games_status.dto.ts index cada572..349b12b 100644 --- a/apps/api/src/modules/games_status/games_status.dto.ts +++ b/apps/api/src/modules/games_status/games_status.dto.ts @@ -9,6 +9,7 @@ import { PlatformDTO } from '../platforms/platforms.dto'; import { GameWithAllDataDTO } from '../games/games.dto'; import { PaginationDTO } from '../pagination/pagination.dto'; import { ProfileInfoDTO } from '../profiles/profiles.dto'; +import { CoverDTO } from '../covers/covers.dto'; registerEnumType(GameStatus, { name: 'gameStatus', @@ -203,3 +204,15 @@ export class FriendsGameStatusReviewsDTO { @Field(() => String, { nullable: true }) score: string | null; } + +@ObjectType({ description: 'User last edited games statuses' }) +export class LastEditedGamesStatusDTO { + @Field(() => Number) + id: number; + @Field(() => String) + name: string; + @Field(() => CoverDTO, { nullable: true }) + cover: CoverDTO | null; + @Field(() => GameStatus) + status: GameStatus; +} diff --git a/apps/api/src/modules/games_status/games_status.module.ts b/apps/api/src/modules/games_status/games_status.module.ts index f8144d4..bfe2d06 100644 --- a/apps/api/src/modules/games_status/games_status.module.ts +++ b/apps/api/src/modules/games_status/games_status.module.ts @@ -8,9 +8,11 @@ import { AuthModule } from '../auth/auth.module'; import { CommandHandlerType, CqrsModule, QueryHandlerType } from '@nestjs/cqrs'; import { GetAllUserGamesStatusByOauthIdHandler } from './queries/get_all_user_games_status_by_oauthid/get_all_user_games_status_by_oauthid.handler'; import { RemoveUserGameStatusByUserOauthIdHandler } from './commands/remove_user_game_status_by_user_oauth_id/remove_user_game_status_by_user_oauth_id.handler'; +import { GetLastEditedGamesHandler } from './queries/get_last_edited_games/get_last_edited_games.handler'; const QueryHandlers: QueryHandlerType[] = [ GetAllUserGamesStatusByOauthIdHandler, + GetLastEditedGamesHandler, ]; const CommandHandlers: CommandHandlerType[] = [ diff --git a/apps/api/src/modules/games_status/games_status.resolver.ts b/apps/api/src/modules/games_status/games_status.resolver.ts index 1cf9d56..a3c55e1 100644 --- a/apps/api/src/modules/games_status/games_status.resolver.ts +++ b/apps/api/src/modules/games_status/games_status.resolver.ts @@ -1,4 +1,4 @@ -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UpsertGameStatusArgsDTO, GameStatusSuccessResponseDTO, @@ -9,21 +9,27 @@ import { SortOptionsDTO, GameStatusProgressStateDTO, UserFriendGamesStatusResponseWithPaginationDTO, + LastEditedGamesStatusDTO, } from './games_status.dto'; import { GamesStatusService } from './games_status.service'; import { HttpException, HttpStatus, UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { User } from '../auth/auth.decorators'; -import { UserAuthDTO } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; import { GetAllUserFriendGamesStatusArgs, GetAllUserGamesStatusArgs, } from './games_status.args'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { QueryBus } from '@nestjs/cqrs'; +import { GetLastEditedGamesQuery } from './queries/get_last_edited_games/get_last_edited_games.query'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Resolver() export class GamesStatusResolver { - constructor(private readonly gamesStatusService: GamesStatusService) {} + constructor( + private readonly gamesStatusService: GamesStatusService, + private readonly queryBus: QueryBus, + ) {} @UseGuards(JwtAuthGuard) @Mutation(() => GameStatusSuccessResponseDTO) async upsertGameStatus( @@ -217,4 +223,15 @@ export class GamesStatusResolver { this.gamesStatusService.getAvailableGamesStatusProgressStates(), }; } + + @UseGuards(JwtAuthGuard) + @Query(() => [LastEditedGamesStatusDTO], { + name: 'lastEditedGames', + }) + async getLastEditedGames( + @User() user: UserAuthDTO, + @Args('limit', { type: () => Int, defaultValue: 5 }) limit: number, + ) { + return this.queryBus.execute(new GetLastEditedGamesQuery(user.sub, limit)); + } } diff --git a/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts new file mode 100644 index 0000000..a1abf0c --- /dev/null +++ b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.handler.ts @@ -0,0 +1,47 @@ +import { PrismaService } from '../../../database/prisma.service'; +import { + GetLastEditedGamesQuery, + GetLastEditedGamesQueryResponse, +} from './get_last_edited_games.query'; +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; + +@QueryHandler(GetLastEditedGamesQuery) +export class GetLastEditedGamesHandler + implements + IQueryHandler +{ + constructor(private readonly prisma: PrismaService) {} + + async execute( + query: GetLastEditedGamesQuery, + ): Promise { + const { userId, limit } = query; + const games = await this.prisma.gamesStatus.findMany({ + where: { + user: { + oauthId: userId, + }, + }, + orderBy: { updatedAt: 'desc' }, + take: limit, + select: { + status: true, + id: true, + game: { + select: { + id: true, + name: true, + cover: true, + }, + }, + }, + }); + console.log(games); + return games.map((gs) => ({ + id: gs.id, + name: gs.game.name, + cover: gs.game.cover, + status: gs.status, + })); + } +} diff --git a/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts new file mode 100644 index 0000000..f29ca0a --- /dev/null +++ b/apps/api/src/modules/games_status/queries/get_last_edited_games/get_last_edited_games.query.ts @@ -0,0 +1,24 @@ +import { Query } from '@nestjs/cqrs'; + +export class GetLastEditedGamesQuery extends Query< + GetLastEditedGamesQueryResponse[] +> { + constructor( + public readonly userId: string, + public readonly limit: number = 5, + ) { + super(); + } +} + +export type GetLastEditedGamesQueryResponse = { + id: number; + name: string; + status: string; + cover: { + id: number; + bigUrl: string; + mediumUrl: string; + smallUrl: string; + } | null; +}; diff --git a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts index 90c3cf5..bbbea82 100644 --- a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts +++ b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration.controller.ts @@ -12,8 +12,8 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ParseCsvFilePipe } from './howlongtobeat_migration.pipe'; import { HowLongToBeatMigrationService } from './howlongtobeat_migration.service'; import { HowLongToBeatAccountCsvGamesSchema } from './howlongtobeat_migration.dto'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { RequestWithUser } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { RequestWithUser } from '../auth/infrastructure/graphql/auth.dto'; import { HowLongToBeatMigrationStatusService } from './howlongtobeat_migration_status/howlongtobeat_migration_status.service'; import { MigrationStatus } from '@prisma/client'; diff --git a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts index c9b5d1d..9324f26 100644 --- a/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts +++ b/apps/api/src/modules/howlongtobeat_migration/howlongtobeat_migration_status/howlongtobeat_migration_status.resolver.ts @@ -1,9 +1,9 @@ import { Query, Resolver } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../../auth/infrastructure/guards/auth-jwt.guard'; import { HowLongToBeatMigrationStatusService } from './howlongtobeat_migration_status.service'; -import { User } from '../../auth/auth.decorators'; -import { UserAuthDTO } from '../../auth/auth.dto'; +import { User } from '../../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../../auth/infrastructure/graphql/auth.dto'; import { HowLongToBeatMigrationStatusDTO } from './howlongtobeat_migration_status.dto'; @Resolver() diff --git a/apps/api/src/modules/igdb/dtos/igdb_games.dto.ts b/apps/api/src/modules/igdb/dtos/igdb_games.dto.ts deleted file mode 100644 index b7ac253..0000000 --- a/apps/api/src/modules/igdb/dtos/igdb_games.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; - -export const igdbGamesSchema = z.array( - z.object({ - id: z.number(), - cover: z.object({ id: z.number(), game: z.number(), url: z.string() }), - first_release_date: z.number().optional(), - genres: z - .array(z.object({ name: z.string(), slug: z.string(), id: z.number() })) - .optional(), - name: z.string(), - slug: z.string(), - url: z.string(), - platforms: z - .array(z.object({ name: z.string(), id: z.number(), slug: z.string() })) - .nullish(), - parent_game: z.number().nullish(), - }), -); - -export type IGDBGamesDto = z.infer; diff --git a/apps/api/src/modules/igdb/igdb.module.ts b/apps/api/src/modules/igdb/igdb.module.ts deleted file mode 100644 index aea3588..0000000 --- a/apps/api/src/modules/igdb/igdb.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { IgdbService } from './igdb.service'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { DatabaseModule } from '../database/database.module'; -import { IgdbAuthModule } from './igdb_auth/igdb_auth.module'; - -@Module({ - imports: [ - HttpModule.registerAsync({ - useFactory: async (configService: ConfigService) => ({ - baseURL: configService.get('IGDB_BASE_API_URL'), - }), - inject: [ConfigService], - }), - ConfigModule, - DatabaseModule, - IgdbAuthModule, - ], - providers: [IgdbService], - exports: [IgdbService], -}) -export class IgdbModule {} diff --git a/apps/api/src/modules/igdb/igdb.service.ts b/apps/api/src/modules/igdb/igdb.service.ts deleted file mode 100644 index cb6a3ea..0000000 --- a/apps/api/src/modules/igdb/igdb.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { firstValueFrom } from 'rxjs'; -import { ConfigService } from '@nestjs/config'; -import { igdbGamesSchema } from './dtos/igdb_games.dto'; -import { IgdbAuthService } from './igdb_auth/igdb_auth.service'; - -@Injectable() -export class IgdbService { - constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, - private readonly igdbAuthService: IgdbAuthService, - ) {} - - async getGamesBySearch(search: string) { - const token = await this.igdbAuthService.getTokenFromDatabase(); - const { data } = await firstValueFrom( - this.httpService.post( - '/games', - `search "${search}"; fields name,first_release_date,slug,cover.*,release_dates.date, genres.name, genres.slug, url, id, slug, platforms.name, platforms.slug; limit 5;`, - { - headers: { - 'Client-ID': this.configService.get('IGDB_CLIENT_ID'), - Authorization: `Bearer ${token}`, - }, - }, - ), - ); - return igdbGamesSchema.safeParse(data); - } -} diff --git a/apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts b/apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts deleted file mode 100644 index 239bfaa..0000000 --- a/apps/api/src/modules/igdb/igdb_auth/igbd_auth.axios_interceptor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { - AxiosInterceptor, - AxiosRejectedInterceptor, -} from '@narando/nest-axios-interceptor'; -import { isAxiosError } from 'axios'; -import { IgdbAuthService } from './igdb_auth.service'; - -@Injectable() -export class IgdbAxiosInterceptor extends AxiosInterceptor { - private readonly logger = new Logger(IgdbAxiosInterceptor.name); - constructor( - httpService: HttpService, - private readonly igdbAuthService: IgdbAuthService, - ) { - super(httpService); - } - - protected responseRejected(): AxiosRejectedInterceptor { - return async (error) => { - if (isAxiosError(error) && error.response?.status === 401) { - this.logger.error(error.response.data); - await this.igdbAuthService.getTokenFromOAuth(); - } - }; - } -} diff --git a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts b/apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts deleted file mode 100644 index a0a06b0..0000000 --- a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { DatabaseModule } from '../../database/database.module'; -import { IgdbAuthService } from './igdb_auth.service'; -import { IgdbAxiosInterceptor } from './igbd_auth.axios_interceptor'; - -@Module({ - imports: [ - HttpModule.registerAsync({ - useFactory: async (configService: ConfigService) => ({ - baseURL: configService.get('IGDB_AUTH0_BASE_URL'), - }), - inject: [ConfigService], - }), - ConfigModule, - DatabaseModule, - ], - providers: [IgdbAxiosInterceptor, IgdbAuthService], - exports: [IgdbAuthService], -}) -export class IgdbAuthModule {} diff --git a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts b/apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts deleted file mode 100644 index b464bd3..0000000 --- a/apps/api/src/modules/igdb/igdb_auth/igdb_auth.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { ConfigService } from '@nestjs/config'; -import { PrismaService } from '../../database/prisma.service'; -import { OAuthTokenDto } from './dtos/igdb.dto'; -import { firstValueFrom } from 'rxjs'; -import { AxiosResponse } from 'axios'; - -@Injectable() -export class IgdbAuthService { - constructor( - private readonly httpService: HttpService, - private readonly configService: ConfigService, - private readonly prismaService: PrismaService, - ) {} - - async getTokenFromOAuth(): Promise { - const { data } = await firstValueFrom>( - this.httpService.post( - '/token', - { - client_id: this.configService.get('IGDB_CLIENT_ID'), - client_secret: this.configService.get('IGDB_CLIENT_SECRET'), - grant_type: 'client_credentials', - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ), - ); - if (data.access_token) { - const token = await this.getTokenFromDatabase(); - if (token) { - await this.updateTokenInDatabase(data.access_token); - } else { - await this.saveTokenToDatabase(data.access_token); - } - } - return data; - } - - async updateTokenInDatabase( - token: OAuthTokenDto['access_token'], - ): Promise { - await this.prismaService.iGBDBAuth.update({ - where: { - id: 1, - }, - data: { - token, - }, - }); - } - - async getTokenFromDatabase(): Promise { - const data = await this.prismaService.iGBDBAuth.findFirst(); - if (!data) { - return null; - } - return data.token; - } - - async saveTokenToDatabase( - token: OAuthTokenDto['access_token'], - ): Promise { - await this.prismaService.iGBDBAuth.create({ - data: { - token, - }, - }); - } -} diff --git a/apps/api/src/modules/igdb/models/igdb_game.model.ts b/apps/api/src/modules/igdb/models/igdb_game.model.ts deleted file mode 100644 index f414a40..0000000 --- a/apps/api/src/modules/igdb/models/igdb_game.model.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; - -@ObjectType() -class Cover { - @Field(() => ID) - id: string; - - @Field() - url: string; -} - -@ObjectType() -class Genre { - @Field(() => ID) - id: string; - - @Field() - name: string; - - @Field() - slug: string; - - @Field() - igdbId: number; -} - -@ObjectType() -class Platform { - @Field(() => ID) - id: string; - - @Field() - name: string; - - @Field() - igdbId: number; - - @Field() - slug: string; -} - -@ObjectType() -export class IgdbGame { - @Field(() => ID) - id: string; - - @Field() - name: string; - - @Field(() => Cover) - cover: Cover; - - @Field({ nullable: true }) - first_release_date: number; - - @Field(() => [Genre], { nullable: true }) - genres: Genre[]; - - @Field(() => [Platform], { nullable: true }) - platforms: Platform[]; - - @Field() - slug: string; - - @Field() - url: string; -} diff --git a/apps/api/src/modules/images/images.controller.ts b/apps/api/src/modules/images/images.controller.ts index a6fa31c..3e35647 100644 --- a/apps/api/src/modules/images/images.controller.ts +++ b/apps/api/src/modules/images/images.controller.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import { ImagesService } from './images.service'; import { FileInterceptor } from '@nestjs/platform-express'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { ZodValidationPipe } from '../validation/validation.pipe'; import { ImageUploadDTO, diff --git a/apps/api/src/modules/platforms/platforms.resolver.ts b/apps/api/src/modules/platforms/platforms.resolver.ts index 4254578..fa803e9 100644 --- a/apps/api/src/modules/platforms/platforms.resolver.ts +++ b/apps/api/src/modules/platforms/platforms.resolver.ts @@ -1,9 +1,9 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; import { PlatformsService } from './platforms.service'; import { PlatformsDTO, UpdatePlatformDisplayNameDTO } from './platforms.dto'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Resolver() export class PlatformsResolver { diff --git a/apps/api/src/modules/profiles/profiles.resolver.ts b/apps/api/src/modules/profiles/profiles.resolver.ts index bfb312d..8f1cfa7 100644 --- a/apps/api/src/modules/profiles/profiles.resolver.ts +++ b/apps/api/src/modules/profiles/profiles.resolver.ts @@ -5,9 +5,9 @@ import { ProfileInfoUpdateResponseDTO, } from './profiles.dto'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { User } from '../auth/auth.decorators'; -import { UserAuthDTO } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; import { ProfilesRepository } from './profiles.repository'; @Resolver() diff --git a/apps/api/src/modules/roles/roles.resolver.ts b/apps/api/src/modules/roles/roles.resolver.ts index 9c87d7c..bee0c08 100644 --- a/apps/api/src/modules/roles/roles.resolver.ts +++ b/apps/api/src/modules/roles/roles.resolver.ts @@ -2,8 +2,8 @@ import { Injectable, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; import { RoleDTO, UpdateUserRoleDTO, UpdateUserRoleInput } from './roles.dto'; import { RolesService } from './roles.service'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Injectable() export class RolesResolver { diff --git a/apps/api/src/modules/search/search.module.ts b/apps/api/src/modules/search/search.module.ts index e3908b4..941b1dd 100644 --- a/apps/api/src/modules/search/search.module.ts +++ b/apps/api/src/modules/search/search.module.ts @@ -1,5 +1,4 @@ import { forwardRef, Module } from '@nestjs/common'; -import { IgdbModule } from '../igdb/igdb.module'; import { DatabaseModule } from '../database/database.module'; import { SearchResolver } from './search.resolver'; import { SearchService } from './search.service'; @@ -15,7 +14,6 @@ const handlers = [FetchGamesFromHltbQueryHandler]; imports: [ CqrsModule, ConfigModule, - forwardRef(() => IgdbModule), forwardRef(() => DatabaseModule), GamesModule, forwardRef(() => HowLongToBeatParserModule), diff --git a/apps/api/src/modules/user_stats/user_stats.resolver.ts b/apps/api/src/modules/user_stats/user_stats.resolver.ts index 9dd204e..1097af5 100644 --- a/apps/api/src/modules/user_stats/user_stats.resolver.ts +++ b/apps/api/src/modules/user_stats/user_stats.resolver.ts @@ -2,9 +2,9 @@ import { Resolver, Query, Args } from '@nestjs/graphql'; import { UserStatsService } from './user_stats.service'; import { GetUserStatsArgs, UserStatsDTO } from './user_stats.dto'; import { UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { User } from '../auth/auth.decorators'; -import { UserAuthDTO } from '../auth/auth.dto'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { User } from '../auth/infrastructure/decorators/auth.decorators'; +import { UserAuthDTO } from '../auth/infrastructure/graphql/auth.dto'; @Resolver() export class UserStatsResolver { diff --git a/apps/api/src/modules/users/users.resolver.ts b/apps/api/src/modules/users/users.resolver.ts index 6f819ff..173ee43 100644 --- a/apps/api/src/modules/users/users.resolver.ts +++ b/apps/api/src/modules/users/users.resolver.ts @@ -2,8 +2,8 @@ import { Injectable, NotFoundException, UseGuards } from '@nestjs/common'; import { UserDataDTO, UserDTO } from './users.dto'; import { Args, Query } from '@nestjs/graphql'; import { UsersService } from './users.service'; -import { JwtAuthGuard } from '../auth/guards/auth-jwt.guard'; -import { AdminUserGuard } from '../auth/guards/admin-user.guard'; +import { JwtAuthGuard } from '../auth/infrastructure/guards/auth-jwt.guard'; +import { AdminUserGuard } from '../auth/infrastructure/guards/admin-user.guard'; @Injectable() export class UsersResolver { diff --git a/apps/api/src/schema.gql b/apps/api/src/schema.gql index dd31311..a298c81 100644 --- a/apps/api/src/schema.gql +++ b/apps/api/src/schema.gql @@ -52,6 +52,20 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type ExternalGameDTO { + backgroundUrl: String + coverUrl: String + id: String! + name: String! + platforms: [ExternalGamePlatformDTO!]! + releaseDate: DateTime! +} + +type ExternalGamePlatformDTO { + id: String! + name: String! +} + input FiltersGameStatus { achievementsCompleted: String! platform: String! @@ -189,6 +203,14 @@ type HowLongToBeatMigrationStatusDTO { status: MigrationStatus } +"""User last edited games statuses""" +type LastEditedGamesStatusDTO { + cover: CoverDTO + id: Float! + name: String! + status: GameStatus! +} + """The status of the migration of a user's HowLongToBeat account""" enum MigrationStatus { FAILED @@ -297,6 +319,7 @@ type Query { gamesStatusSortOptions: SortOptionsDTO! getAllUserGamesStatusByOauthId(oauthId: String!): [UserGamesStatusResponseDTO!]! getProfileCollections: [CollectionDTO!]! + lastEditedGames(limit: Int! = 5): [LastEditedGamesStatusDTO!]! migrationStatus: HowLongToBeatMigrationStatusDTO! """Get user and friends game status reviews""" @@ -307,6 +330,7 @@ type Query { profileInfo: ProfileInfoDTO! roles: [RoleDTO!]! search(input: String!): SearchResult! + upcomingGames(limit: Float!): [ExternalGameDTO!]! user(oauthId: String!): UserDataDTO! """Query to get user"s friend games statuses""" diff --git a/apps/native/.gitignore b/apps/native/.gitignore index 12bdb34..f12a21f 100644 --- a/apps/native/.gitignore +++ b/apps/native/.gitignore @@ -27,4 +27,9 @@ __generated__/* .env.development .env.test .env.production -eas.json \ No newline at end of file +eas.json +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli \ No newline at end of file diff --git a/apps/native/.nvmrc b/apps/native/.nvmrc index adb5558..cf2efde 100644 --- a/apps/native/.nvmrc +++ b/apps/native/.nvmrc @@ -1 +1 @@ -22.14.0 \ No newline at end of file +24.13.0 \ No newline at end of file diff --git a/apps/native/app.json b/apps/native/app.json index 9550810..656d736 100644 --- a/apps/native/app.json +++ b/apps/native/app.json @@ -8,20 +8,19 @@ "icon": "./assets/images/icon.png", "owner": "game-critique", "userInterfaceStyle": "light", + "newArchEnabled": true, "experiments": { - "tsconfigPaths": true + "tsconfigPaths": true, + "typedRoutes": true }, "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" + "image": "./assets/splash-2.png", + "resizeMode": "contain" }, "updates": { "fallbackToCacheTimeout": 0 }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "com.gamecritique.mobile" @@ -32,11 +31,27 @@ "backgroundColor": "#FFFFFF" }, "package": "com.gamecritique.mobile", - "permissions": [ - "INTERNET" - ] + "permissions": ["INTERNET"] }, "plugins": [ + [ + "expo-build-properties", + { + "android": { + "newArchEnabled": true + }, + "ios": { + "newArchEnabled": true + } + } + ], + [ + "expo-dev-client", + { + "launchMode": "most-recent", + "addGeneratedScheme": false + } + ], "expo-router", [ "react-native-auth0", @@ -51,7 +66,8 @@ } ], "expo-font", - "expo-secure-store" + "expo-secure-store", + "expo-asset" ], "extra": { "router": { diff --git a/apps/native/app/(app)/(authorized)/_layout.tsx b/apps/native/app/(app)/(authorized)/_layout.tsx new file mode 100644 index 0000000..0f43939 --- /dev/null +++ b/apps/native/app/(app)/(authorized)/_layout.tsx @@ -0,0 +1,24 @@ +import { Redirect, Stack } from "expo-router"; +import { useAuth0 } from "react-native-auth0"; + +import { SkeletonText } from "@/ui/feedback/skeleton/skeleton"; +import { Box } from "@/ui/layout/box/box"; + +const AppAuthorizedLayout = () => { + const { user, isLoading } = useAuth0(); + + if (isLoading) { + return ( + + + + ); + } + + if (!user) { + return ; + } + return ; +}; + +export default AppAuthorizedLayout; diff --git a/apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx b/apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx new file mode 100644 index 0000000..d78e1b1 --- /dev/null +++ b/apps/native/app/(app)/(authorized)/games_status/games_status_info/[games_status_id].tsx @@ -0,0 +1,37 @@ +import { Stack, useLocalSearchParams } from "expo-router"; + +import { BaseScreenLayout } from "@/modules/layouts/base_screen_layout/base_screen_layout"; +import { GoBackHeader } from "@/modules/layouts/go_back_header/go_back_header"; +import { useUserGameStatus } from "@/modules/screens/user_game_status/use_user_game_status/use_user_game_status"; +import { UserGameStatusScreen } from "@/modules/screens/user_game_status/user_game_status_screen"; + +const GamesStatusInfo = () => { + const { games_status_id, oauth_id } = useLocalSearchParams<{ + games_status_id: string; + oauth_id: string; + }>(); + const userGameStatusQuery = useUserGameStatus({ + gameStatusId: games_status_id, + oauthId: oauth_id, + }); + return ( + + ( + + ), + }} + /> + + + ); +}; + +export default GamesStatusInfo; diff --git a/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx b/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx index e83132c..e82a475 100644 --- a/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx +++ b/apps/native/app/(app)/(tabs)/(authorized)/games/_layout.tsx @@ -34,12 +34,7 @@ const GamesLayout = () => { header: () => , }} /> - , - }} - /> + { - return ( - - - - ); -}; - -export default GamesStatusInfo; diff --git a/apps/native/app/(app)/(tabs)/home.tsx b/apps/native/app/(app)/(tabs)/home.tsx index dc0145f..7c8d272 100644 --- a/apps/native/app/(app)/(tabs)/home.tsx +++ b/apps/native/app/(app)/(tabs)/home.tsx @@ -1,5 +1,5 @@ -import { BaseScreenLayout } from "../../../modules/layouts/base_screen_layout/base_screen_layout"; -import { HomeScreen } from "../../../modules/screens/homepage/home_screen"; +import { BaseScreenLayout } from "@/modules/layouts/base_screen_layout/base_screen_layout"; +import { HomeScreen } from "@/modules/screens/homepage/home_screen"; export default function Page() { return ( diff --git a/apps/native/app/_layout.tsx b/apps/native/app/_layout.tsx index c948d6e..512d144 100644 --- a/apps/native/app/_layout.tsx +++ b/apps/native/app/_layout.tsx @@ -1,19 +1,24 @@ -import { ToastProvider } from "@tamagui/toast"; import { useFonts } from "expo-font"; import { SplashScreen, Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { useCallback } from "react"; import { LogBox } from "react-native"; import { Auth0Provider } from "react-native-auth0"; -import { SafeAreaProvider } from "react-native-safe-area-context"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { + initialWindowMetrics, + SafeAreaProvider, +} from "react-native-safe-area-context"; import { TamaguiProvider } from "tamagui"; -import { Toast } from "ui/feedback/toast/toast"; -import { ApolloProvider } from "../modules/graphql/apollo_provider"; -import { Header } from "../modules/layouts/header/header"; -import { SafeToastViewport } from "../modules/layouts/safe_toast_viewport/safe_toast_viewport"; import tamaguiConfig from "../tamagui.config"; +import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider"; +import { ApolloProvider } from "@/modules/graphql/apollo_provider"; +import { Header } from "@/modules/layouts/header/header"; + +import "@/global.css"; + SplashScreen.preventAutoHideAsync(); const AUTH0_DOMAIN = process.env.EXPO_PUBLIC_AUTH0_DOMAIN; @@ -36,33 +41,45 @@ const RootLayout = () => { return null; } return ( - - - + + - - - - - - - - - - - + + + + + + + + + + + + + - - + + ); }; diff --git a/apps/native/assets/splash-2.png b/apps/native/assets/splash-2.png new file mode 100644 index 0000000..e30a04b Binary files /dev/null and b/apps/native/assets/splash-2.png differ diff --git a/apps/native/assets/splash.png b/apps/native/assets/splash.png deleted file mode 100644 index 0e89705..0000000 Binary files a/apps/native/assets/splash.png and /dev/null differ diff --git a/apps/native/babel.config.js b/apps/native/babel.config.js index 3124469..5ba6600 100644 --- a/apps/native/babel.config.js +++ b/apps/native/babel.config.js @@ -1,19 +1,26 @@ module.exports = function (api) { api.cache(true); + return { - presets: ["babel-preset-expo"], + presets: [ + ["babel-preset-expo", { jsxImportSource: "nativewind" }], + "nativewind/babel", + ], + plugins: [ - "expo-router/babel", - "react-native-reanimated/plugin", [ "module-resolver", { + root: ["./"], alias: { ui: "./ui", + "@": "./", + "tailwind.config": "./tailwind.config.js", }, extensions: [".js", ".jsx", ".ts", ".tsx"], }, ], + "react-native-worklets/plugin", ], }; }; diff --git a/apps/native/components/ui/gluestack-ui-provider/config.ts b/apps/native/components/ui/gluestack-ui-provider/config.ts new file mode 100644 index 0000000..32842d9 --- /dev/null +++ b/apps/native/components/ui/gluestack-ui-provider/config.ts @@ -0,0 +1,309 @@ +"use client"; +import { vars } from "nativewind"; + +export const config = { + light: vars({ + "--color-primary-0": "179 179 179", + "--color-primary-50": "153 153 153", + "--color-primary-100": "128 128 128", + "--color-primary-200": "115 115 115", + "--color-primary-300": "102 102 102", + "--color-primary-400": "82 82 82", + "--color-primary-500": "51 51 51", + "--color-primary-600": "41 41 41", + "--color-primary-700": "31 31 31", + "--color-primary-800": "13 13 13", + "--color-primary-900": "10 10 10", + "--color-primary-950": "8 8 8", + + /* Secondary */ + "--color-secondary-0": "253 253 253", + "--color-secondary-50": "251 251 251", + "--color-secondary-100": "246 246 246", + "--color-secondary-200": "242 242 242", + "--color-secondary-300": "237 237 237", + "--color-secondary-400": "230 230 231", + "--color-secondary-500": "217 217 219", + "--color-secondary-600": "198 199 199", + "--color-secondary-700": "189 189 189", + "--color-secondary-800": "177 177 177", + "--color-secondary-900": "165 164 164", + "--color-secondary-950": "157 157 157", + + /* Tertiary */ + "--color-tertiary-0": "255 250 245", + "--color-tertiary-50": "255 242 229", + "--color-tertiary-100": "255 233 213", + "--color-tertiary-200": "254 209 170", + "--color-tertiary-300": "253 180 116", + "--color-tertiary-400": "251 157 75", + "--color-tertiary-500": "231 129 40", + "--color-tertiary-600": "215 117 31", + "--color-tertiary-700": "180 98 26", + "--color-tertiary-800": "130 73 23", + "--color-tertiary-900": "108 61 19", + "--color-tertiary-950": "84 49 18", + + /* Error */ + "--color-error-0": "254 233 233", + "--color-error-50": "254 226 226", + "--color-error-100": "254 202 202", + "--color-error-200": "252 165 165", + "--color-error-300": "248 113 113", + "--color-error-400": "239 68 68", + "--color-error-500": "230 53 53", + "--color-error-600": "220 38 38", + "--color-error-700": "185 28 28", + "--color-error-800": "153 27 27", + "--color-error-900": "127 29 29", + "--color-error-950": "83 19 19", + + /* Success */ + "--color-success-0": "228 255 244", + "--color-success-50": "202 255 232", + "--color-success-100": "162 241 192", + "--color-success-200": "132 211 162", + "--color-success-300": "102 181 132", + "--color-success-400": "72 151 102", + "--color-success-500": "52 131 82", + "--color-success-600": "42 121 72", + "--color-success-700": "32 111 62", + "--color-success-800": "22 101 52", + "--color-success-900": "20 83 45", + "--color-success-950": "27 50 36", + + /* Warning */ + "--color-warning-0": "255 249 245", + "--color-warning-50": "255 244 236", + "--color-warning-100": "255 231 213", + "--color-warning-200": "254 205 170", + "--color-warning-300": "253 173 116", + "--color-warning-400": "251 149 75", + "--color-warning-500": "231 120 40", + "--color-warning-600": "215 108 31", + "--color-warning-700": "180 90 26", + "--color-warning-800": "130 68 23", + "--color-warning-900": "108 56 19", + "--color-warning-950": "84 45 18", + + /* Info */ + "--color-info-0": "236 248 254", + "--color-info-50": "199 235 252", + "--color-info-100": "162 221 250", + "--color-info-200": "124 207 248", + "--color-info-300": "87 194 246", + "--color-info-400": "50 180 244", + "--color-info-500": "13 166 242", + "--color-info-600": "11 141 205", + "--color-info-700": "9 115 168", + "--color-info-800": "7 90 131", + "--color-info-900": "5 64 93", + "--color-info-950": "3 38 56", + + /* Typography */ + "--color-typography-0": "254 254 255", + "--color-typography-50": "245 245 245", + "--color-typography-100": "229 229 229", + "--color-typography-200": "219 219 220", + "--color-typography-300": "212 212 212", + "--color-typography-400": "163 163 163", + "--color-typography-500": "140 140 140", + "--color-typography-600": "115 115 115", + "--color-typography-700": "82 82 82", + "--color-typography-800": "64 64 64", + "--color-typography-900": "38 38 39", + "--color-typography-950": "23 23 23", + + /* Outline */ + "--color-outline-0": "253 254 254", + "--color-outline-50": "243 243 243", + "--color-outline-100": "230 230 230", + "--color-outline-200": "221 220 219", + "--color-outline-300": "211 211 211", + "--color-outline-400": "165 163 163", + "--color-outline-500": "140 141 141", + "--color-outline-600": "115 116 116", + "--color-outline-700": "83 82 82", + "--color-outline-800": "65 65 65", + "--color-outline-900": "39 38 36", + "--color-outline-950": "26 23 23", + + /* Background */ + "--color-background-0": "255 255 255", + "--color-background-50": "246 246 246", + "--color-background-100": "242 241 241", + "--color-background-200": "220 219 219", + "--color-background-300": "213 212 212", + "--color-background-400": "162 163 163", + "--color-background-500": "142 142 142", + "--color-background-600": "116 116 116", + "--color-background-700": "83 82 82", + "--color-background-800": "65 64 64", + "--color-background-900": "39 38 37", + "--color-background-950": "18 18 18", + + /* Background Special */ + "--color-background-error": "254 241 241", + "--color-background-warning": "255 243 234", + "--color-background-success": "237 252 242", + "--color-background-muted": "247 248 247", + "--color-background-info": "235 248 254", + + /* Focus Ring Indicator */ + "--color-indicator-primary": "55 55 55", + "--color-indicator-info": "83 153 236", + "--color-indicator-error": "185 28 28", + }), + dark: vars({ + "--color-primary-0": "166 166 166", + "--color-primary-50": "175 175 175", + "--color-primary-100": "186 186 186", + "--color-primary-200": "197 197 197", + "--color-primary-300": "212 212 212", + "--color-primary-400": "221 221 221", + "--color-primary-500": "230 230 230", + "--color-primary-600": "240 240 240", + "--color-primary-700": "250 250 250", + "--color-primary-800": "253 253 253", + "--color-primary-900": "254 249 249", + "--color-primary-950": "253 252 252", + + /* Secondary */ + "--color-secondary-0": "20 20 20", + "--color-secondary-50": "23 23 23", + "--color-secondary-100": "31 31 31", + "--color-secondary-200": "39 39 39", + "--color-secondary-300": "44 44 44", + "--color-secondary-400": "56 57 57", + "--color-secondary-500": "63 64 64", + "--color-secondary-600": "86 86 86", + "--color-secondary-700": "110 110 110", + "--color-secondary-800": "135 135 135", + "--color-secondary-900": "150 150 150", + "--color-secondary-950": "164 164 164", + + /* Tertiary */ + "--color-tertiary-0": "84 49 18", + "--color-tertiary-50": "108 61 19", + "--color-tertiary-100": "130 73 23", + "--color-tertiary-200": "180 98 26", + "--color-tertiary-300": "215 117 31", + "--color-tertiary-400": "231 129 40", + "--color-tertiary-500": "251 157 75", + "--color-tertiary-600": "253 180 116", + "--color-tertiary-700": "254 209 170", + "--color-tertiary-800": "255 233 213", + "--color-tertiary-900": "255 242 229", + "--color-tertiary-950": "255 250 245", + + /* Error */ + "--color-error-0": "83 19 19", + "--color-error-50": "127 29 29", + "--color-error-100": "153 27 27", + "--color-error-200": "185 28 28", + "--color-error-300": "220 38 38", + "--color-error-400": "230 53 53", + "--color-error-500": "239 68 68", + "--color-error-600": "249 97 96", + "--color-error-700": "229 91 90", + "--color-error-800": "254 202 202", + "--color-error-900": "254 226 226", + "--color-error-950": "254 233 233", + + /* Success */ + "--color-success-0": "27 50 36", + "--color-success-50": "20 83 45", + "--color-success-100": "22 101 52", + "--color-success-200": "32 111 62", + "--color-success-300": "42 121 72", + "--color-success-400": "52 131 82", + "--color-success-500": "72 151 102", + "--color-success-600": "102 181 132", + "--color-success-700": "132 211 162", + "--color-success-800": "162 241 192", + "--color-success-900": "202 255 232", + "--color-success-950": "228 255 244", + + /* Warning */ + "--color-warning-0": "84 45 18", + "--color-warning-50": "108 56 19", + "--color-warning-100": "130 68 23", + "--color-warning-200": "180 90 26", + "--color-warning-300": "215 108 31", + "--color-warning-400": "231 120 40", + "--color-warning-500": "251 149 75", + "--color-warning-600": "253 173 116", + "--color-warning-700": "254 205 170", + "--color-warning-800": "255 231 213", + "--color-warning-900": "255 244 237", + "--color-warning-950": "255 249 245", + + /* Info */ + "--color-info-0": "3 38 56", + "--color-info-50": "5 64 93", + "--color-info-100": "7 90 131", + "--color-info-200": "9 115 168", + "--color-info-300": "11 141 205", + "--color-info-400": "13 166 242", + "--color-info-500": "50 180 244", + "--color-info-600": "87 194 246", + "--color-info-700": "124 207 248", + "--color-info-800": "162 221 250", + "--color-info-900": "199 235 252", + "--color-info-950": "236 248 254", + + /* Typography */ + "--color-typography-0": "23 23 23", + "--color-typography-50": "38 38 39", + "--color-typography-100": "64 64 64", + "--color-typography-200": "82 82 82", + "--color-typography-300": "115 115 115", + "--color-typography-400": "140 140 140", + "--color-typography-500": "163 163 163", + "--color-typography-600": "212 212 212", + "--color-typography-700": "219 219 220", + "--color-typography-800": "229 229 229", + "--color-typography-900": "245 245 245", + "--color-typography-950": "254 254 255", + + /* Outline */ + "--color-outline-0": "26 23 23", + "--color-outline-50": "39 38 36", + "--color-outline-100": "65 65 65", + "--color-outline-200": "83 82 82", + "--color-outline-300": "115 116 116", + "--color-outline-400": "140 141 141", + "--color-outline-500": "165 163 163", + "--color-outline-600": "211 211 211", + "--color-outline-700": "221 220 219", + "--color-outline-800": "230 230 230", + "--color-outline-900": "243 243 243", + "--color-outline-950": "253 254 254", + + /* Background */ + "--color-background-0": "18 18 18", + "--color-background-50": "39 38 37", + "--color-background-100": "65 64 64", + "--color-background-200": "83 82 82", + "--color-background-300": "116 116 116", + "--color-background-400": "142 142 142", + "--color-background-500": "162 163 163", + "--color-background-600": "213 212 212", + "--color-background-700": "229 228 228", + "--color-background-800": "242 241 241", + "--color-background-900": "246 246 246", + "--color-background-950": "255 255 255", + + /* Background Special */ + "--color-background-error": "66 43 43", + "--color-background-warning": "65 47 35", + "--color-background-success": "28 43 33", + "--color-background-muted": "51 51 51", + "--color-background-info": "26 40 46", + + /* Focus Ring Indicator */ + "--color-indicator-primary": "247 247 247", + "--color-indicator-info": "161 199 245", + "--color-indicator-error": "232 70 69", + }), +}; diff --git a/apps/native/components/ui/gluestack-ui-provider/index.tsx b/apps/native/components/ui/gluestack-ui-provider/index.tsx new file mode 100644 index 0000000..311fefb --- /dev/null +++ b/apps/native/components/ui/gluestack-ui-provider/index.tsx @@ -0,0 +1,39 @@ +import { OverlayProvider } from "@gluestack-ui/core/overlay/creator"; +import { ToastProvider } from "@gluestack-ui/core/toast/creator"; +import { useColorScheme } from "nativewind"; +import React, { useEffect } from "react"; +import { View, ViewProps } from "react-native"; + +import { config } from "./config"; + +export type ModeType = "light" | "dark" | "system"; + +export function GluestackUIProvider({ + mode = "light", + ...props +}: { + mode?: ModeType; + children?: React.ReactNode; + style?: ViewProps["style"]; +}) { + const { colorScheme, setColorScheme } = useColorScheme(); + + useEffect(() => { + setColorScheme(mode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode]); + + return ( + + + {props.children} + + + ); +} diff --git a/apps/native/global.css b/apps/native/global.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/native/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/native/metro.config.js b/apps/native/metro.config.js index a980779..1e21e03 100644 --- a/apps/native/metro.config.js +++ b/apps/native/metro.config.js @@ -1,22 +1,10 @@ // Learn more https://docs.expo.io/guides/customizing-metro const { getDefaultConfig } = require("expo/metro-config"); -const path = require("path"); - -module.exports = (() => { - // Find the workspace root, this can be replaced with `find-yarn-workspace-root` - const workspaceRoot = path.resolve(__dirname, "../../"); - const projectRoot = __dirname; - - const config = getDefaultConfig(__dirname); - // 1. Watch all files within the monorepo - config.watchFolders = [workspaceRoot]; - // 2. Let Metro know where to resolve packages, and in what order - config.resolver.nodeModulesPaths = [ - path.resolve(projectRoot, "node_modules"), - path.resolve(workspaceRoot, "node_modules"), - ]; - // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` - // config.resolver.disableHierarchicalLookup = true; +const { withNativeWind } = require("nativewind/metro"); + +const getModifiedConfig = () => { + const config = getDefaultConfig(__dirname, { isCSSEnabled: true }); + config.resolver.sourceExts.push("mjs"); const { transformer, resolver } = config; @@ -31,5 +19,20 @@ module.exports = (() => { sourceExts: [...resolver.sourceExts, "svg"], }; + config.server.enhanceMiddleware = (middleware) => { + return (req, res, next) => { + res.setHeader("Cross-Origin-Embedder-Policy", "credentialless"); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + middleware(req, res, next); + }; + }; + return config; -})(); +}; + +const modifiedConfig = getModifiedConfig(); + +module.exports = withNativeWind(modifiedConfig, { + input: "./global.css", + configPath: "./tailwind.config.js", +}); diff --git a/apps/native/modules/dates/date_to_polish_locale.ts b/apps/native/modules/dates/date_to_polish_locale.ts new file mode 100644 index 0000000..6c6a546 --- /dev/null +++ b/apps/native/modules/dates/date_to_polish_locale.ts @@ -0,0 +1,8 @@ +export const formatReleaseDateToPolishLocale = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString("pl-PL", { + day: "numeric", + month: "long", + year: "numeric", + }); +}; diff --git a/apps/native/modules/games_status/games_status_form/games_status_form.tsx b/apps/native/modules/games_status/games_status_form/games_status_form.tsx index 09a111b..4a9664f 100644 --- a/apps/native/modules/games_status/games_status_form/games_status_form.tsx +++ b/apps/native/modules/games_status/games_status_form/games_status_form.tsx @@ -1,6 +1,6 @@ import { ErrorMessage } from "@hookform/error-message"; import { Controller, FormProvider } from "react-hook-form"; -import { Button, Form, Separator, View, XStack, YStack } from "tamagui"; +import { Button, Form, Separator, View } from "tamagui"; import { Checkbox } from "ui/forms/checkbox"; import { Input } from "ui/forms/input"; import { Select } from "ui/forms/select"; @@ -16,6 +16,9 @@ import { } from "./use_games_status_form"; import { GameInfoQuery } from "../../screens/game/use_get_game_info/game_info.generated"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GamesStatusFormProps = { initialValues?: InitialValues; gameStatusId?: number; @@ -38,7 +41,7 @@ export const GamesStatusForm = ({
- + Status* @@ -73,13 +76,15 @@ export const GamesStatusForm = ({ name="status" control={control} /> - + Czas gry - + - + - + Platforma* @@ -181,9 +186,9 @@ export const GamesStatusForm = ({ name="platform" control={control} /> - + - + Osiągnięcia @@ -210,13 +215,19 @@ export const GamesStatusForm = ({ Ta gra lub platforma nie posiada osiągnięć )} - + - + Ocena - + - + - + + {truncateString(text, 20)} - - + + ); }; diff --git a/apps/native/modules/layouts/header/header.tsx b/apps/native/modules/layouts/header/header.tsx index fa31ad6..92ffb63 100644 --- a/apps/native/modules/layouts/header/header.tsx +++ b/apps/native/modules/layouts/header/header.tsx @@ -1,39 +1,15 @@ -import { Link } from "expo-router"; import { View } from "react-native"; -import { useAuth0 } from "react-native-auth0"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { XStack } from "tamagui"; -import { SearchButton } from "./search/search_button"; -import { UserProfileButton } from "./user_profile_button"; -import AppLogo from "../../../assets/logo/logo_app.svg"; -import { Text } from "../../../ui/typography/text"; +import { HeaderContent } from "@/modules/layouts/header/header_content"; export const Header = () => { const insets = useSafeAreaInsets(); - const { user } = useAuth0(); return ( <> - - - {!user && } - - - - - - - - - - + + ); }; diff --git a/apps/native/modules/layouts/header/header_content.tsx b/apps/native/modules/layouts/header/header_content.tsx new file mode 100644 index 0000000..8f3a590 --- /dev/null +++ b/apps/native/modules/layouts/header/header_content.tsx @@ -0,0 +1,40 @@ +import { useAuth0 } from "react-native-auth0"; + +import { NotificationIcon } from "@/modules/layouts/header/notifications/notification_icon"; +import { useUserProfileInfo } from "@/modules/screens/profile/use_user_profile_info/use_user_profile_info"; +import { truncateString } from "@/modules/strings/truncate_string"; +import { Box } from "@/ui/layout/box/box"; +import { + Avatar, + AvatarFallbackText, + AvatarImage, +} from "@/ui/media_and_icons/avatar/avatar"; +import { GText } from "@/ui/typography/text"; + +export const HeaderContent = () => { + const { user } = useAuth0(); + const userProfileInfo = useUserProfileInfo(); + + return ( + + {user && ( + <> + + Siema, {truncateString(user.name ?? "", 15)} + + + + + Jane Doe + + + + + )} + + ); +}; diff --git a/apps/native/modules/layouts/header/notifications/notification_icon.tsx b/apps/native/modules/layouts/header/notifications/notification_icon.tsx new file mode 100644 index 0000000..cba45ed --- /dev/null +++ b/apps/native/modules/layouts/header/notifications/notification_icon.tsx @@ -0,0 +1,10 @@ +import { Bell } from "lucide-react-native"; + +import { Button, ButtonIcon } from "@/ui/forms/button/button"; +export const NotificationIcon = () => { + return ( + + ); +}; diff --git a/apps/native/modules/layouts/header/search/search_button.tsx b/apps/native/modules/layouts/header/search/search_button.tsx deleted file mode 100644 index a7db1fe..0000000 --- a/apps/native/modules/layouts/header/search/search_button.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Search } from "@tamagui/lucide-icons"; -import { router } from "expo-router"; - -import { ButtonWithIcon } from "../../../../ui/forms/button_icon"; - -export const SearchButton = () => { - return ( - { - router.push("/search/search"); - }} - icon={} - backgrounded={false} - backgroundColor="transparent" - padding={4} - /> - ); -}; diff --git a/apps/native/modules/layouts/header/user_profile_button.tsx b/apps/native/modules/layouts/header/user_profile_button.tsx index 3695691..3e8d34f 100644 --- a/apps/native/modules/layouts/header/user_profile_button.tsx +++ b/apps/native/modules/layouts/header/user_profile_button.tsx @@ -1,15 +1,14 @@ import { LogIn } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { ButtonWithIcon } from "../../../ui/forms/button_icon"; +import { Button, ButtonIcon } from "@/ui/forms/button/button"; export const UserProfileButton = () => { return ( - router.push("auth")} - icon={} - backgroundColor="transparent" - padding={4} - /> + ); }; diff --git a/apps/native/modules/screens/auth/auth_screen.tsx b/apps/native/modules/screens/auth/auth_screen.tsx index 9b742a8..52d7f57 100644 --- a/apps/native/modules/screens/auth/auth_screen.tsx +++ b/apps/native/modules/screens/auth/auth_screen.tsx @@ -1,20 +1,17 @@ -import { Spinner, YStack } from "tamagui"; +import { Spinner } from "tamagui"; import { GoogleLogo } from "./assets/google_logo"; import { useAuth } from "./use_auth"; -import { AppLogo } from "../../../assets/logo/app_logo"; -import { ButtonWithIcon } from "../../../ui/forms/button_icon"; + +import { AppLogo } from "@/assets/logo/app_logo"; +import { ButtonWithIcon } from "@/ui/forms/button_icon"; +import { VStack } from "@/ui/layout/vstack/vstack"; export const AuthScreen = () => { const { onLogin, isLoading } = useAuth(); return ( - + {isLoading ? ( @@ -27,6 +24,6 @@ export const AuthScreen = () => { Zaloguj się )} - + ); }; diff --git a/apps/native/modules/screens/auth/use_auth.ts b/apps/native/modules/screens/auth/use_auth.ts index 91a1f7b..10243ac 100644 --- a/apps/native/modules/screens/auth/use_auth.ts +++ b/apps/native/modules/screens/auth/use_auth.ts @@ -1,7 +1,7 @@ -import { useToastController } from "@tamagui/toast"; import { router } from "expo-router"; import * as SecureStore from "expo-secure-store"; import { useAuth0 } from "react-native-auth0"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { useVerifyOrCreateLazyQuery } from "./auth_verify_graphql/auth_verify.generated"; diff --git a/apps/native/modules/screens/collection/collection_add_form_page/collection_add_form/use_collection_add_form/use_collection_add_form.ts b/apps/native/modules/screens/collection/collection_add_form_page/collection_add_form/use_collection_add_form/use_collection_add_form.ts index a7182aa..c84a320 100644 --- a/apps/native/modules/screens/collection/collection_add_form_page/collection_add_form/use_collection_add_form/use_collection_add_form.ts +++ b/apps/native/modules/screens/collection/collection_add_form_page/collection_add_form/use_collection_add_form/use_collection_add_form.ts @@ -1,6 +1,6 @@ -import { useToastController } from "@tamagui/toast"; import { router } from "expo-router"; import { useForm } from "react-hook-form"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { z } from "zod"; import { useAddGameToCollection } from "./add_game_to_collection/use_game_to_collection"; diff --git a/apps/native/modules/screens/collection/collection_details/collection_details.tsx b/apps/native/modules/screens/collection/collection_details/collection_details.tsx index feb3705..c8dc758 100644 --- a/apps/native/modules/screens/collection/collection_details/collection_details.tsx +++ b/apps/native/modules/screens/collection/collection_details/collection_details.tsx @@ -1,14 +1,5 @@ -import { useLocalSearchParams, useNavigation } from "expo-router"; -import { useEffect } from "react"; -import { - Card, - Image, - ScrollView, - Spinner, - View, - XStack, - YStack, -} from "tamagui"; +import { useLocalSearchParams } from "expo-router"; +import { Card, Image, ScrollView, Spinner, View } from "tamagui"; import { Text } from "ui/typography/text"; import { CollectionDetailsEmpty } from "./collection_details_empty/collection_details_empty"; @@ -17,6 +8,9 @@ import { useCollectionDetails } from "./use_collection_details/use_collection_de import { useResetCollectionHeaderTitle } from "./use_reset_collection_header_title"; import { useSetHeaderTitle } from "../../../router/use_set_header_title"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const CollectionDetails = () => { const { id } = useLocalSearchParams<{ id: string }>(); useResetCollectionHeaderTitle(); @@ -37,7 +31,7 @@ export const CollectionDetails = () => { const collection = collectionDetailQuery.data.collection; return ( - + {collection.games.map((game) => { return ( @@ -47,7 +41,7 @@ export const CollectionDetails = () => { marginBottom={8} > - + { {game.name} - + ); @@ -68,6 +62,6 @@ export const CollectionDetails = () => { - + ); }; diff --git a/apps/native/modules/screens/collection/collection_details/collection_details_empty/collection_details_empty.tsx b/apps/native/modules/screens/collection/collection_details/collection_details_empty/collection_details_empty.tsx index d06a63b..9cdd0de 100644 --- a/apps/native/modules/screens/collection/collection_details/collection_details_empty/collection_details_empty.tsx +++ b/apps/native/modules/screens/collection/collection_details/collection_details_empty/collection_details_empty.tsx @@ -1,11 +1,13 @@ -import { View, YStack } from "tamagui"; +import { View } from "tamagui"; import { Text } from "ui/typography/text"; import { EmptyCollection } from "../assets/empty_collection"; import { CollectionDetailsFab } from "../collection_details_fab/collection_details_fab"; + +import { VStack } from "@/ui/layout/vstack/vstack"; export const CollectionDetailsEmpty = () => { return ( - + @@ -16,6 +18,6 @@ export const CollectionDetailsEmpty = () => { Użyj przycisku by coś dodać! - + ); }; diff --git a/apps/native/modules/screens/collection/collection_details/collection_details_fab/collection_details_fab.tsx b/apps/native/modules/screens/collection/collection_details/collection_details_fab/collection_details_fab.tsx index c00cb4e..42dbce0 100644 --- a/apps/native/modules/screens/collection/collection_details/collection_details_fab/collection_details_fab.tsx +++ b/apps/native/modules/screens/collection/collection_details/collection_details_fab/collection_details_fab.tsx @@ -1,20 +1,17 @@ +import { PlusCircle } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { FloatingAction } from "react-native-floating-action"; -import { XStack } from "tamagui"; -import { ACTION_NAMES, FabOptions } from "./fab_options"; +import { Fab, FabIcon, FabLabel } from "@/ui/overlay/fab/fab"; export const CollectionDetailsFab = () => { return ( - - { - if (name === ACTION_NAMES.ADD_GAME) { - router.push("collection/collection_game_search"); - } - }} - /> - + router.push("collection/collection_game_search")} + > + + Dodaj grę do kolekcji + ); }; diff --git a/apps/native/modules/screens/collection/new_collection/new_collection_form/new_collection_form.tsx b/apps/native/modules/screens/collection/new_collection/new_collection_form/new_collection_form.tsx index 5ca6068..cdc74b9 100644 --- a/apps/native/modules/screens/collection/new_collection/new_collection_form/new_collection_form.tsx +++ b/apps/native/modules/screens/collection/new_collection/new_collection_form/new_collection_form.tsx @@ -1,12 +1,14 @@ import { router } from "expo-router"; import { Controller, FormProvider } from "react-hook-form"; -import { Button, Form, YStack } from "tamagui"; +import { Button, Form } from "tamagui"; import { Input } from "ui/forms/input"; import { TextArea } from "ui/forms/text_area"; import { Text } from "ui/typography/text"; import { useNewCollectionForm } from "./use_new_collection_form"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const NewCollectionForm = () => { const { methods, onSubmit } = useNewCollectionForm({ onSuccess: () => router.push("/collection"), @@ -14,12 +16,7 @@ export const NewCollectionForm = () => { return ( - + { return ( @@ -60,7 +57,7 @@ export const NewCollectionForm = () => { Pola oznaczone gwiazdką są wymagane - + ); diff --git a/apps/native/modules/screens/collection/new_collection/new_collection_form/use_new_collection_form.ts b/apps/native/modules/screens/collection/new_collection/new_collection_form/use_new_collection_form.ts index 5e4af82..27b6758 100644 --- a/apps/native/modules/screens/collection/new_collection/new_collection_form/use_new_collection_form.ts +++ b/apps/native/modules/screens/collection/new_collection/new_collection_form/use_new_collection_form.ts @@ -1,6 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { useToastController } from "@tamagui/toast"; import { useForm } from "react-hook-form"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { NewCollectionPageFields, diff --git a/apps/native/modules/screens/collection/new_collection/new_collection_page.tsx b/apps/native/modules/screens/collection/new_collection/new_collection_page.tsx index 10ad209..36469be 100644 --- a/apps/native/modules/screens/collection/new_collection/new_collection_page.tsx +++ b/apps/native/modules/screens/collection/new_collection/new_collection_page.tsx @@ -1,10 +1,12 @@ -import { Card, YStack } from "tamagui"; +import { Card } from "tamagui"; import { NewCollectionForm } from "./new_collection_form/new_collection_form"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const NewCollectionPage = () => { return ( - + { > - + ); }; diff --git a/apps/native/modules/screens/friends/friends_list/friends_list_fab/friends_list_fab.tsx b/apps/native/modules/screens/friends/friends_list/friends_list_fab/friends_list_fab.tsx index 9503020..8105f67 100644 --- a/apps/native/modules/screens/friends/friends_list/friends_list_fab/friends_list_fab.tsx +++ b/apps/native/modules/screens/friends/friends_list/friends_list_fab/friends_list_fab.tsx @@ -1,23 +1,17 @@ +import { Search } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { FloatingAction } from "react-native-floating-action"; -import { XStack } from "tamagui"; -import { ACTION_NAMES, FabOptions } from "./friends_list_fab_options"; +import { Fab, FabIcon, FabLabel } from "@/ui/overlay/fab/fab"; export const FriendsListFab = () => { return ( - - { - if (name === ACTION_NAMES.FRIENDS_REQUESTS) { - router.push("/friends/friends_requests/"); - } - if (name === ACTION_NAMES.FRIENDS_SEARCH) { - router.push("/friends/friends_search/"); - } - }} - /> - + router.push("/friends/friends_search/")} + > + + Dodaj znajomych + ); }; diff --git a/apps/native/modules/screens/friends/friends_list/friends_list_screen.tsx b/apps/native/modules/screens/friends/friends_list/friends_list_screen.tsx index 2a48e5b..d86b7ac 100644 --- a/apps/native/modules/screens/friends/friends_list/friends_list_screen.tsx +++ b/apps/native/modules/screens/friends/friends_list/friends_list_screen.tsx @@ -2,21 +2,17 @@ import { ChevronRight } from "@tamagui/lucide-icons"; import { router } from "expo-router"; import { Fragment, useState } from "react"; import { RefreshControl } from "react-native-gesture-handler"; -import { - Card, - ScrollView, - Separator, - Spinner, - View, - XStack, - YStack, -} from "tamagui"; +import { Card, ScrollView, Separator, Spinner, View } from "tamagui"; import { Text } from "ui/typography/text"; import { FriendsListFab } from "./friends_list_fab/friends_list_fab"; import { useFriendsList } from "./use_friends_list/use_friends_list"; import { UserAvatar } from "../../../user/user_avatar/user_avatar"; +import { Pressable } from "@/ui/forms/pressable/pressable"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const FriendsListScreen = () => { const [isRefreshing, setIsRefreshing] = useState(false); const friendsListQuery = useFriendsList(); @@ -46,33 +42,32 @@ export const FriendsListScreen = () => { Twoi znajomi - + {friends.map((friend, index) => ( - router.push( `/friends/user_profile/${friend.id}?take=5&skip=0`, ) } > - + {friend.name} - + - + {friends.length > 1 && friends.length - 1 !== index && ( )} ))} - + diff --git a/apps/native/modules/screens/friends/friends_requests/friends_requests_empty/friends_requests_empty.tsx b/apps/native/modules/screens/friends/friends_requests/friends_requests_empty/friends_requests_empty.tsx index e05857c..ebc4c13 100644 --- a/apps/native/modules/screens/friends/friends_requests/friends_requests_empty/friends_requests_empty.tsx +++ b/apps/native/modules/screens/friends/friends_requests/friends_requests_empty/friends_requests_empty.tsx @@ -1,9 +1,11 @@ import { RefreshControl } from "react-native-gesture-handler"; -import { ScrollView, View, YStack } from "tamagui"; +import { ScrollView, View } from "tamagui"; import { Text } from "ui/typography/text"; import UndrawSearch from "../assets/undraw_mobile_search.svg"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type FriendsRequestsEmptyProps = { onRefresh: () => void; refreshing: boolean; @@ -19,7 +21,7 @@ export const FriendsRequestsEmpty = ({ } > - + - + Nie bój się zaprosić znajomych. Wróc na poprzedni ekran i zaproś kogoś! - - + + ); }; diff --git a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_results.tsx b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_results.tsx index a919fab..6ac08d0 100644 --- a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_results.tsx +++ b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_results.tsx @@ -1,8 +1,10 @@ -import { ScrollView, YStack } from "tamagui"; +import { ScrollView } from "tamagui"; import { Text } from "ui/typography/text"; import { FriendsRequestsSender } from "./friends_requests_sender/friends_requests_sender"; -import { pluralizePolish } from "../../../../strings/pluralize"; + +import { pluralizePolish } from "@/modules/strings/pluralize"; +import { VStack } from "@/ui/layout/vstack/vstack"; type FriendsRequestsResultsProps = { friendsRequests: FriendRequest[]; @@ -20,7 +22,7 @@ export const FriendsRequestsResults = ({ }: FriendsRequestsResultsProps) => { return ( - + {pluralizePolish( friendsRequests.length, @@ -46,7 +48,7 @@ export const FriendsRequestsResults = ({ }} /> ))} - + ); }; diff --git a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/friends_requests_sender.tsx b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/friends_requests_sender.tsx index 0966c1c..af56bd5 100644 --- a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/friends_requests_sender.tsx +++ b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/friends_requests_sender.tsx @@ -1,8 +1,11 @@ -import { Button, Spinner, XStack, YStack } from "tamagui"; +import { Button, Spinner } from "tamagui"; import { Text } from "ui/typography/text"; import { useAcceptFriendRequest } from "./use_accept_friend_request/use_accept_friend_request"; -import { UserAvatar } from "../../../../../user/user_avatar/user_avatar"; + +import { UserAvatar } from "@/modules/user/user_avatar/user_avatar"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; type FriendsRequestsSenderProps = { sender: Sender; @@ -25,20 +28,15 @@ export const FriendsRequestsSender = ({ acceptFriendRequest({ variables: { senderOauthId: sender.oauthId } }); }; return ( - - + + {sender.profile?.name} - - - + + + - - - + + + ); }; diff --git a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts index 20c68c1..9b2bc58 100644 --- a/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts +++ b/apps/native/modules/screens/friends/friends_requests/friends_requests_results/friends_requests_sender/use_accept_friend_request/use_accept_friend_request.ts @@ -1,4 +1,4 @@ -import { useToastController } from "@tamagui/toast"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { useAcceptFriendRequestMutation } from "./accept_friend_request_mutation.generated"; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx index 8dcda84..c234229 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_empty.tsx @@ -1,12 +1,13 @@ -import { YStack } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const FriendsSearchEmpty = () => { return ( - + Brak wyników - + ); }; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx index 8ce8e44..a34bb69 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result.tsx @@ -1,11 +1,13 @@ -import { Separator, XStack } from "tamagui"; +import { Separator } from "tamagui"; import { Text } from "ui/typography/text"; import { FriendsSearchResultSendRequestButton } from "./friends_search_result_send_request_button/friends_search_result_send_request_button"; -import { truncateString } from "../../../../../strings/truncate_string"; -import { UserAvatar } from "../../../../../user/user_avatar/user_avatar"; import { useSendFriendRequest } from "../use_send_friend_request/use_send_friend_request"; +import { truncateString } from "@/modules/strings/truncate_string"; +import { UserAvatar } from "@/modules/user/user_avatar/user_avatar"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type FriendsSearchResultProps = { oauthId: string; avatarUrl?: string; @@ -34,13 +36,13 @@ export const FriendsSearchResult = ({ return ( <> - - + + {truncateString(name || "", 20)} - + - + {usersLength > 1 && usersLength - 1 !== currentIndex && ( )} diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx index 481639c..42cf218 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_result/friends_search_result_send_request_button/friends_search_result_send_request_button.tsx @@ -1,6 +1,8 @@ -import { Button, Spinner, YStack } from "tamagui"; +import { Button, Spinner } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type FriendsSearchResultSendRequestButtonProps = { isFriendRequestSent: boolean; handleSendFriendRequest: (oauthId: string) => Promise; @@ -18,14 +20,14 @@ export const FriendsSearchResultSendRequestButton = ({ }: FriendsSearchResultSendRequestButtonProps) => { if (isFriendRequestSent) { return ( - + Oczekuje na akceptacje - + ); } diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx index 8527fdd..dd181c7 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/friends_search_results.tsx @@ -1,8 +1,10 @@ -import { ScrollView, YStack } from "tamagui"; +import { ScrollView } from "tamagui"; import { FriendsSearchEmpty } from "./friends_search_empty"; import { FriendsSearchResult } from "./friends_search_result/friends_search_result"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type FriendsSearchResultsProps = { users: User[]; }; @@ -20,7 +22,7 @@ export const FriendsSearchResults = ({ users }: FriendsSearchResultsProps) => { } return ( - + {users.map((user, index) => ( { isFriendRequestSent={user.isFriendRequestSent} /> ))} - + ); }; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts b/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts index 4579102..3cf2b91 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts +++ b/apps/native/modules/screens/friends/friends_search/friends_search_results/use_send_friend_request/use_send_friend_request.ts @@ -1,4 +1,4 @@ -import { useToastController } from "@tamagui/toast"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { useSendFriendRequestMutation } from "./send_friend_request_mutation.generated"; diff --git a/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx b/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx index c66698c..4f0875d 100644 --- a/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx +++ b/apps/native/modules/screens/friends/friends_search/friends_search_screen.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Spinner, YStack } from "tamagui"; +import { Spinner } from "tamagui"; import { Text } from "ui/typography/text"; import { useDebounce } from "use-debounce"; @@ -7,12 +7,14 @@ import { FriendsSearchResults } from "./friends_search_results/friends_search_re import { useGetUsersSearch } from "./use_get_users_search/use_get_users_search"; import { SearchInput } from "../../search/search_input/search_input"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const FriendsSearchScreen = () => { const [username, setUsername] = useState(""); const [debouncedUsername] = useDebounce(username, 1000); const { data, loading } = useGetUsersSearch({ input: debouncedUsername }); return ( - + Wyszukiwarka @@ -21,7 +23,7 @@ export const FriendsSearchScreen = () => { Używając powyższego inputa możesz wyszukać użytkownków i dodać ich do znajomych. - + {loading && } {data && data.usersSearch && ( { }))} /> )} - - + + ); }; diff --git a/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx b/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx index c229b19..f0444cc 100644 --- a/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx +++ b/apps/native/modules/screens/friends/user_profile/user_profile_info_card/user_profile_info_card.tsx @@ -1,9 +1,10 @@ import { RefreshCcw } from "@tamagui/lucide-icons"; -import { Card, View, XStack } from "tamagui"; +import { Card, View } from "tamagui"; import { ButtonWithIcon } from "ui/forms/button_icon"; import { Text } from "ui/typography/text"; -import { UserAvatar } from "../../../../user/user_avatar/user_avatar"; +import { UserAvatar } from "@/modules/user/user_avatar/user_avatar"; +import { HStack } from "@/ui/layout/hstack/hstack"; type UserProfileInfoCardProps = { name?: string | null; @@ -18,20 +19,20 @@ export const UserProfileInfoCard = ({ }: UserProfileInfoCardProps) => { return ( - + - + Nazwa: {name} - + } /> - + ); }; diff --git a/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx b/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx index fd7b988..3b4bc3a 100644 --- a/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx +++ b/apps/native/modules/screens/friends/user_profile/user_profile_screen.tsx @@ -1,6 +1,6 @@ import { Filter } from "@tamagui/lucide-icons"; import { Link, useLocalSearchParams } from "expo-router"; -import { ScrollView, Spinner, View, XStack, YStack } from "tamagui"; +import { Spinner, View } from "tamagui"; import { useUserFriendGamesStatus } from "./use_user_friend_games_status/use_user_friend_games_status"; import { useUserProfile } from "./use_user_profile/use_user_profile"; @@ -9,6 +9,9 @@ import { GamesStatusList } from "../../games/games_status_list/games_status_list import { mapGamesStatusToItem } from "../../games/games_status_list/map_games_status_to_item"; import { GamesStatusListSearch } from "../../games/games_status_list_search/games_status_list_search"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const UserProfileScreen = () => { const localSearchParams = useLocalSearchParams<{ oauth_id: string }>(); const { oauth_id } = localSearchParams; @@ -27,7 +30,7 @@ export const UserProfileScreen = () => { userFriendGamesStatus.data?.userFriendGamesStatus.userGamesStatus || [], ); return ( - + {!userProfileQuery.loading && userProfileQuery.data ? ( { ) : ( )} - + { - + { items={items} oauthId={userProfileQuery.data?.user.oauthId} /> - + ); }; diff --git a/apps/native/modules/screens/game/game_info/game_info.tsx b/apps/native/modules/screens/game/game_info/game_info.tsx index 7bb3682..7ed9218 100644 --- a/apps/native/modules/screens/game/game_info/game_info.tsx +++ b/apps/native/modules/screens/game/game_info/game_info.tsx @@ -1,6 +1,8 @@ -import { Card, XStack } from "tamagui"; +import { Card } from "tamagui"; import { Text } from "ui/typography/text"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type GameInfoProps = { game: { name: string; @@ -16,7 +18,7 @@ export const GameInfo = ({ game }: GameInfoProps) => { {game.name} - + {game.releaseYear && ( {game.releaseYear} @@ -40,7 +42,7 @@ export const GameInfo = ({ game }: GameInfoProps) => { : platform} ))} - + ); diff --git a/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx b/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx index 5dca38a..4e65cc8 100644 --- a/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx +++ b/apps/native/modules/screens/game/game_preparing_info/game_preparing_info.tsx @@ -1,6 +1,8 @@ -import { Button, YStack } from "tamagui"; +import { Button } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GamePreparingInfoProps = { onRefreshClick: () => void; }; @@ -9,7 +11,7 @@ export const GamePreparingInfo = ({ onRefreshClick, }: GamePreparingInfoProps) => { return ( - + Gra jest aktualnie pobierana do naszej bazy. @@ -24,6 +26,6 @@ export const GamePreparingInfo = ({ > Odśwież - + ); }; diff --git a/apps/native/modules/screens/game/game_screen.tsx b/apps/native/modules/screens/game/game_screen.tsx index 656aa48..12fd37f 100644 --- a/apps/native/modules/screens/game/game_screen.tsx +++ b/apps/native/modules/screens/game/game_screen.tsx @@ -1,6 +1,5 @@ -import { useLocalSearchParams, useNavigation } from "expo-router"; -import { useEffect } from "react"; -import { Spinner, YStack, ScrollView } from "tamagui"; +import { useLocalSearchParams } from "expo-router"; +import { Spinner, ScrollView } from "tamagui"; import { GameCompletionTime } from "./game_completion_time/game_completion_time"; import { GameImage } from "./game_image/game_image"; @@ -10,6 +9,8 @@ import { GameTabs } from "./game_tabs/game_tabs"; import { useGetGameInfo } from "./use_get_game_info/use_get_game_info"; import { useSetHeaderTitle } from "../../router/use_set_header_title"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GameScreenProps = { redirect: { addToGameStatusUrl: string; @@ -34,14 +35,14 @@ export const GameScreen = ({ redirect }: GameScreenProps) => { const game = gameQuery.data.game; return ( - - + + - + { completionist={game.completionTime?.completionist} mainExtra={game.completionTime?.mainExtra} /> - + ); }; diff --git a/apps/native/modules/screens/game/game_tabs/game_tabs.tsx b/apps/native/modules/screens/game/game_tabs/game_tabs.tsx index e17cce9..e869a52 100644 --- a/apps/native/modules/screens/game/game_tabs/game_tabs.tsx +++ b/apps/native/modules/screens/game/game_tabs/game_tabs.tsx @@ -1,8 +1,10 @@ -import { Check, Plus } from "@tamagui/lucide-icons"; +import { Check } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { Card, Separator, View, YStack } from "tamagui"; +import { Card, View } from "tamagui"; import { Text } from "ui/typography/text"; +import { VStack } from "@/ui/layout/vstack/vstack"; + type GameTabsProps = { game: { name: string; @@ -14,10 +16,6 @@ type GameTabsProps = { }; export const GameTabs = ({ game, redirect }: GameTabsProps) => { - const redirectToCollectionAddForm = () => { - router.push(`/collection/collection_add_form/${game.hltbId}`); - }; - const redirectToGamesStatusAddForm = () => { router.push(`${redirect.addToGameStatusUrl}/${game.hltbId}`); }; @@ -36,14 +34,14 @@ export const GameTabs = ({ game, redirect }: GameTabsProps) => { > - + Dodaj do swoich gier - + ); diff --git a/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx b/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx index b11a7e8..6f80fba 100644 --- a/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx +++ b/apps/native/modules/screens/game_status_reviews/game_status_reviews_screen.tsx @@ -1,14 +1,18 @@ import { ChevronRight } from "@tamagui/lucide-icons"; import { router, useLocalSearchParams } from "expo-router"; -import { ScrollView, Separator, Spinner, XStack, YStack } from "tamagui"; +import { ScrollView, Separator, Spinner, XStack } from "tamagui"; import { useGameStatusReviewStore } from "./use_game_status_review_store/use_game_status_review_store"; -import { Text } from "../../../ui/typography/text"; import { truncateString } from "../../strings/truncate_string"; import { UserAvatar } from "../../user/user_avatar/user_avatar"; import { useFriendsGameReviews } from "../user_game_status/user_game_status_friends_reviews/use_friends_game_reviews/use_friends_game_reviews"; import { parseScore } from "../user_game_status/user_game_status_sections/user_game_status_score_section/parse_score"; +import { Pressable } from "@/ui/forms/pressable/pressable"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; +import { Text } from "@/ui/typography/text"; + type GameStatusReviewsScreenProps = { redirect: { review: "friends" | "games"; @@ -55,11 +59,11 @@ export const GameStatusReviewsScreen = ({ }; return ( - + {friendsGameReviews.map((review) => { return ( - { handleReviewClick({ @@ -68,13 +72,13 @@ export const GameStatusReviewsScreen = ({ }); }} > - - + + - + {truncateString(review.profile?.name || "", 20)} @@ -93,15 +97,15 @@ export const GameStatusReviewsScreen = ({ )} - - + + {review.review && } - + - + ); })} - + ); }; diff --git a/apps/native/modules/screens/games/games_screen.tsx b/apps/native/modules/screens/games/games_screen.tsx index 32b3d5d..ae9ce51 100644 --- a/apps/native/modules/screens/games/games_screen.tsx +++ b/apps/native/modules/screens/games/games_screen.tsx @@ -1,6 +1,6 @@ import { Filter } from "@tamagui/lucide-icons"; import { Link } from "expo-router"; -import { View, XStack, YStack } from "tamagui"; +import { View } from "tamagui"; import { GamesStatusCategoriesFab } from "./games_status_categories_fab/games_status_categories_fab"; import { GamesStatusList } from "./games_status_list/games_status_list"; @@ -8,14 +8,17 @@ import { mapGamesStatusToItem } from "./games_status_list/map_games_status_to_it import { GamesStatusListSearch } from "./games_status_list_search/games_status_list_search"; import { useUserGamesStatus } from "./use_user_games_status/use_user_games_status"; +import { HStack } from "@/ui/layout/hstack/hstack"; +import { VStack } from "@/ui/layout/vstack/vstack"; + export const GamesScreen = () => { const gamesStatus = useUserGamesStatus(); const items = mapGamesStatusToItem( gamesStatus.data?.userGamesStatus.userGamesStatus || [], ); return ( - - + + { - + { onRefresh={gamesStatus.onRefresh} /> - + ); }; diff --git a/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx b/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx index e54f7d6..33be4c2 100644 --- a/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx +++ b/apps/native/modules/screens/games/games_status_add_form/games_status_add_form_screen.tsx @@ -1,12 +1,14 @@ import { useLocalSearchParams } from "expo-router"; import { KeyboardAvoidingView } from "react-native"; -import { Card, Spinner, XStack, Separator, ScrollView } from "tamagui"; +import { Card, Spinner, Separator, ScrollView } from "tamagui"; import { Text } from "ui/typography/text"; import { GamesStatusAddForm } from "./games_status_add_form"; import { truncateString } from "../../../strings/truncate_string"; import { useGetGameInfo } from "../../game/use_get_game_info/use_get_game_info"; +import { HStack } from "@/ui/layout/hstack/hstack"; + export const GamesStatusAddFormScreen = () => { const { hltb_id } = useLocalSearchParams<{ hltb_id: string }>(); const gameQuery = useGetGameInfo(hltb_id); @@ -19,7 +21,7 @@ export const GamesStatusAddFormScreen = () => { const game = gameQuery.data.game; return ( - + { height="100%" > - + Aktualnie dodajesz: {truncateString(game.name, 15)} - + diff --git a/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx b/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx index 526eff5..f4a81d8 100644 --- a/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx +++ b/apps/native/modules/screens/games/games_status_categories_fab/games_status_categories_fab.tsx @@ -1,23 +1,17 @@ +import { PlusCircle } from "@tamagui/lucide-icons"; import { router } from "expo-router"; -import { FloatingAction } from "react-native-floating-action"; -import { XStack } from "tamagui"; -import { - GAMES_STATUS_CATEGORIES_FAB_OPTIONS, - ACTION_NAMES, -} from "./games_status_categories_fab_options"; +import { Fab, FabIcon, FabLabel } from "@/ui/overlay/fab/fab"; export const GamesStatusCategoriesFab = () => { return ( - - { - if (name === ACTION_NAMES.ADD_GAME) { - router.push("/games/games_search"); - } - }} - /> - + router.push("/games/games_search")} + > + + Dodaj nową grę + ); }; diff --git a/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx b/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx index 107dded..81eaa26 100644 --- a/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx +++ b/apps/native/modules/screens/games/games_status_edit_form_screen/games_status_edit_form/games_status_edit_form.tsx @@ -1,11 +1,13 @@ import { KeyboardAvoidingView } from "react-native"; -import { Card, ScrollView, Separator, XStack } from "tamagui"; +import { Card, ScrollView, Separator } from "tamagui"; import { Text } from "ui/typography/text"; -import { GamesStatusForm } from "../../../../games_status/games_status_form/games_status_form"; -import { truncateString } from "../../../../strings/truncate_string"; import { UserGameStatusQuery } from "../../../user_game_status/use_user_game_status/user_game_status_query.generated"; +import { GamesStatusForm } from "@/modules/games_status/games_status_form/games_status_form"; +import { truncateString } from "@/modules/strings/truncate_string"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type GamesStatusEditFormProps = { gameStatus: UserGameStatusQuery["userGameStatus"]; }; @@ -23,14 +25,14 @@ export const GamesStatusEditForm = ({ height="100%" > - + Aktualnie edytujesz: {truncateString(gameStatus.game.name, 15)} - + { const { data, loading } = useGetGamesStatusFilters(); const { form, handleSubmit } = useGamesStatusFiltersForm(); if (loading || !data) { return ( - - - + + + ); } return ( - +
- + { - + + + + ); + } + return ( + + + + ); +}; + +type CurrentlyPlayingCarouselItemProps = { + item: { + gameStatusId: number; + gameImage: string; + gameTitle: string; + gameStatus: string; + }; +}; + +const CurrentlyPlayingCarouselItem = ({ + item, +}: CurrentlyPlayingCarouselItemProps) => { + const router = useRouter(); + const onPress = () => { + router.push( + `/(app)/(authorized)/games_status/games_status_info/${item.gameStatusId}`, + ); + }; + return ( + + + {item.gameTitle} + + + {truncateString(item.gameTitle, 15)} + + + {item.gameStatus} + + + + + ); +}; diff --git a/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts new file mode 100644 index 0000000..fedb176 --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated.ts @@ -0,0 +1,58 @@ +import * as Types from '../../../../../__generated__/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type LastEditedGamesQueryVariables = Types.Exact<{ + limit: Types.Scalars['Int']['input']; +}>; + + +export type LastEditedGamesQuery = { __typename?: 'Query', lastEditedGames: Array<{ __typename?: 'LastEditedGamesStatusDTO', id: number, name: string, status: Types.GameStatus, cover?: { __typename?: 'CoverDTO', bigUrl: string } | null }> }; + + +export const LastEditedGamesDocument = gql` + query LastEditedGames($limit: Int!) { + lastEditedGames(limit: $limit) { + id + name + status + cover { + bigUrl + } + } +} + `; + +/** + * __useLastEditedGamesQuery__ + * + * To run a query within a React component, call `useLastEditedGamesQuery` and pass it any options that fit your needs. + * When your component renders, `useLastEditedGamesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useLastEditedGamesQuery({ + * variables: { + * limit: // value for 'limit' + * }, + * }); + */ +export function useLastEditedGamesQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(LastEditedGamesDocument, options); + } +export function useLastEditedGamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(LastEditedGamesDocument, options); + } +export function useLastEditedGamesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(LastEditedGamesDocument, options); + } +export type LastEditedGamesQueryHookResult = ReturnType; +export type LastEditedGamesLazyQueryHookResult = ReturnType; +export type LastEditedGamesSuspenseQueryHookResult = ReturnType; +export type LastEditedGamesQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql new file mode 100644 index 0000000..51753ff --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.graphql @@ -0,0 +1,10 @@ +query LastEditedGames($limit: Int!) { + lastEditedGames(limit: $limit) { + id + name + status + cover { + bigUrl + } + } +} \ No newline at end of file diff --git a/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts new file mode 100644 index 0000000..c3b5ef3 --- /dev/null +++ b/apps/native/modules/screens/homepage/last_updated/use_last_edited_games/use_last_edited_games.ts @@ -0,0 +1,18 @@ +import { useLastEditedGamesQuery } from "@/modules/screens/homepage/last_updated/use_last_edited_games/last_edited_games.generated"; +export const useLastEditedGames = () => { + return useLastEditedGamesQuery({ + variables: { + limit: 10, + }, + }); +}; + +export const useRefetchLastEditedGames = () => { + const query = useLastEditedGames(); + + const refetchLastEditedGames = async () => { + await query.refetch(); + }; + + return { refetchLastEditedGames }; +}; diff --git a/apps/native/modules/screens/profile/profile_editor_form/photo_editor/photo_editor.tsx b/apps/native/modules/screens/profile/profile_editor_form/photo_editor/photo_editor.tsx index 0f7804a..5a060f6 100644 --- a/apps/native/modules/screens/profile/profile_editor_form/photo_editor/photo_editor.tsx +++ b/apps/native/modules/screens/profile/profile_editor_form/photo_editor/photo_editor.tsx @@ -1,8 +1,8 @@ -import { useToastController } from "@tamagui/toast"; import * as ImagePicker from "expo-image-picker"; import { useState } from "react"; import { useFormContext } from "react-hook-form"; import { Button } from "tamagui"; +import { useToastController } from "ui/feedback/toast/use_toast_controller"; import { Text } from "ui/typography/text"; import { getFileInfo } from "../../../../files/get_file_info"; diff --git a/apps/native/modules/screens/profile/profile_editor_form/profile_editor_form.tsx b/apps/native/modules/screens/profile/profile_editor_form/profile_editor_form.tsx index 71bf54e..8f86ce9 100644 --- a/apps/native/modules/screens/profile/profile_editor_form/profile_editor_form.tsx +++ b/apps/native/modules/screens/profile/profile_editor_form/profile_editor_form.tsx @@ -1,9 +1,11 @@ import { Controller, FormProvider } from "react-hook-form"; -import { Button, Form, Input, Label, Spinner, XStack } from "tamagui"; +import { Button, Form, Input, Label, Spinner } from "tamagui"; import { PhotoEditor } from "./photo_editor/photo_editor"; import { useProfileEditorForm } from "./use_profile_editor_form"; +import { HStack } from "@/ui/layout/hstack/hstack"; + type ProfileEditorFormProps = { onSubmit: () => void; defaultValues: { @@ -28,7 +30,7 @@ export const ProfileEditorForm = ({ style={{ display: "flex", alignItems: "center", gap: 16 }} > - + { return ( @@ -43,7 +45,7 @@ export const ProfileEditorForm = ({ name="name" control={methods.control} /> - + - +
- + ); }; diff --git a/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx b/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx index a540602..76672c9 100644 --- a/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx +++ b/apps/native/modules/screens/profile/upload_hltb_migration_document/hltb_document_picker/hltb_document_picker.tsx @@ -1,8 +1,10 @@ -import { Button, Spinner, YStack } from "tamagui"; +import { Button, Spinner } from "tamagui"; import { Text } from "ui/typography/text"; import { useHltbDocumentPicker } from "./use_hltb_document_picker"; -import { truncateString } from "../../../../strings/truncate_string"; + +import { truncateString } from "@/modules/strings/truncate_string"; +import { VStack } from "@/ui/layout/vstack/vstack"; type HltbDocumentPickerProps = { buttonVisible: boolean; @@ -17,7 +19,7 @@ export const HltbDocumentPicker = ({ } if (!buttonVisible) return null; return ( - +