Compare commits
No commits in common. "4112645f5e61c9f42479f678cffa0eb1cb486531" and "fd4bf757fad6268670059466be5a4ccb2faf9df7" have entirely different histories.
4112645f5e
...
fd4bf757fa
|
|
@ -6,11 +6,9 @@ import { CatalogModule } from './catalog/catalog.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
import { FilesModule } from './files/files.module';
|
import { FilesModule } from './files/files.module';
|
||||||
import { ReviewsModule } from './reviews/reviews.module';
|
|
||||||
import { ReservationsModule } from './reservations/reservations.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule, ReviewsModule, ReservationsModule],
|
imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [AuthService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AuthService>(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -24,13 +24,9 @@ export class AuthService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
login(user: Omit<User, 'password'>) {
|
async login(user: { email: string; password: string }) {
|
||||||
this.logger.log('LOG');
|
this.logger.log('LOG');
|
||||||
const payload = {
|
const payload = { username: user.email, sub: user.password };
|
||||||
username: user.email,
|
|
||||||
sub: user.userId,
|
|
||||||
userId: user.userId,
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
access_token: this.jwtService.sign(payload),
|
access_token: this.jwtService.sign(payload),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { CatalogController } from './catalog.controller';
|
||||||
|
|
||||||
|
describe('CatalogController', () => {
|
||||||
|
let controller: CatalogController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [CatalogController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<CatalogController>(CatalogController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,64 +1,47 @@
|
||||||
import { Controller, Get, Query, Param, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { CatalogService } from './catalog.service';
|
import { CatalogService } from './catalog.service';
|
||||||
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
||||||
import { CatalogFiltersDto } from './dto/catalog-filters.dto';
|
import { CatalogItemDto } from './dto/catalog-item.dto';
|
||||||
import {
|
import { CatalogParamsDto } from './dto/catalog-params.dto';
|
||||||
CatalogItemShortDto,
|
|
||||||
CatalogItemLongDto,
|
|
||||||
} from './dto/catalog-item.dto';
|
|
||||||
import {
|
import {
|
||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiProperty,
|
ApiProperty,
|
||||||
ApiBody,
|
|
||||||
ApiTags,
|
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
|
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
|
||||||
import { CreateYachtDto } from './dto/create-yacht.dto';
|
|
||||||
|
|
||||||
@ApiTags('catalog')
|
|
||||||
@Controller('catalog')
|
@Controller('catalog')
|
||||||
export class CatalogController {
|
export class CatalogController {
|
||||||
constructor(private readonly catalogService: CatalogService) {}
|
constructor(private readonly catalogService: CatalogService) {}
|
||||||
|
|
||||||
@Get('filter')
|
@Get()
|
||||||
@ApiOperation({ summary: 'Filter catalog items with query parameters' })
|
@ApiOperation({ summary: 'Get catalog with filters, search and sorting' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Filtered catalog items',
|
description: 'Catalog items with filters applied',
|
||||||
type: CatalogResponseDto,
|
type: CatalogResponseDto,
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'search', required: false, type: String })
|
@ApiQuery({
|
||||||
@ApiQuery({ name: 'minLength', required: false, type: Number })
|
name: 'params',
|
||||||
@ApiQuery({ name: 'maxLength', required: false, type: Number })
|
required: false,
|
||||||
@ApiQuery({ name: 'minPrice', required: false, type: Number })
|
description: 'JSON string of filter parameters',
|
||||||
@ApiQuery({ name: 'maxPrice', required: false, type: Number })
|
type: String,
|
||||||
@ApiQuery({ name: 'minYear', required: false, type: Number })
|
})
|
||||||
@ApiQuery({ name: 'maxYear', required: false, type: Number })
|
async getCatalog(
|
||||||
@ApiQuery({ name: 'guests', required: false, type: Number })
|
@Query('params') paramsString?: string,
|
||||||
@ApiQuery({ name: 'sortByPrice', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'paymentType', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'quickBooking', required: false, type: Boolean })
|
|
||||||
@ApiQuery({ name: 'hasToilet', required: false, type: Boolean })
|
|
||||||
@ApiQuery({ name: 'date', required: false, type: Date })
|
|
||||||
@ApiQuery({ name: 'departureTime', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'arrivalTime', required: false, type: String })
|
|
||||||
async getFilteredCatalog(
|
|
||||||
@Query() filters: CatalogFiltersDto,
|
|
||||||
): Promise<CatalogResponseDto> {
|
): Promise<CatalogResponseDto> {
|
||||||
return this.catalogService.getCatalog(filters);
|
let params: CatalogParamsDto = {};
|
||||||
|
|
||||||
|
if (paramsString) {
|
||||||
|
try {
|
||||||
|
params = JSON.parse(decodeURIComponent(paramsString));
|
||||||
|
} catch (error) {
|
||||||
|
params = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
return this.catalogService.getCatalog(params);
|
||||||
@ApiOperation({ summary: 'Get all catalog items (deprecated, use /filter)' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'All catalog items',
|
|
||||||
type: [CatalogItemShortDto],
|
|
||||||
})
|
|
||||||
async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
|
|
||||||
return this.catalogService.getAllCatalogItems();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('main-page')
|
@Get('main-page')
|
||||||
|
|
@ -68,46 +51,22 @@ export class CatalogController {
|
||||||
return this.catalogService.getMainPageCatalog();
|
return this.catalogService.getMainPageCatalog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('user/:userId')
|
@Get('all')
|
||||||
@ApiOperation({ summary: 'Get catalog items by user ID' })
|
@ApiOperation({ summary: 'Get all catalog items without filters' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Catalog items for the specified user',
|
description: 'All catalog items',
|
||||||
type: [CatalogItemShortDto],
|
type: [CatalogItemDto],
|
||||||
})
|
})
|
||||||
async getCatalogByUserId(
|
async getAllCatalogItems(): Promise<CatalogItemDto[]> {
|
||||||
@Param('userId') userId: string,
|
return this.catalogService.getAllCatalogItems();
|
||||||
): Promise<CatalogItemShortDto[]> {
|
|
||||||
return this.catalogService.getCatalogByUserId(Number(userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get('filtered')
|
||||||
@ApiOperation({ summary: 'Get catalog item by ID with full details' })
|
@ApiOperation({ summary: 'Get catalog with explicit query parameters' })
|
||||||
@ApiResponse({
|
async getFilteredCatalog(
|
||||||
status: 200,
|
@Query() params: CatalogParamsDto,
|
||||||
description: 'Catalog item with reviews, reservations and owner',
|
): Promise<CatalogResponseDto> {
|
||||||
type: CatalogItemLongDto,
|
return this.catalogService.getCatalog(params);
|
||||||
})
|
|
||||||
async getCatalogItemById(
|
|
||||||
@Param('id') id: string,
|
|
||||||
): Promise<CatalogItemLongDto | null> {
|
|
||||||
return this.catalogService.getCatalogItemById(Number(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@HttpCode(HttpStatus.CREATED)
|
|
||||||
@ApiOperation({ summary: 'Create a new yacht in catalog' })
|
|
||||||
@ApiBody({ type: CreateYachtDto })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 201,
|
|
||||||
description: 'Yacht successfully created',
|
|
||||||
type: CatalogItemLongDto,
|
|
||||||
})
|
|
||||||
@ApiResponse({
|
|
||||||
status: 400,
|
|
||||||
description: 'Bad request',
|
|
||||||
})
|
|
||||||
async createYacht(@Body() createYachtDto: CreateYachtDto): Promise<CatalogItemLongDto> {
|
|
||||||
return this.catalogService.createYacht(createYachtDto);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
import { Module, forwardRef } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CatalogService } from './catalog.service';
|
|
||||||
import { CatalogController } from './catalog.controller';
|
import { CatalogController } from './catalog.controller';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { CatalogService } from './catalog.service';
|
||||||
import { ReservationsModule } from '../reservations/reservations.module';
|
|
||||||
import { ReviewsModule } from '../reviews/reviews.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
|
||||||
UsersModule, // This provides UsersService
|
|
||||||
forwardRef(() => ReservationsModule), // This provides ReservationsService
|
|
||||||
ReviewsModule, // This provides ReviewsService
|
|
||||||
],
|
|
||||||
controllers: [CatalogController],
|
controllers: [CatalogController],
|
||||||
providers: [CatalogService],
|
providers: [CatalogService],
|
||||||
exports: [CatalogService], // Export for other modules to use
|
exports: [CatalogService],
|
||||||
})
|
})
|
||||||
export class CatalogModule {}
|
export class CatalogModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { CatalogService } from './catalog.service';
|
||||||
|
|
||||||
|
describe('CatalogService', () => {
|
||||||
|
let service: CatalogService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [CatalogService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<CatalogService>(CatalogService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,667 +1,337 @@
|
||||||
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { UsersService } from '../users/users.service';
|
import { CatalogItemDto } from './dto/catalog-item.dto';
|
||||||
import { ReservationsService } from '../reservations/reservations.service';
|
|
||||||
import { ReviewsService } from '../reviews/reviews.service';
|
|
||||||
import {
|
|
||||||
CatalogItemShortDto,
|
|
||||||
CatalogItemLongDto,
|
|
||||||
} from './dto/catalog-item.dto';
|
|
||||||
import { CatalogParamsDto } from './dto/catalog-params.dto';
|
import { CatalogParamsDto } from './dto/catalog-params.dto';
|
||||||
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
||||||
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
|
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
|
||||||
import { CatalogFiltersDto } from './dto/catalog-filters.dto';
|
|
||||||
import { CreateYachtDto } from './dto/create-yacht.dto';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CatalogService {
|
export class CatalogService {
|
||||||
constructor(
|
private catalogItems: CatalogItemDto[] = [
|
||||||
private readonly usersService: UsersService,
|
|
||||||
@Inject(forwardRef(() => ReservationsService))
|
|
||||||
private readonly reservationsService: ReservationsService,
|
|
||||||
private readonly reviewsService: ReviewsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private catalogItems: CatalogItemLongDto[] = [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Азимут 55',
|
name: 'Azimut 55',
|
||||||
length: 16.7,
|
length: 16.7,
|
||||||
speed: 32,
|
speed: 32,
|
||||||
minCost: 85000,
|
minCost: 85000,
|
||||||
mainImageUrl: 'uploads/1st.jpg',
|
mainImageUrl: '/api/uploads/1765727362318-238005198.jpg',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765727362318-238005198.jpg',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: true,
|
isFeatured: true,
|
||||||
year: 2022,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 12,
|
|
||||||
width: 4.8,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1200,
|
|
||||||
description:
|
|
||||||
'Роскошная моторная яхта Азимут 55 - это воплощение итальянского стиля и российского качества. Идеально подходит для прогулок по Финскому заливу, корпоративных мероприятий и романтических свиданий. На борту: три комфортабельные каюты, просторный салон с панорамным остеклением, полностью оборудованная кухня и две ванные комнаты. Максимальная скорость 32 узла позволяет быстро добраться до самых живописных мест Карельского перешейка.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Сансикер Манхэттен 52',
|
name: 'Sunseeker Manhattan 52',
|
||||||
length: 15.8,
|
length: 15.8,
|
||||||
speed: 34,
|
speed: 34,
|
||||||
minCost: 92000,
|
minCost: 92000,
|
||||||
mainImageUrl: 'uploads/2nd.jpg',
|
mainImageUrl: '/api/uploads/1765728068963-634185622.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728068963-634185622.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: false,
|
hasQuickRent: false,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
topText: '🔥 Лучшее предложение',
|
topText: '🔥 Лучшее предложение',
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 4.5,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1400,
|
|
||||||
description:
|
|
||||||
'Британский шик и русская душа в одной яхте! Сансикер Манхэттен 52 - выбор настоящих ценителей морских путешествий. Просторный кокпит с мягкими диванами, бар на 8 персон, система мультимедиа премиум-класса. Идеальна для празднования дня рождения на воде или деловой встречи с партнерами. Отличная маневренность позволяет заходить в марины Санкт-Петербурга и Кронштадта.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'Принцесс V55',
|
name: 'Princess V55',
|
||||||
length: 16.7,
|
length: 16.7,
|
||||||
speed: 33,
|
speed: 33,
|
||||||
minCost: 78000,
|
minCost: 78000,
|
||||||
mainImageUrl: 'uploads/3rd.jpg',
|
mainImageUrl: '/api/uploads/1765728152179-305707681.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728152179-305707681.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
topText: '🍷 Идеальна для заката с бокалом вина',
|
topText: '🍷 Идеальна для заката с бокалом вина',
|
||||||
year: 2021,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 12,
|
|
||||||
width: 4.7,
|
|
||||||
cabinsCount: 4,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1100,
|
|
||||||
description:
|
|
||||||
'Принцесс V55 - королева российских вод! Эта яхта создана для тех, кто ценит комфорт и элегантность. Четыре уютные каюты с кондиционером, гальюн с душем, полностью оборудованная камбузная зона. Особенность - огромный платц с гидравлическим трапом для купания в Ладожском озере. Отличный выбор для семейного отдыха или рыбалки с друзьями.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'Ферретти 500',
|
name: 'Ferretti 500',
|
||||||
length: 15.2,
|
length: 15.2,
|
||||||
speed: 31,
|
speed: 31,
|
||||||
minCost: 68000,
|
minCost: 68000,
|
||||||
mainImageUrl: 'uploads/4th.jpg',
|
mainImageUrl: '/api/uploads/1765728169750-667674217.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728169750-667674217.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
topText: '⏳ Часто бронируется - успей',
|
topText: '⏳ Часто бронируется - успей',
|
||||||
year: 2020,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 4.3,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 900,
|
|
||||||
description:
|
|
||||||
'Итальянская страсть в русской стихии! Ферретти 500 сочетает в себе средиземноморский шарм и надежность для суровых условий Балтики. Просторный салон с панорамными окнами, обеденная зона на 6 человек, современная навигационная система. Идеально подходит для фотосессий на фоне разводных мостов Петербурга или романтического ужина под звуки волн.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'Си Рей 510 Сандансер',
|
name: 'Sea Ray 510 Sundancer',
|
||||||
length: 15.5,
|
length: 15.5,
|
||||||
speed: 35,
|
speed: 35,
|
||||||
minCost: 72000,
|
minCost: 72000,
|
||||||
mainImageUrl: 'uploads/5th.jpg',
|
mainImageUrl: '/api/uploads/1765728190017-763752836.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728190017-763752836.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: false,
|
hasQuickRent: false,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 4.6,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1300,
|
|
||||||
description:
|
|
||||||
'Американская мощь для русского моря! Си Рей 510 Сандансер - самая быстрая яхта в нашем флоте. Развивает скорость до 35 узлов, что позволяет за день обогнуть весь Финский залив. Три комфортабельные каюты, система стабилизации на стоянке, мощная аудиосистема с сабвуфером. Отличный выбор для любителей острых ощущений и скоростных прогулок.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
name: 'Бавария SR41',
|
name: 'Bavaria SR41',
|
||||||
length: 12.5,
|
length: 12.5,
|
||||||
speed: 28,
|
speed: 28,
|
||||||
minCost: 45000,
|
minCost: 45000,
|
||||||
mainImageUrl: 'uploads/6th.jpg',
|
mainImageUrl: '/api/uploads/1765728208376-854378188.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728208376-854378188.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2019,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 3.9,
|
|
||||||
cabinsCount: 2,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 320,
|
|
||||||
description:
|
|
||||||
'Немецкое качество для русского характера! Бавария SR41 - надежная и экономичная яхта для спокойных прогулок по Ладоге. Две уютные каюты, просторный кокпит с тентом от дождя, лебедка для подъема парусов. Идеальный выбор для начинающих яхтсменов или семейного отдыха с детьми. Расход топлива всего 15 литров в час!',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
name: 'Жанно Мери Фишер 895',
|
name: 'Jeanneau Merry Fisher 895',
|
||||||
length: 8.9,
|
length: 8.9,
|
||||||
speed: 25,
|
speed: 25,
|
||||||
minCost: 32000,
|
minCost: 32000,
|
||||||
mainImageUrl: 'uploads/1st.jpg',
|
mainImageUrl: '/api/uploads/1765728227998-695635756.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728227998-695635756.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2022,
|
|
||||||
comfortCapacity: 4,
|
|
||||||
maxCapacity: 6,
|
|
||||||
width: 3.0,
|
|
||||||
cabinsCount: 1,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 250,
|
|
||||||
description:
|
|
||||||
'Французская элегантность для русского простора! Жанно Мери Фишер 895 - компактная, но вместительная яхта для рыбалки и пикников. Одна просторная каюта, открытый кокпит, столик для барбекю. Отлично подходит для выездов на природу, ночевки в бухтах или обучения детей управлению яхтой. Самый экономичный вариант в нашем флоте!',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
name: 'Бенето Свифт Троулер 41',
|
name: 'Beneteau Swift Trawler 41',
|
||||||
length: 12.5,
|
length: 12.5,
|
||||||
speed: 22,
|
speed: 22,
|
||||||
minCost: 55000,
|
minCost: 55000,
|
||||||
mainImageUrl: 'uploads/2nd.jpg',
|
mainImageUrl: '/api/uploads/1765728068963-634185622.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728068963-634185622.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: false,
|
hasQuickRent: false,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2021,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 4.2,
|
|
||||||
cabinsCount: 2,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 425,
|
|
||||||
description:
|
|
||||||
'Французский траулер для русского севера! Бенето Свифт Троулер 41 создан для длительных путешествий по Белому морю. Экономичный дизельный двигатель, большой запас топлива, система опреснения воды. Две комфортабельные каюты с подогревом пола. Идеальный выбор для экспедиций или многодневных круизов по северным островам.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
name: 'Лагун 450',
|
name: 'Lagoon 450',
|
||||||
length: 13.5,
|
length: 13.5,
|
||||||
speed: 20,
|
speed: 20,
|
||||||
minCost: 65000,
|
minCost: 65000,
|
||||||
mainImageUrl: 'uploads/3rd.jpg',
|
mainImageUrl: '/api/uploads/1765728152179-305707681.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728152179-305707681.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2020,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 7.8,
|
|
||||||
cabinsCount: 4,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 90,
|
|
||||||
description:
|
|
||||||
'Французский катамаран для русского размаха! Лагун 450 - невероятно устойчивая и просторная яхта. Четыре отдельные каюты с санузлами, огромный салон-трансформер, две кухни. Идеально подходит для больших компаний, свадебных церемоний на воде или длительных круизов всей семьей. Не кренится даже в шторм!',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
name: 'Фонтен Пажо Люсия 40',
|
name: 'Fountaine Pajot Lucia 40',
|
||||||
length: 11.7,
|
length: 11.7,
|
||||||
speed: 18,
|
speed: 18,
|
||||||
minCost: 58000,
|
minCost: 58000,
|
||||||
mainImageUrl: 'uploads/4th.jpg',
|
mainImageUrl: '/api/uploads/1765728169750-667674217.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728169750-667674217.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 7.1,
|
|
||||||
cabinsCount: 4,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 80,
|
|
||||||
description:
|
|
||||||
'Французский катамаран класса люкс! Фонтен Пажо Люсия 40 - это плавающий пятизвездочный отель. Четыре каюты-люкс с джакузи, салон с камином, профессиональная кухня с шеф-поваром. Система стабилизации на якоре, гидромассажный бассейн на палубе. Выбор настоящих ценителей роскоши и комфорта на воде.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
name: 'Дюфур 460',
|
name: 'Dufour 460',
|
||||||
length: 14.1,
|
length: 14.1,
|
||||||
speed: 26,
|
speed: 26,
|
||||||
minCost: 62000,
|
minCost: 62000,
|
||||||
mainImageUrl: 'uploads/5th.jpg',
|
mainImageUrl: '/api/uploads/1765728190017-763752836.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728190017-763752836.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: false,
|
hasQuickRent: false,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2022,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 4.5,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 380,
|
|
||||||
description:
|
|
||||||
'Французская парусная яхта для русского ветра! Дюфур 460 - мечта любого яхтсмена. Три просторные каюты, кокпит с мягкими сиденьями, современное парусное вооружение. Идеально сбалансированная, легко управляется даже новичками. Отличный выбор для регат, обучения парусному спорту или романтических круизов под парусами.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
name: 'Гранд Бэнкс 60',
|
name: 'Grand Banks 60',
|
||||||
length: 18.3,
|
length: 18.3,
|
||||||
speed: 24,
|
speed: 24,
|
||||||
minCost: 125000,
|
minCost: 125000,
|
||||||
mainImageUrl: 'uploads/6th.jpg',
|
mainImageUrl: '/api/uploads/1765728208376-854378188.webp',
|
||||||
galleryUrls: [
|
galleryUrls: [
|
||||||
'uploads/gal1.jpg',
|
'/api/uploads/1765728208376-854378188.webp',
|
||||||
'uploads/gal2.jpg',
|
'/api/uploads/1765727637471-474231444.webp',
|
||||||
'uploads/gal3.jpg',
|
'/api/uploads/1765727743466-187581713.webp',
|
||||||
'uploads/gal4.jpg',
|
'/api/uploads/1765727819793-158598111.webp',
|
||||||
'uploads/gal5.jpg',
|
'/api/uploads/1765727903961-559930753.webp',
|
||||||
'uploads/gal6.jpg',
|
'/api/uploads/1765727928577-55944164.webp',
|
||||||
'uploads/gal7.jpg',
|
'/api/uploads/1765727948691-389175954.webp',
|
||||||
'uploads/gal8.jpg',
|
'/api/uploads/1765727961591-243584836.webp',
|
||||||
'uploads/gal9.jpg',
|
'/api/uploads/1765727992064-37229339.webp',
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
],
|
||||||
hasQuickRent: true,
|
hasQuickRent: true,
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 5.2,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1600,
|
|
||||||
description:
|
|
||||||
'Американская легенда для русского океана! Гранд Бэнкс 60 - экспедиционная яхта для самых смелых путешествий. Три каюты-люкс, салон с библиотекой, зимний сад, сауна. Автономность плавания - 30 дней! Способна пересечь Баренцево море и дойти до Шпицбергена. Выбор для настоящих морских волков и исследователей Арктики.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
private toShortDto(item: CatalogItemLongDto): CatalogItemShortDto {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
length,
|
|
||||||
speed,
|
|
||||||
minCost,
|
|
||||||
mainImageUrl,
|
|
||||||
galleryUrls,
|
|
||||||
hasQuickRent,
|
|
||||||
isFeatured,
|
|
||||||
topText,
|
|
||||||
} = item;
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
length,
|
|
||||||
speed,
|
|
||||||
minCost,
|
|
||||||
mainImageUrl,
|
|
||||||
galleryUrls,
|
|
||||||
hasQuickRent,
|
|
||||||
isFeatured,
|
|
||||||
topText,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCatalogItemById(id: number): Promise<CatalogItemLongDto | null> {
|
|
||||||
const item = this.catalogItems.find((item) => item.id === id);
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
const ownerId = [1, 2][(id - 1) % 2];
|
|
||||||
const owner = await this.usersService.findById(ownerId);
|
|
||||||
|
|
||||||
if (owner) {
|
|
||||||
item.owner = owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.reviews = this.reviewsService.getReviewsByYachtId(id);
|
|
||||||
item.reservations = this.reservationsService.getReservationsByYachtId(id);
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
|
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
|
||||||
const clonedCatalog = [...this.catalogItems];
|
const clonedCatalog = [...this.catalogItems];
|
||||||
const filteredCatalog = clonedCatalog.filter(
|
const filteredCatalog = clonedCatalog.filter(
|
||||||
({ isFeatured }) => !isFeatured,
|
({ isFeatured }) => !isFeatured,
|
||||||
);
|
);
|
||||||
|
|
||||||
const featuredYachtIndex = clonedCatalog.findIndex(
|
const featuredYacthIndex = clonedCatalog.findIndex(
|
||||||
(item) => item.isFeatured === true,
|
(item) => item.isFeatured === true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (featuredYachtIndex !== -1) {
|
if (featuredYacthIndex !== -1) {
|
||||||
const minCost = Math.min(
|
|
||||||
...filteredCatalog.map((item) => item.minCost || Infinity),
|
|
||||||
);
|
|
||||||
|
|
||||||
const mappedRestYachts = filteredCatalog.slice(0, 6).map((item) => ({
|
|
||||||
...this.toShortDto(item),
|
|
||||||
isBestOffer: item.minCost === minCost,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
featuredYacht: this.toShortDto(clonedCatalog[featuredYachtIndex]),
|
featuredYacht: clonedCatalog[featuredYacthIndex],
|
||||||
restYachts: mappedRestYachts,
|
restYachts: filteredCatalog.slice(0, 6),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCatalog(filters?: CatalogFiltersDto): Promise<CatalogResponseDto> {
|
async getCatalog(params?: CatalogParamsDto): Promise<CatalogResponseDto> {
|
||||||
// Start with all items converted to short DTO
|
let filteredItems = [...this.catalogItems];
|
||||||
let filteredItems = this.catalogItems.map((item) => this.toShortDto(item));
|
|
||||||
|
|
||||||
// Get the full items for filtering by properties not in short DTO
|
if (params?.search) {
|
||||||
const fullItems = this.catalogItems;
|
const searchTerm = params.search.toLowerCase();
|
||||||
|
|
||||||
|
filteredItems = filteredItems.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(searchTerm),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.filter?.price) {
|
||||||
|
const { minvalue, maxvalue } = params.filter.price;
|
||||||
|
|
||||||
// Search filter
|
|
||||||
if (filters?.search) {
|
|
||||||
const searchTerm = filters.search.toLowerCase();
|
|
||||||
filteredItems = filteredItems.filter((item) => {
|
filteredItems = filteredItems.filter((item) => {
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
if (minvalue !== undefined && item.minCost < minvalue) return false;
|
||||||
return (
|
if (maxvalue !== undefined && item.minCost > maxvalue) return false;
|
||||||
item.name.toLowerCase().includes(searchTerm) ||
|
|
||||||
(fullItem && fullItem.description.toLowerCase().includes(searchTerm))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Length filter
|
|
||||||
if (filters?.minLength !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter(
|
|
||||||
(item) => item.length >= filters.minLength!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filters?.maxLength !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter(
|
|
||||||
(item) => item.length <= filters.maxLength!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Price filter
|
|
||||||
if (filters?.minPrice !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter(
|
|
||||||
(item) => item.minCost >= filters.minPrice!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filters?.maxPrice !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter(
|
|
||||||
(item) => item.minCost <= filters.maxPrice!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Year filter
|
|
||||||
if (filters?.minYear !== undefined || filters?.maxYear !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter((item) => {
|
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
|
||||||
if (!fullItem) return false;
|
|
||||||
|
|
||||||
if (filters.minYear !== undefined && fullItem.year < filters.minYear)
|
|
||||||
return false;
|
|
||||||
if (filters.maxYear !== undefined && fullItem.year > filters.maxYear)
|
|
||||||
return false;
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters?.guests !== undefined) {
|
if (params?.sort?.field) {
|
||||||
const totalPeople = filters?.guests!;
|
const { field, direction = 'asc' } = params.sort;
|
||||||
filteredItems = filteredItems.filter((item) => {
|
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
filteredItems.sort((a, b) => {
|
||||||
return fullItem && fullItem.maxCapacity >= totalPeople;
|
let valueA = a[field];
|
||||||
|
let valueB = b[field];
|
||||||
|
|
||||||
|
if (typeof valueA === 'string') valueA = valueA.toLowerCase();
|
||||||
|
if (typeof valueB === 'string') valueB = valueB.toLowerCase();
|
||||||
|
|
||||||
|
if (valueA < valueB) return direction === 'asc' ? -1 : 1;
|
||||||
|
if (valueA > valueB) return direction === 'asc' ? 1 : -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick booking filter
|
|
||||||
if (filters?.quickBooking !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter(
|
|
||||||
(item) => item.hasQuickRent === filters.quickBooking,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has toilet filter (check if cabinsCount > 0)
|
|
||||||
if (filters?.hasToilet !== undefined) {
|
|
||||||
filteredItems = filteredItems.filter((item) => {
|
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
|
||||||
if (!fullItem) return false;
|
|
||||||
return filters.hasToilet
|
|
||||||
? fullItem.cabinsCount > 0
|
|
||||||
: fullItem.cabinsCount === 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payment type filter (assuming all accept card payments)
|
|
||||||
if (filters?.paymentType) {
|
|
||||||
// For now, all yachts accept card payments
|
|
||||||
// You could add a paymentTypes array to CatalogItemLongDto if needed
|
|
||||||
console.log('Payment type filtering:', filters.paymentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date/time filter (check reservations)
|
|
||||||
if (filters?.date || filters?.departureTime || filters?.arrivalTime) {
|
|
||||||
// This is more complex - would need to check reservations
|
|
||||||
// For now, we'll return all items
|
|
||||||
console.log('Date/time filtering available for future implementation');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters?.sortByPrice === 'asc') {
|
|
||||||
filteredItems.sort((a, b) => a.minCost - b.minCost);
|
|
||||||
} else if (filters?.sortByPrice === 'desc') {
|
|
||||||
filteredItems.sort((a, b) => b.minCost - a.minCost);
|
|
||||||
} else {
|
|
||||||
filteredItems.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: filteredItems,
|
items: filteredItems,
|
||||||
total: filteredItems.length,
|
total: filteredItems.length,
|
||||||
filters,
|
filters: params?.filter,
|
||||||
search: filters?.search,
|
sort: params?.sort,
|
||||||
|
search: params?.search,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
|
async getAllCatalogItems(): Promise<CatalogItemDto[]> {
|
||||||
return this.catalogItems.map((item) => this.toShortDto(item));
|
return this.catalogItems;
|
||||||
}
|
|
||||||
|
|
||||||
async getCatalogByUserId(userId: number): Promise<CatalogItemShortDto[]> {
|
|
||||||
// Логика определения ownerId: нечетные id -> ownerId=1, четные id -> ownerId=2
|
|
||||||
const filteredItems = this.catalogItems.filter((item) => {
|
|
||||||
return item.owner?.userId === userId;
|
|
||||||
});
|
|
||||||
return filteredItems.map((item) => this.toShortDto(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
async createYacht(createYachtDto: CreateYachtDto): Promise<CatalogItemLongDto> {
|
|
||||||
const newId = Math.max(...this.catalogItems.map((item) => item.id || 0), 0) + 1;
|
|
||||||
const owner = await this.usersService.findById(createYachtDto.userId);
|
|
||||||
|
|
||||||
const newYacht: CatalogItemLongDto = {
|
|
||||||
id: newId,
|
|
||||||
name: createYachtDto.name,
|
|
||||||
length: createYachtDto.length,
|
|
||||||
speed: createYachtDto.speed,
|
|
||||||
minCost: createYachtDto.minCost,
|
|
||||||
mainImageUrl: createYachtDto.mainImageUrl,
|
|
||||||
galleryUrls: createYachtDto.galleryUrls,
|
|
||||||
hasQuickRent: createYachtDto.hasQuickRent,
|
|
||||||
isFeatured: createYachtDto.isFeatured,
|
|
||||||
year: createYachtDto.year,
|
|
||||||
comfortCapacity: createYachtDto.comfortCapacity,
|
|
||||||
maxCapacity: createYachtDto.maxCapacity,
|
|
||||||
width: createYachtDto.width,
|
|
||||||
cabinsCount: createYachtDto.cabinsCount,
|
|
||||||
matherial: createYachtDto.matherial,
|
|
||||||
power: createYachtDto.power,
|
|
||||||
description: createYachtDto.description,
|
|
||||||
owner: owner || ({ userId: createYachtDto.userId } as any),
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
topText: createYachtDto.topText,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.catalogItems.push(newYacht);
|
|
||||||
return newYacht;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import {
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
IsNumber,
|
|
||||||
IsBoolean,
|
|
||||||
IsDate,
|
|
||||||
Min,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
export class CatalogFiltersDto {
|
|
||||||
@ApiProperty({ required: false, description: 'Search by yacht name' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
search?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Minimum length in meters',
|
|
||||||
example: 10,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Type(() => Number)
|
|
||||||
minLength?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Maximum length in meters',
|
|
||||||
example: 20,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Type(() => Number)
|
|
||||||
maxLength?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Minimum price',
|
|
||||||
example: 30000,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Type(() => Number)
|
|
||||||
minPrice?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Maximum price',
|
|
||||||
example: 100000,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Type(() => Number)
|
|
||||||
maxPrice?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Sort by price',
|
|
||||||
example: 100000,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Type(() => Number)
|
|
||||||
sortByPrice?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Minimum year', example: 2019 })
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1900)
|
|
||||||
@Type(() => Number)
|
|
||||||
minYear?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Maximum year', example: 2023 })
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1900)
|
|
||||||
@Type(() => Number)
|
|
||||||
maxYear?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Number of guests', example: 4 })
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
@Type(() => Number)
|
|
||||||
guests?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Payment type',
|
|
||||||
example: 'card',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
paymentType?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Quick booking available',
|
|
||||||
example: true,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@Type(() => Boolean)
|
|
||||||
quickBooking?: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Has toilet', example: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@Type(() => Boolean)
|
|
||||||
hasToilet?: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Departure date',
|
|
||||||
example: '2025-12-20',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsDate()
|
|
||||||
@Type(() => Date)
|
|
||||||
date?: Date;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Departure time',
|
|
||||||
example: '08:00',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
departureTime?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
description: 'Arrival time',
|
|
||||||
example: '20:00',
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
arrivalTime?: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +1,12 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
export class CatalogItemDto {
|
||||||
export class UserDto {
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
userId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Иван' })
|
|
||||||
firstName: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Андреев' })
|
|
||||||
lastName: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: '+79009009090' })
|
|
||||||
phone: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'ivan@yachting.ru' })
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Северный Флот', required: false })
|
|
||||||
companyName?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1234567890, required: false })
|
|
||||||
inn?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1122334455667, required: false })
|
|
||||||
ogrn?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReviewDto {
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
reviewerId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
yachtId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 5 })
|
|
||||||
starsCount: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Excellent yacht!' })
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReservationDto {
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
yachtId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
reservatorId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1733097600 })
|
|
||||||
startUtc: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1733133600 })
|
|
||||||
endUtc: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CatalogItemShortDto {
|
|
||||||
@ApiProperty({ example: 1 })
|
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@ApiProperty({ example: 'Азимут 55' })
|
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 16.7 })
|
|
||||||
length: number;
|
length: number;
|
||||||
|
|
||||||
@ApiProperty({ example: 32 })
|
|
||||||
speed: number;
|
speed: number;
|
||||||
|
|
||||||
@ApiProperty({ example: 85000 })
|
|
||||||
minCost: number;
|
minCost: number;
|
||||||
|
|
||||||
@ApiProperty({ example: 'api/uploads/1765727362318-238005198.jpg' })
|
|
||||||
mainImageUrl: string;
|
mainImageUrl: string;
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
type: [String],
|
|
||||||
example: ['api/uploads/1765727362318-238005198.jpg'],
|
|
||||||
})
|
|
||||||
galleryUrls: string[];
|
galleryUrls: string[];
|
||||||
|
|
||||||
@ApiProperty({ example: true })
|
|
||||||
hasQuickRent: boolean;
|
hasQuickRent: boolean;
|
||||||
|
|
||||||
@ApiProperty({ example: true })
|
|
||||||
isFeatured: boolean;
|
isFeatured: boolean;
|
||||||
|
|
||||||
@ApiProperty({ required: false, example: '🔥 Лучшее предложение' })
|
|
||||||
topText?: string;
|
topText?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, example: true })
|
|
||||||
isBestOffer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CatalogItemLongDto extends CatalogItemShortDto {
|
|
||||||
@ApiProperty({ example: 2022 })
|
|
||||||
year: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 8 })
|
|
||||||
comfortCapacity: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 12 })
|
|
||||||
maxCapacity: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 4.8 })
|
|
||||||
width: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 3 })
|
|
||||||
cabinsCount: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Стеклопластик' })
|
|
||||||
matherial: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1200 })
|
|
||||||
power: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Роскошная моторная яхта...' })
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@ApiProperty({ type: UserDto })
|
|
||||||
owner: UserDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: [ReviewDto] })
|
|
||||||
reviews: ReviewDto[];
|
|
||||||
|
|
||||||
@ApiProperty({ type: [ReservationDto] })
|
|
||||||
reservations: ReservationDto[];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,11 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { CatalogItemDto } from './catalog-item.dto';
|
||||||
import { CatalogItemShortDto } from './catalog-item.dto';
|
|
||||||
import { CatalogFiltersDto } from './catalog-filters.dto';
|
|
||||||
|
|
||||||
export class CatalogResponseDto {
|
export class CatalogResponseDto {
|
||||||
@ApiProperty({ type: [CatalogItemShortDto] })
|
items: CatalogItemDto[];
|
||||||
items: CatalogItemShortDto[];
|
|
||||||
|
|
||||||
@ApiProperty({ example: 42 })
|
|
||||||
total: number;
|
total: number;
|
||||||
|
|
||||||
@ApiProperty({ required: false, example: 1 })
|
|
||||||
page?: number;
|
page?: number;
|
||||||
|
|
||||||
@ApiProperty({ required: false, example: 10 })
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
filters?: any;
|
||||||
@ApiProperty({ type: CatalogFiltersDto, required: false })
|
|
||||||
filters?: CatalogFiltersDto;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
example: { field: 'name', direction: 'asc' },
|
|
||||||
})
|
|
||||||
sort?: any;
|
sort?: any;
|
||||||
|
|
||||||
@ApiProperty({ required: false, example: 'yacht' })
|
|
||||||
search?: string;
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export class CreateYachtDto {
|
|
||||||
@ApiProperty({ example: 'Азимут 55', description: 'Название яхты' })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 16.7, description: 'Длина яхты в метрах' })
|
|
||||||
length: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 32, description: 'Скорость в узлах' })
|
|
||||||
speed: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 85000, description: 'Минимальная стоимость аренды' })
|
|
||||||
minCost: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: 'api/uploads/1765727362318-238005198.jpg',
|
|
||||||
description: 'URL главного изображения',
|
|
||||||
})
|
|
||||||
mainImageUrl: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
type: [String],
|
|
||||||
example: ['api/uploads/1765727362318-238005198.jpg'],
|
|
||||||
description: 'URL галереи изображений',
|
|
||||||
})
|
|
||||||
galleryUrls: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ example: true, description: 'Доступна быстрая аренда' })
|
|
||||||
hasQuickRent: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ example: false, description: 'Рекомендуемая яхта' })
|
|
||||||
isFeatured: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 2022, description: 'Год выпуска' })
|
|
||||||
year: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 8, description: 'Комфортная вместимость' })
|
|
||||||
comfortCapacity: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 12, description: 'Максимальная вместимость' })
|
|
||||||
maxCapacity: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 4.8, description: 'Ширина в метрах' })
|
|
||||||
width: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 3, description: 'Количество кают' })
|
|
||||||
cabinsCount: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Стеклопластик', description: 'Материал корпуса' })
|
|
||||||
matherial: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1200, description: 'Мощность двигателя' })
|
|
||||||
power: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: 'Роскошная моторная яхта...',
|
|
||||||
description: 'Описание яхты',
|
|
||||||
})
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1, description: 'ID владельца яхты' })
|
|
||||||
userId: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
required: false,
|
|
||||||
example: '🔥 Лучшее предложение',
|
|
||||||
description: 'Текст для отображения сверху',
|
|
||||||
})
|
|
||||||
topText?: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { CatalogItemShortDto } from './catalog-item.dto';
|
import { CatalogItemDto } from './catalog-item.dto';
|
||||||
|
|
||||||
export class MainPageCatalogResponseDto {
|
export class MainPageCatalogResponseDto {
|
||||||
featuredYacht: CatalogItemShortDto;
|
featuredYacht: CatalogItemDto;
|
||||||
restYachts: CatalogItemShortDto[];
|
restYachts: CatalogItemDto[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ async function bootstrap() {
|
||||||
app.setGlobalPrefix('');
|
app.setGlobalPrefix('');
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: ['http://localhost:3000', "http://travelmarine.ru", "https://travelmarine.ru"],
|
origin: ['http://localhost:3000'],
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
allowedHeaders: 'Content-Type, Authorization, X-Requested-With',
|
allowedHeaders: 'Content-Type, Authorization, X-Requested-With',
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto';
|
|
||||||
|
|
||||||
export class ReservationItemDto {
|
|
||||||
@ApiProperty({ example: 1, description: 'ID яхты' })
|
|
||||||
yachtId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1, description: 'ID резерватора' })
|
|
||||||
reservatorId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1733097600, description: 'Начало резервации (Unix timestamp в UTC)' })
|
|
||||||
startUtc: number;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 1733133600, description: 'Конец резервации (Unix timestamp в UTC)' })
|
|
||||||
endUtc: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReservationWithYachtDto extends ReservationItemDto {
|
|
||||||
@ApiProperty({ example: 1, description: 'ID резервации' })
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
type: CatalogItemLongDto,
|
|
||||||
required: false,
|
|
||||||
description: 'Данные о яхте',
|
|
||||||
})
|
|
||||||
yacht?: CatalogItemLongDto;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
|
||||||
import { ApiOperation, ApiResponse, ApiBody, ApiParam } from '@nestjs/swagger';
|
|
||||||
import { ReservationsService } from './reservations.service';
|
|
||||||
import { ReservationItemDto, ReservationWithYachtDto } from './reservation-item.dto';
|
|
||||||
|
|
||||||
@Controller('reservations')
|
|
||||||
export class ReservationsController {
|
|
||||||
constructor(private readonly reservationsService: ReservationsService) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@ApiOperation({ summary: 'Создать новую резервацию' })
|
|
||||||
@ApiBody({ type: ReservationItemDto })
|
|
||||||
@ApiResponse({ status: 201, description: 'Резервация успешно создана' })
|
|
||||||
create(@Body() dto: ReservationItemDto) {
|
|
||||||
return this.reservationsService.createReservation(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('user/:userId')
|
|
||||||
@ApiOperation({ summary: 'Получить резервации по ID пользователя' })
|
|
||||||
@ApiParam({ name: 'userId', description: 'ID пользователя', type: Number })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Список резерваций пользователя с данными о яхтах',
|
|
||||||
type: [ReservationWithYachtDto],
|
|
||||||
})
|
|
||||||
async findByUserId(@Param('userId') userId: string) {
|
|
||||||
return this.reservationsService.getReservationsByUserId(Number(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('yacht/:yachtId')
|
|
||||||
findByYachtId(@Param('yachtId') yachtId: string) {
|
|
||||||
return this.reservationsService.getReservationsByYachtId(Number(yachtId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
findAll() {
|
|
||||||
return this.reservationsService.getAllReservations();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { Module, forwardRef } from '@nestjs/common';
|
|
||||||
import { ReservationsService } from './reservations.service';
|
|
||||||
import { ReservationsController } from './reservations.controller';
|
|
||||||
import { CatalogModule } from '../catalog/catalog.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [forwardRef(() => CatalogModule)],
|
|
||||||
controllers: [ReservationsController],
|
|
||||||
providers: [ReservationsService],
|
|
||||||
exports: [ReservationsService], // Export for other modules to use
|
|
||||||
})
|
|
||||||
export class ReservationsModule {}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
|
||||||
import { ReservationItemDto } from './reservation-item.dto';
|
|
||||||
import { CatalogService } from '../catalog/catalog.service';
|
|
||||||
import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto';
|
|
||||||
|
|
||||||
export interface Reservation extends ReservationItemDto {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReservationWithYacht extends Reservation {
|
|
||||||
yacht?: CatalogItemLongDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ReservationsService {
|
|
||||||
constructor(
|
|
||||||
@Inject(forwardRef(() => CatalogService))
|
|
||||||
private readonly catalogService: CatalogService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private reservations: Reservation[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
yachtId: 1,
|
|
||||||
reservatorId: 1,
|
|
||||||
// Corrected: Jan 1, 2026 20:00 UTC to Jan 2, 2026 08:00 UTC
|
|
||||||
startUtc: 1767369600, // Jan 1, 2026 20:00:00 UTC
|
|
||||||
endUtc: 1767412800, // Jan 2, 2026 08:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
yachtId: 3,
|
|
||||||
reservatorId: 2,
|
|
||||||
// Jan 3, 2026 08:00 UTC to Jan 5, 2026 20:00 UTC
|
|
||||||
startUtc: 1767484800, // Jan 3, 2026 08:00:00 UTC
|
|
||||||
endUtc: 1767715200, // Jan 5, 2026 20:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
yachtId: 5,
|
|
||||||
reservatorId: 1,
|
|
||||||
// Jan 10, 2026 20:00 UTC to Jan 12, 2026 08:00 UTC
|
|
||||||
startUtc: 1768070400, // Jan 10, 2026 20:00:00 UTC
|
|
||||||
endUtc: 1768176000, // Jan 12, 2026 08:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
yachtId: 7,
|
|
||||||
reservatorId: 2,
|
|
||||||
// Jan 15, 2026 08:00 UTC to Jan 17, 2026 20:00 UTC
|
|
||||||
startUtc: 1768435200, // Jan 15, 2026 08:00:00 UTC
|
|
||||||
endUtc: 1768684800, // Jan 17, 2026 20:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
yachtId: 9,
|
|
||||||
reservatorId: 1,
|
|
||||||
// Jan 20, 2026 20:00 UTC to Jan 22, 2026 08:00 UTC
|
|
||||||
startUtc: 1768944000, // Jan 20, 2026 20:00:00 UTC
|
|
||||||
endUtc: 1769049600, // Jan 22, 2026 08:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
yachtId: 11,
|
|
||||||
reservatorId: 2,
|
|
||||||
// Jan 25, 2026 08:00 UTC to Jan 27, 2026 20:00 UTC
|
|
||||||
startUtc: 1769385600, // Jan 25, 2026 08:00:00 UTC
|
|
||||||
endUtc: 1769635200, // Jan 27, 2026 20:00:00 UTC
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
private idCounter = 7;
|
|
||||||
|
|
||||||
createReservation(dto: ReservationItemDto): Reservation {
|
|
||||||
const reservation = {
|
|
||||||
id: this.idCounter++,
|
|
||||||
...dto,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.reservations.push(reservation);
|
|
||||||
return reservation;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getReservationsByUserId(userId: number): Promise<ReservationWithYacht[]> {
|
|
||||||
const reservations = this.reservations.filter((r) => r.reservatorId === userId);
|
|
||||||
|
|
||||||
// Populate данные по яхте для каждой резервации
|
|
||||||
const reservationsWithYacht = await Promise.all(
|
|
||||||
reservations.map(async (reservation) => {
|
|
||||||
const yacht = await this.catalogService.getCatalogItemById(reservation.yachtId);
|
|
||||||
return {
|
|
||||||
...reservation,
|
|
||||||
yacht: yacht || undefined,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return reservationsWithYacht;
|
|
||||||
}
|
|
||||||
|
|
||||||
getReservationsByYachtId(yachtId: number): Reservation[] {
|
|
||||||
return this.reservations.filter((r) => r.yachtId === yachtId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllReservations(): Reservation[] {
|
|
||||||
return this.reservations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export class ReviewItemDto {
|
|
||||||
reviewerId: number;
|
|
||||||
yachtId: number;
|
|
||||||
starsCount: number;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
|
||||||
import { ReviewsService } from './reviews.service';
|
|
||||||
import { ReviewItemDto } from './review-item.dto';
|
|
||||||
|
|
||||||
@Controller('reviews')
|
|
||||||
export class ReviewsController {
|
|
||||||
constructor(private readonly reviewsService: ReviewsService) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
create(@Body() dto: ReviewItemDto) {
|
|
||||||
return this.reviewsService.createReview(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('user/:userId')
|
|
||||||
findByUserId(@Param('userId') userId: string) {
|
|
||||||
return this.reviewsService.getReviewsByUserId(Number(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('yacht/:yachtId')
|
|
||||||
findByYachtId(@Param('yachtId') yachtId: string) {
|
|
||||||
return this.reviewsService.getReviewsByYachtId(Number(yachtId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
findAll() {
|
|
||||||
return this.reviewsService.getAllReviews();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ReviewsService } from './reviews.service';
|
|
||||||
import { ReviewsController } from './reviews.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [ReviewsController],
|
|
||||||
providers: [ReviewsService],
|
|
||||||
exports: [ReviewsService], // Export for other modules to use
|
|
||||||
})
|
|
||||||
export class ReviewsModule {}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ReviewItemDto } from './review-item.dto';
|
|
||||||
|
|
||||||
export interface Review extends ReviewItemDto {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ReviewsService {
|
|
||||||
private reviews: Review[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 1,
|
|
||||||
starsCount: 5,
|
|
||||||
description: 'Excellent yacht!',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 1,
|
|
||||||
starsCount: 4,
|
|
||||||
description: 'Very good experience',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 3,
|
|
||||||
starsCount: 3,
|
|
||||||
description: 'Average condition',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 5,
|
|
||||||
starsCount: 5,
|
|
||||||
description: 'Perfect for sailing',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 7,
|
|
||||||
starsCount: 4,
|
|
||||||
description: 'Comfortable and fast',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 9,
|
|
||||||
starsCount: 2,
|
|
||||||
description: 'Needs maintenance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 11,
|
|
||||||
starsCount: 5,
|
|
||||||
description: 'Luxury experience',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 12,
|
|
||||||
starsCount: 4,
|
|
||||||
description: 'Great value for money',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
private idCounter = 9;
|
|
||||||
|
|
||||||
createReview(dto: ReviewItemDto): Review {
|
|
||||||
const review = {
|
|
||||||
id: this.idCounter++,
|
|
||||||
...dto,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.reviews.push(review);
|
|
||||||
return review;
|
|
||||||
}
|
|
||||||
|
|
||||||
getReviewsByUserId(userId: number): Review[] {
|
|
||||||
return this.reviews.filter((r) => r.reviewerId === userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getReviewsByYachtId(yachtId: number): Review[] {
|
|
||||||
return this.reviews.filter((r) => r.yachtId === yachtId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllReviews(): Review[] {
|
|
||||||
return this.reviews;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,4 @@ export type User = {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
yachts?: Yacht[];
|
yachts?: Yacht[];
|
||||||
companyName?: string;
|
|
||||||
inn?: number;
|
|
||||||
ogrn?: number;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@ import { UsersService } from './users.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UsersService],
|
providers: [UsersService],
|
||||||
exports: [UsersService], // This is important!
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
describe('UsersService', () => {
|
||||||
|
let service: UsersService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [UsersService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<UsersService>(UsersService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -9,47 +9,19 @@ export class UsersService {
|
||||||
private readonly users: User[] = [
|
private readonly users: User[] = [
|
||||||
{
|
{
|
||||||
userId: 1,
|
userId: 1,
|
||||||
firstName: 'Иван',
|
firstName: 'Ivan',
|
||||||
lastName: 'Андреев',
|
lastName: 'Andreev',
|
||||||
phone: '+79009009090',
|
phone: '+79009009090',
|
||||||
email: 'ivan@yachting.ru',
|
email: 'email@email.com',
|
||||||
password: 'admin',
|
password: 'admin',
|
||||||
companyName: 'Северный Флот',
|
|
||||||
inn: 1234567890,
|
|
||||||
ogrn: 1122334455667,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userId: 2,
|
userId: 2,
|
||||||
firstName: 'Сергей',
|
firstName: 'Sergey',
|
||||||
lastName: 'Большаков',
|
lastName: 'Bolshakov',
|
||||||
phone: '+79119119191',
|
phone: '+79009009090',
|
||||||
email: 'sergey@yachting.ru',
|
email: 'email1@email.com',
|
||||||
password: 'admin',
|
password: 'admin',
|
||||||
companyName: 'Балтийские Просторы',
|
|
||||||
inn: 9876543210,
|
|
||||||
ogrn: 9988776655443,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: 3,
|
|
||||||
firstName: 'Анна',
|
|
||||||
lastName: 'Петрова',
|
|
||||||
phone: '+79229229292',
|
|
||||||
email: 'anna@yachting.ru',
|
|
||||||
password: 'admin',
|
|
||||||
companyName: 'Ладожские Ветры',
|
|
||||||
inn: 5555555555,
|
|
||||||
ogrn: 3333444455556,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: 4,
|
|
||||||
firstName: 'Дмитрий',
|
|
||||||
lastName: 'Соколов',
|
|
||||||
phone: '+79339339393',
|
|
||||||
email: 'dmitry@yachting.ru',
|
|
||||||
password: 'admin',
|
|
||||||
companyName: 'Финский Залив',
|
|
||||||
inn: 1111222233,
|
|
||||||
ogrn: 7777888899990,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -60,7 +32,8 @@ export class UsersService {
|
||||||
const user = this.users.find((user) => user.email === email);
|
const user = this.users.find((user) => user.email === email);
|
||||||
|
|
||||||
if (user && includeYachts) {
|
if (user && includeYachts) {
|
||||||
user.yachts = [];
|
// Fetch yachts for this user
|
||||||
|
user.yachts = await [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|
@ -84,6 +57,7 @@ export class UsersService {
|
||||||
return this.users;
|
return this.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch all users with their yachts
|
||||||
const usersWithYachts = await Promise.all(
|
const usersWithYachts = await Promise.all(
|
||||||
this.users.map(async (user) => {
|
this.users.map(async (user) => {
|
||||||
const yachts = [];
|
const yachts = [];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { YachtsController } from './yacht.controller';
|
||||||
|
|
||||||
|
describe('YachtController', () => {
|
||||||
|
let controller: YachtsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [YachtsController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<YachtsController>(YachtsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { YachtsService } from './yachts.service';
|
||||||
|
|
||||||
|
describe('YachtsService', () => {
|
||||||
|
let service: YachtsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [YachtsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<YachtsService>(YachtsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 206 KiB |
BIN
uploads/1st.jpg
|
Before Width: | Height: | Size: 94 KiB |
BIN
uploads/2nd.jpg
|
Before Width: | Height: | Size: 125 KiB |
BIN
uploads/3rd.jpg
|
Before Width: | Height: | Size: 176 KiB |
BIN
uploads/4th.jpg
|
Before Width: | Height: | Size: 35 KiB |
BIN
uploads/6th.jpg
|
Before Width: | Height: | Size: 177 KiB |
BIN
uploads/gal1.jpg
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 134 KiB |
BIN
uploads/gal2.jpg
|
Before Width: | Height: | Size: 133 KiB |
BIN
uploads/gal3.jpg
|
Before Width: | Height: | Size: 167 KiB |
BIN
uploads/gal4.jpg
|
Before Width: | Height: | Size: 176 KiB |
BIN
uploads/gal5.jpg
|
Before Width: | Height: | Size: 166 KiB |
BIN
uploads/gal6.jpg
|
Before Width: | Height: | Size: 134 KiB |
BIN
uploads/gal7.jpg
|
Before Width: | Height: | Size: 167 KiB |
BIN
uploads/gal8.jpg
|
Before Width: | Height: | Size: 176 KiB |
BIN
uploads/gal9.jpg
|
Before Width: | Height: | Size: 166 KiB |