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;