Steve Smith
Performer Controller Code Samples
import {
Controller,
Injectable,
HttpCode,
HttpStatus,
UsePipes,
ValidationPipe,
Get,
Param,
Query,
HttpException,
Put,
Body,
UseGuards,
Request,
Post
} from '@nestjs/common';
import { omit } from 'lodash';
import {
DataResponse,
PageableData
} from 'src/kernel';
import { Roles } from 'src/modules/auth';
import { AuthGuard, RoleGuard } from 'src/modules/auth/guards';
import { ContactPayload } from 'src/modules/contact/payloads';
import { MailerService } from 'src/modules/mailer';
import { PERFORMER_STATUSES, UNALLOWED_SELF_UPDATE_FIELDS } from '../constants';
import {
PerformerDto,
IPerformerResponse
} from '../dtos';
import {
PerformerSearchPayload, PerformerUpdatePayload
} from '../payloads';
import { PerformerService, PerformerSearchService } from '../services';
@Injectable()
@Controller('performers')
export class PerformerController {
constructor(
private readonly performerService: PerformerService,
private readonly performerSearchService: PerformerSearchService,
private readonly mailService: MailerService
) {}
@Get('/search')
@HttpCode(HttpStatus.OK)
@UsePipes(new ValidationPipe({ transform: true }))
async usearch(
@Query() req: PerformerSearchPayload
): Promise<DataResponse<PageableData<IPerformerResponse>>> {
req.status = 'active';
// default sort is score
// req.sort = 'popular';
const data = await this.performerSearchService.search(req);
return DataResponse.ok(data);
}
@Get('/me')
@HttpCode(HttpStatus.OK)
@Roles('performer')
@UseGuards(RoleGuard)
@UsePipes(new ValidationPipe({ transform: true }))
async getMyProfile(@Request() req: any): Promise<DataResponse<any>> {
const { authUser, jwtToken } = req;
const performer = await this.performerService.findByUserId(
authUser.sourceId
);
const details = await this.performerService.getDetails(performer._id, {
responseDocument: true,
jwtToken
});
if (details.status === PERFORMER_STATUSES.INACTIVE) {
throw new HttpException('This account is suspended', 403);
}
return DataResponse.ok(details.toResponse(true));
}
@Get('/:username')
@HttpCode(HttpStatus.OK)
async getDetails(
@Param('username') performerUsername: string
): Promise<DataResponse<Partial<PerformerDto>>> {
const data = await this.performerService.findByUsername(performerUsername);
if (data.status !== PERFORMER_STATUSES.ACTIVE) {
throw new HttpException('This account is suspended', 403);
}
return DataResponse.ok(data.toPublicDetailsResponse());
}
@Put('/')
@HttpCode(HttpStatus.OK)
@Roles('performer')
@UseGuards(RoleGuard)
@UsePipes(new ValidationPipe({ transform: true }))
async updateMyProfile(
@Body() payload: PerformerUpdatePayload,
@Request() req: any
): Promise<DataResponse<Partial<PerformerDto>>> {
const { sourceId } = req.authUser;
const performer = await this.performerService.findByUserId(sourceId);
const data = await this.performerService.update(
performer._id,
// remove unwanted fields
omit(payload, UNALLOWED_SELF_UPDATE_FIELDS)
);
return DataResponse.ok(data);
}
@Get('/:username/related')
@HttpCode(HttpStatus.OK)
async getRelated(
@Param('username') username: string
): Promise<DataResponse<any>> {
const data = await this.performerService.getRelated(username);
return DataResponse.ok(data);
}
@Post('/:username/contact')
@HttpCode(HttpStatus.OK)
@UseGuards(AuthGuard)
async contact(
@Param('username') username: string,
@Body() payload: ContactPayload
): Promise<DataResponse<any>> {
const performer = await this.performerService.findByUsername(username);
if (!performer || performer.status !== 'active') { return DataResponse.error(new Error("Model doesn't exists")); }
await this.mailService.send({
subject: `New contact from ${payload.name}`,
to: performer.email,
data: payload,
template: 'contact-performer'
});
return DataResponse.ok(true);
}
}
Performer Service Code Samples
import {
Injectable, forwardRef, Inject
} from '@nestjs/common';
import { Model } from 'mongoose';
import {
EntityNotFoundException, QueueEventService
} from 'src/kernel';
import { ObjectId } from 'mongodb';
import { FileService } from 'src/modules/file/services';
import { FileDto } from 'src/modules/file';
import { UserDto } from 'src/modules/user/dtos';
import { REF_TYPE } from 'src/modules/file/constants';
import { isObjectId } from 'src/kernel/helpers/string.helper';
import { CategoryService } from 'src/modules/category/services';
import { EVENT } from 'src/kernel/constants';
import { UserService } from 'src/modules/user/services';
import { PerformerDto } from '../dtos';
import {
UsernameExistedException
} from '../exceptions';
import {
PerformerModel
} from '../models';
import {
PerformerCreatePayload,
PerformerUpdatePayload
} from '../payloads';
import { PERFORMER_MODEL_PROVIDER } from '../providers';
import { PERFORMER_CHANNEL } from '../constants';
@Injectable()
export class PerformerService {
constructor(
@Inject(forwardRef(() => CategoryService))
private readonly categoryService: CategoryService,
@Inject(PERFORMER_MODEL_PROVIDER)
private readonly Performer: Model<PerformerModel>,
private readonly fileService: FileService,
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
private readonly queueEventService: QueueEventService
) {}
public async findById(id: string | ObjectId): Promise<PerformerDto> {
const model = await this.Performer.findById(id);
if (!model) return null;
return new PerformerDto(model);
}
public async findByEmail(email: string): Promise<PerformerDto> {
if (!email) {
return null;
}
const model = await this.Performer.findOne({
email: email.toLowerCase()
});
if (!model) return null;
return new PerformerDto(model);
}
public async create(
payload: PerformerCreatePayload,
user?: UserDto
): Promise<PerformerDto> {
const data = {
...payload,
updatedAt: new Date(),
createdAt: new Date()
} as any;
const userNameCheck = await this.Performer.countDocuments({
username: payload.username.trim()
});
if (userNameCheck) {
throw new UsernameExistedException();
}
const usernameCheck2 = await this.userService.findByUsername(
payload.username.trim()
);
if (usernameCheck2) {
throw new UsernameExistedException();
}
if (user) {
data.createdBy = user._id;
}
data.username = data.username.trim();
data.email = data.email ? data.email.toLowerCase() : null;
if (!data.name) data.name = data.username;
const performer = await this.Performer.create(data);
// update / create auth
await this.queueEventService.publish({
channel: PERFORMER_CHANNEL,
eventName: EVENT.CREATED,
data: {
performer: performer.toObject(),
payload
}
});
return new PerformerDto(performer);
}
public async update(
id: string | ObjectId,
payload: Partial<PerformerUpdatePayload>
): Promise<any> {
const performer = await this.Performer.findById(id);
if (!performer) {
throw new EntityNotFoundException();
}
const data = { ...payload, updatedAt: new Date() } as any;
if (!data.name) {
data.name = [data.firstName || '', data.lastName || ''].join(' ');
}
if (data.username && data.username.trim() !== performer.username) {
const usernameCheck = await this.Performer.countDocuments({
username: data.username.trim(),
_id: { $ne: performer._id }
});
if (usernameCheck) {
throw new UsernameExistedException();
}
const usernameCheck2 = await this.userService.findByUsername(
payload.username.trim()
);
if (usernameCheck2._id.toString() !== performer.userId?.toString()) {
throw new UsernameExistedException();
}
data.username = data.username.trim();
}
data.categoryIds = data.categoryIds || [];
if (!data.name) data.name = data.username;
await this.Performer.updateOne({ _id: id }, data);
// update / create auth
await this.queueEventService.publish({
channel: PERFORMER_CHANNEL,
eventName: EVENT.UPDATED,
data: {
performer: performer.toObject(),
payload
}
});
return { updated: true };
}
public async updateAvatar(performer: PerformerDto, file: FileDto) {
await this.Performer.updateOne(
{ _id: performer._id },
{
avatarId: file._id,
avatarPath: file.path
}
);
await this.fileService.addRef(file._id, {
itemId: performer._id,
itemType: REF_TYPE.PERFORMER
});
const p = await this.Performer.findById(performer._id);
await this.queueEventService.publish({
channel: PERFORMER_CHANNEL,
eventName: 'avatarUpdated',
data: {
performer: p.toObject(),
file
}
});
return file;
}
public async updateWelcomeVideo(user: PerformerDto, file: FileDto) {
await this.Performer.updateOne(
{ _id: user._id },
{
welcomeVideoId: file._id,
welcomeVideoPath: file.path
}
);
await this.fileService.addRef(file._id, {
itemId: user._id,
itemType: REF_TYPE.PERFORMER
});
return file;
}
public async findByUserId(userId: string | ObjectId) {
return this.Performer.findOne({ userId });
}
public async calculateScore(performerId) {
const performer = performerId instanceof this.Performer
? performerId
: await this.Performer.findById(performerId);
if (!performer) return 0;
let score = 0;
if (performer.vip) score += 1;
if (performer.verified) score += 1;
return score;
}
}
Frontend - Model Page Code Samples
import moment from 'moment';
import Head from 'next/head';
import Router from 'next/router';
import React, { useEffect, useState } from 'react';
import { truncate } from 'lodash';
import {
Layout, Row, Col, Button, Tag, message
} from 'antd';
import { connect, useSelector } from 'react-redux';
import { performerService } from 'src/services';
import Review from '@components/reviews';
import { IPerformer, IUIConfig } from 'src/interfaces';
import { ProfileImagesCarousel } from '@components/performer/profile-images-carousel';
import ProfileContact from '@components/performer/profile-contact';
import { ProfileRates } from '@components/performer/profile-rates';
import { PerformeServicesList } from '@components/performer/peformer-services-list';
import { RelatedProfiles } from '@components/performer';
import { getYtbEmbeddedLink } from '@lib/utils';
import { VideoPopUp } from '@components/video-popup/video-popup';
import RightSideBanner from '@components/common/right-side-bar';
import Link from 'next/link';
import '@components/performer/performer.less';
import './index.less';
interface IProps {
ui: IUIConfig;
performer: IPerformer;
attributes: any;
}
function redirect404(ctx: any) {
if (process.browser) {
Router.replace('/');
message.error('The model account has been deactivated');
return;
}
ctx.res.writeHead && ctx.res.writeHead(302, { Location: '/' });
ctx.res.end && ctx.res.end();
}
function PerformerProfile({ performer, ui, attributes }: IProps) {
const [details, setDetails] = useState({} as any);
const [serviceSettings, setServiceSettings] = useState([]);
const [rateSettings, setRateSettings] = useState([]);
const loggedIn = useSelector((state: any) => state.auth.loggedIn);
const currentUser = useSelector((state: any) => state.user.current);
const loadSettings = async (p) => {
try {
const { data } = await performerService.loadSettings(p._id);
const service = data.find((d) => d.group === 'services');
if (service) setServiceSettings(service.settings);
const rate = data.find((d) => d.group === 'rates');
if (rate) setRateSettings(rate.settings);
// eslint-disable-next-line no-empty
} catch (e) {
}
};
const escapeHtml = (unsafe) => (unsafe || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\n\s*\n/g, '\n')
// new line to br
.replace(/(\r\n|\n|\r)/gm, '<br>');
const getAge = (date) => moment().diff(date, 'years');
useEffect(() => {
const mapData = performerService.mapWithAttributes(performer, attributes);
setDetails(mapData);
}, [performer, attributes]);
useEffect(() => {
if (performer) loadSettings(performer);
}, [performer]);
const print = (attrData) => {
if (Array.isArray(attrData)) return attrData.join('; ');
return attrData;
};
const scrollDownToReviewList = () => {
document.getElementById('review-list').scrollIntoView({
behavior: 'smooth'
});
};
return (
<Layout className="model-details-layout">
<Head>
<title>
{`${ui?.siteName} | ${performer?.username || 'Loading...'}`}
</title>
<meta
name="keywords"
content={`${details?.username}, ${details?.name}`}
/>
<meta name="description" content={truncate(details?.bio)} />
{/* OG tags */}
<meta
property="og:title"
content={`${ui?.siteName} | ${details?.username}`}
key="title"
/>
<meta
property="og:image"
content={details?.avatar || '/no-avatar.png'}
/>
<meta
property="og:keywords"
content={`${details?.username}, ${details?.name}`}
/>
<meta property="og:description" content={truncate(details?.bio)} />
</Head>
<Row className="profile-details">
<Col lg={18} md={24} sm={24} xs={24}>
<h1 className="card-title">{details?.name || details?.username}</h1>
<div className="form-container">
{getYtbEmbeddedLink(details.introVideoLink) && (
<VideoPopUp
getYtbEmbeddedLink={getYtbEmbeddedLink}
introVideoLink={details.introVideoLink}
/>
)}
<h1 className="info-title">{details?.name}</h1>
<p className="last-seen">
Last seen online:
{' '}
{details.offlineAt && (
<span>
{moment(details.offlineAt).format('DD/MM/YYYY HH:mm')}
</span>
)}
</p>
<p
className="text"
dangerouslySetInnerHTML={{
__html: escapeHtml(details.aboutMe || '')
}}
/>
</div>
<Row>
<Col lg={12} md={12} sm={24} xs={24}>
<ProfileImagesCarousel performerId={performer._id} />
</Col>
<Col lg={12} md={12} sm={24} xs={24}>
<div className="form-container">
<h1 className="info-title">My details</h1>
<div className="params">
<div>
<span>Gender: </span>
<strong>{print(details.gender)}</strong>
</div>
<div>
<span>Age: </span>
<strong>{getAge(details.dateOfBirth)}</strong>
</div>
<div>
<span>Eyes: </span>
<strong>{print(details.eyes)}</strong>
</div>
<div>
<span>Hair color: </span>
<strong>{print(details.hairColor)}</strong>
</div>
<div>
<span>Hair length: </span>
<strong>{print(details.hairLength)}</strong>
</div>
<div>
<span>Bust size: </span>
<strong>{print(details.bustSize)}</strong>
</div>
<div>
<span>Bust type: </span>
<strong>{print(details.bustType)}</strong>
</div>
<div>
<span>Travels: </span>
<strong>{print(details.travels)}</strong>
</div>
<div>
<span>Weight: </span>
<strong>{print(details.weight)}</strong>
</div>
<div>
<span>Height: </span>
<strong>{print(details.height)}</strong>
</div>
<div>
<span>Ethnicity: </span>
<strong>{print(details.ethnicity)}</strong>
</div>
<div>
<span>Orientation: </span>
<strong>{print(details.orientation)}</strong>
</div>
<div>
<span>Smoker: </span>
<strong className="uppercase">{print(details.smoker)}</strong>
</div>
<div>
<span>Tattoo: </span>
<strong className="uppercase">{print(details.tattoo)}</strong>
</div>
<div>
<span>Nationality: </span>
<strong>{details.country}</strong>
</div>
<div className="tagColor">
<span>Languages: </span>
{details.languages?.map((m) => (<strong><Tag color="#242424">{print(m)}</Tag></strong>))}
</div>
<div className="tagColor">
<span>Services: </span>
{details.services?.map((ser) => (<strong><Tag color="#242424">{print(ser)}</Tag></strong>))}
</div>
<div>
<span>Provides: </span>
<strong>{print(details.provides)}</strong>
</div>
<div className="tagColor">
<span>Meeting with: </span>
{details.meetingWith?.map((m) => <strong><Tag color="#242424">{print(m)}</Tag></strong>)}
</div>
{details?.categories?.length && (
<div className="tagColor">
<span>Category: </span>
{details.categories?.map((c) => (
<Link
href={{
pathname: '/search/category',
query: { id: c.slug }
}}
as={`/category/${c.slug}`}
>
<a>
{c.name}
</a>
</Link>
)).reduce((prev, curr) => [prev, '; ', curr])}
</div>
)}
</div>
<div>
<Row>
<Col lg={12} md={12} sm={24} xs={24}>
<Button
onClick={() => {
if (!loggedIn) {
message.info('Please login to chat with the model');
return;
}
Router.push({ pathname: '/messages', query: { toSource: 'performer', toId: details.userId } }, `/messages?toSource=performer&toId=${details.userId}`);
}}
disabled={currentUser?.roles.includes('performer')}
>
CHAT
</Button>
</Col>
<Col lg={12} md={12} sm={24} xs={24}>
<Button
className="btn-book"
onClick={() => {
if (!loggedIn) {
message.info('Please login to chat with the model');
return;
}
Router.push({ pathname: '/booking', query: { username: performer?.username } });
}}
>
BOOK
</Button>
</Col>
</Row>
</div>
</div>
</Col>
</Row>
<ProfileContact performer={performer} scrollDownToReviewList={scrollDownToReviewList} />
<div>
<h3 className="card-title">RATES</h3>
<ProfileRates
attributes={attributes}
rateSettings={rateSettings}
currency={details.currency}
/>
</div>
<div>
<h3 className="card-title">SERVICES</h3>
<PerformeServicesList
attributes={attributes}
serviceSettings={serviceSettings}
currency={details.currency}
/>
</div>
<div className="reviews">
<h3 className="card-title">REVIEW</h3>
{/* <Review sourceId={details._id} /> */}
<Review sourceId={performer._id} source="performer" performer={performer} />
</div>
</Col>
<Col className="background-right" lg={6} md={24} sm={24} xs={24}>
<RightSideBanner />
<RelatedProfiles performerId={performer._id} />
</Col>
</Row>
</Layout>
);
}
const mapStates = (state: any) => ({
ui: state.ui,
attributes: state.settings.attributes
});
const ProfileConnectRedux = connect(mapStates)(PerformerProfile) as any;
ProfileConnectRedux.getInitialProps = async ({ ctx }) => {
try {
const { query } = ctx;
const resp = await performerService.findOne(query.id);
const { data: performer } = resp;
if (!performer) {
return redirect404(ctx);
}
return {
performer
};
} catch (e) {
return redirect404(ctx);
}
};
export default ProfileConnectRedux;
{"serverDuration": 21, "requestCorrelationId": "8b2088c97010488ea78ec349b63ef0a8"}