Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
    .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;
  • No labels