diff --git a/index.html b/index.html index 3e079c4..e2bbeb1 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ 6 cities + diff --git a/package-lock.json b/package-lock.json index 13cd997..ff6b3fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "eslint-plugin-react-refresh": "0.4.3", "faker": "5.5.3", "jsdom": "22.1.0", - "typescript": "^5.6.3", + "typescript": "5.6.3", "vite": "4.4.11", "vitest": "0.34.6" } diff --git a/src/Pages/favorites-page.tsx b/src/Pages/favorites-page.tsx index efab44e..f8a3e67 100644 --- a/src/Pages/favorites-page.tsx +++ b/src/Pages/favorites-page.tsx @@ -11,30 +11,32 @@ export function FavoritesPage({ offers, }: FavoritesPageProps): React.JSX.Element { return ( - -
- - 6 cities - favorites - -
-
-

Saved listing

-
    - {Map.groupBy(offers, (o) => o.city.name) - .entries() - .map(([cityName, offersInCity]) => ( - - )) - .toArray()} - ; -
-
-
-
-
+
+ +
+ + 6 cities - favorites + +
+
+

Saved listing

+
    + {Map.groupBy(offers, (o) => o.city.name) + .entries() + .map(([cityName, offersInCity]) => ( + + )) + .toArray()} + ; +
+
+
+
+
+
); } diff --git a/src/Pages/main-page.tsx b/src/Pages/main-page.tsx index 2be8d58..b9e70c1 100644 --- a/src/Pages/main-page.tsx +++ b/src/Pages/main-page.tsx @@ -13,106 +13,109 @@ interface MainPageProps { export function MainPage({ offers }: MainPageProps): React.JSX.Element { const [activeOffer, setActiveOffer] = useState>(null); return ( - -
- 6 cities -

Cities

-
-
- -
-
-
-
-
-

Places

- - {offers.length} places to stay in Amsterdam - -
- Sort by - - Popular - - - - -
    -
  • - Popular -
  • -
  • - Price: low to high -
  • -
  • - Price: high to low -
  • -
  • - Top rated first -
  • -
-
- ) => - setActiveOffer(offer) - } - /> +
+ +
+ 6 cities +

Cities

+
+
+
-
- ({ - location: x.location, - id: x.id, - }))} - selectedPoint={ - activeOffer - ? { +
+
+
+
+

Places

+ + {offers.length} places to stay in Amsterdam + +
+ Sort by + + Popular + + + + +
    +
  • + Popular +
  • +
  • + Price: low to high +
  • +
  • + Price: high to low +
  • +
  • + Top rated first +
  • +
+
+ ) => + setActiveOffer(offer)} + isOnMainPage + /> +
+
+ ({ + location: x.location, + id: x.id, + }))} + selectedPoint={ + activeOffer + ? { location: activeOffer?.location, id: activeOffer?.id, } - : undefined - } - /> + : undefined + } + isOnMainPage + /> +
-
-
-
+
+
+ ); } diff --git a/src/Pages/not-found-page/not-found-page.module.css b/src/Pages/not-found-page/not-found-page.css similarity index 70% rename from src/Pages/not-found-page/not-found-page.module.css rename to src/Pages/not-found-page/not-found-page.css index 5155bb6..64dc74f 100644 --- a/src/Pages/not-found-page/not-found-page.module.css +++ b/src/Pages/not-found-page/not-found-page.css @@ -1,20 +1,20 @@ -.container { +.not-found-page { display: flex; flex-direction: column; align-items: center; justify-content: center; } -.title { +.not-found-page__title { text-align: center; } -.link { +.not-found-page__link { color: skyblue; margin: 10px; font-style: italic; } -.link:hover { +.not-found-page__link:hover { text-decoration: underline; font-weight: bold; } diff --git a/src/Pages/not-found-page/not-found-page.tsx b/src/Pages/not-found-page/not-found-page.tsx index ce3c619..05e617a 100644 --- a/src/Pages/not-found-page/not-found-page.tsx +++ b/src/Pages/not-found-page/not-found-page.tsx @@ -1,19 +1,19 @@ import { Link } from 'react-router-dom'; -import styles from './not-found-page.module.css'; import { Helmet } from 'react-helmet-async'; +import {AppRoutes} from '../../dataTypes/enums/app-routes.ts'; export function NotFoundPage(): React.JSX.Element { return ( -
+
404 - not found

404 - Page Not Found

-

+

The page you are looking for might have been removed or is temporarily unavailable.

- + back to main page
diff --git a/src/Pages/offer-page.tsx b/src/Pages/offer-page.tsx index a671867..58c649b 100644 --- a/src/Pages/offer-page.tsx +++ b/src/Pages/offer-page.tsx @@ -2,194 +2,120 @@ import { Helmet } from 'react-helmet-async'; import { Layout } from '../components/layout.tsx'; import { OffersList } from '../components/offer/offers-list.tsx'; import { offerMocks } from '../mocks/offers.ts'; -import { ReviewForm } from '../components/review-form.tsx'; +import { Reviews } from '../components/reviews/reviews.tsx'; +import { reviewMocks } from '../mocks/reviews.ts'; +import { Map } from '../components/map/map.tsx'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { OfferInsideItems } from '../components/offer/offer-inside-items.tsx'; +import { detailedOfferMocks } from '../mocks/detailed-offer.ts'; +import { OfferHost } from '../components/offer/offer-host.tsx'; +import { capitalize, pluralizeAndCombine } from '../utils/string-utils.ts'; +import { Rating } from '../components/rating.tsx'; +import { OfferGallery } from '../components/offer/offer-gallery.tsx'; export function OfferPage(): React.JSX.Element { + const offerId = useParams().id; + const offers = offerMocks.filter((offer) => offer.id !== offerId).slice(0, 3); + const currentOffer = detailedOfferMocks.find( + (offer) => offer.id === offerId, + )!; return ( - -
- 6 cities - offer -
-
-
-
- Photo studio -
-
- Photo studio -
-
- Photo studio -
-
- Photo studio -
-
- Photo studio -
-
- Photo studio -
-
-
-
-
-
- Premium -
-
-

- Beautiful & luxurious studio at great location -

- -
-
-
- - Rating +
+ +
+ 6 cities - offer +
+ +
+
+ {currentOffer.isPremium && ( +
+ Premium +
+ )} +
+

{currentOffer.title}

+
- 4.8 -
-
    -
  • - Apartment -
  • -
  • - 3 Bedrooms -
  • -
  • - Max 4 adults -
  • -
-
- €120 -  night -
-
-

What's inside

-
    -
  • Wi-Fi
  • -
  • Washing machine
  • -
  • Towels
  • -
  • Heating
  • -
  • Coffee machine
  • -
  • Baby seat
  • -
  • Kitchen
  • -
  • Dishwasher
  • -
  • Cabel TV
  • -
  • Fridge
  • + +
      +
    • + {capitalize(currentOffer.type)} +
    • +
    • + {pluralizeAndCombine('bedroom', currentOffer.bedrooms)} +
    • +
    • + Max {pluralizeAndCombine('adult', currentOffer.maxAdults)} +
    -
-
-

Meet the host

-
-
- Host avatar -
- Angelina - Pro +
+ + €{currentOffer.price} + +  night
-
-

- A quiet cozy and picturesque that hides behind a a river by - the unique lightness of Amsterdam. The building is green and - from 18th century. -

-

- An independent House, strategically located between Rembrand - Square and National Opera, but where the bustle of the city - comes to rest in this alley flowery and colorful. -

+ +
+ +
+

+ A quiet cozy and picturesque that hides behind a a river + by the unique lightness of Amsterdam. The building is + green and from 18th century. +

+

+ An independent House, strategically located between + Rembrand Square and National Opera, but where the bustle + of the city comes to rest in this alley flowery and + colorful. +

+
+
-
-

- Reviews · 1 -

-
    -
  • -
    -
    - Reviews avatar -
    - Max -
    -
    -
    -
    - - Rating -
    -
    -

    - A quiet cozy and picturesque that hides behind a a river - by the unique lightness of Amsterdam. The building is - green and from 18th century. -

    - -
    -
  • -
- -
-
-
-
-
-
-
-

- Other places in the neighbourhood -

-
-
+ ({ + location: x.location, + id: x.id, + }))} + selectedPoint={{ + location: currentOffer.location, + id: currentOffer.id, + }} + />
-
-
-
+
+
+

+ Other places in the neighbourhood +

+
+ +
+
+
+
+
+ ); } diff --git a/src/components/app.tsx b/src/components/app.tsx index 58fd7f6..35672f6 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -26,7 +26,7 @@ export function App({ offers }: AppProps): React.JSX.Element { + } diff --git a/src/components/layout.tsx b/src/components/layout.tsx index 21bdb6d..5c492ea 100644 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -11,7 +11,7 @@ export function Layout({ showFooter, }: LayoutProps): React.JSX.Element { return ( -
+ <>
@@ -64,6 +64,6 @@ export function Layout({ )} -
+ ); } diff --git a/src/components/map/map.tsx b/src/components/map/map.tsx index c2a345e..6ae3b62 100644 --- a/src/components/map/map.tsx +++ b/src/components/map/map.tsx @@ -1,14 +1,16 @@ -import { useRef, useEffect } from 'react'; +import React, { useRef, useEffect } from 'react'; import { Icon, Marker, layerGroup } from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { useMap } from './use-map.ts'; import { City } from '../../dataTypes/city.ts'; import { Point } from '../../dataTypes/point.ts'; +import cn from 'classnames'; interface MapProps { city: City; points: Point[]; selectedPoint: Point | undefined; + isOnMainPage?: boolean; } const defaultCustomIcon = new Icon({ @@ -23,8 +25,8 @@ const currentCustomIcon = new Icon({ iconAnchor: [20, 40], }); -export function Map(props: MapProps): JSX.Element { - const { city, points, selectedPoint } = props; +export function Map(props: MapProps): React.JSX.Element { + const { city, points, selectedPoint, isOnMainPage } = props; const mapRef = useRef(null); const map = useMap(mapRef, city); @@ -53,5 +55,15 @@ export function Map(props: MapProps): JSX.Element { } }, [map, points, selectedPoint]); - return
; + return ( +
+
+ ); } diff --git a/src/components/offer/offer-card.tsx b/src/components/offer/offer-card.tsx index b728c02..4ae3767 100644 --- a/src/components/offer/offer-card.tsx +++ b/src/components/offer/offer-card.tsx @@ -1,6 +1,8 @@ import { RoomType } from '../../dataTypes/enums/room-type.ts'; import { Link } from 'react-router-dom'; import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import cn from 'classnames'; +import { Rating } from '../rating.tsx'; interface PlaceCardProps { id: string; @@ -8,10 +10,12 @@ interface PlaceCardProps { type: RoomType; image: string; title: string; + rating: number; onMouseEnter?: (id: string) => void; onMouseLeave?: () => void; isPremium?: boolean; isFavorite?: boolean; + isOnMainPage?: boolean; } export function OfferCard({ @@ -20,10 +24,12 @@ export function OfferCard({ type, image, title, + rating, onMouseEnter, onMouseLeave, isPremium, isFavorite, + isOnMainPage, }: PlaceCardProps): React.JSX.Element { const handleMouseEnter = (): void => onMouseEnter?.(id); const handleMouseLeave = (): void => onMouseLeave?.(); @@ -31,15 +37,25 @@ export function OfferCard({
{isPremium && (
Premium
)} -
- +
+
-
-
- - Rating -
-
+

- {title} + {title}

{type}

diff --git a/src/components/offer/offer-gallery.tsx b/src/components/offer/offer-gallery.tsx new file mode 100644 index 0000000..a1047be --- /dev/null +++ b/src/components/offer/offer-gallery.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +interface OfferGalleryProps { + imageSources: string[]; +} + +export function OfferGallery({ + imageSources, +}: OfferGalleryProps): React.JSX.Element { + return ( +
+
+ {imageSources.map((src) => ( +
+ Photo studio +
+ ))} +
+
+ ); +} diff --git a/src/components/offer/offer-host.tsx b/src/components/offer/offer-host.tsx new file mode 100644 index 0000000..e4c288b --- /dev/null +++ b/src/components/offer/offer-host.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { User } from '../../dataTypes/user.ts'; +import { getFirstName } from '../../utils/username-utils.ts'; + +interface OfferHostProps { + host: User; +} + +export function OfferHost({ host }: OfferHostProps): React.JSX.Element { + return ( + <> +

Meet the host

+
+
+ Host avatar +
+ {getFirstName(host.name)} + {host.isPro && Pro} +
+ + ); +} diff --git a/src/components/offer/offer-inside-items.tsx b/src/components/offer/offer-inside-items.tsx new file mode 100644 index 0000000..793f717 --- /dev/null +++ b/src/components/offer/offer-inside-items.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +interface OfferInsideItemsProps { + items: string[]; +} + +export function OfferInsideItems({ + items, +}: OfferInsideItemsProps): React.JSX.Element { + return ( +
+

What's inside

+
    + {items.map((item) => ( +
  • + {item} +
  • + ))} +
+
+ ); +} diff --git a/src/components/offer/offers-list.tsx b/src/components/offer/offers-list.tsx index 9bb90cd..7d9c3bc 100644 --- a/src/components/offer/offers-list.tsx +++ b/src/components/offer/offers-list.tsx @@ -6,11 +6,13 @@ import { Nullable } from 'vitest'; interface OffersListProps { offers: Offer[]; onActiveOfferChange?: (offer: Nullable) => void; + isOnMainPage?: boolean; } export function OffersList({ offers, onActiveOfferChange, + isOnMainPage, }: OffersListProps): React.JSX.Element { const handleActiveOfferChange = (offer: Nullable): void => { onActiveOfferChange?.(offer); @@ -25,10 +27,12 @@ export function OffersList({ type={offer.type} image={offer.previewImage} title={offer.title} + rating={offer.rating} onMouseEnter={() => handleActiveOfferChange(offer)} onMouseLeave={() => handleActiveOfferChange(null)} isFavorite={offer.isFavorite} isPremium={offer.isPremium} + isOnMainPage={isOnMainPage} /> ))}
diff --git a/src/components/rating.tsx b/src/components/rating.tsx new file mode 100644 index 0000000..7db3415 --- /dev/null +++ b/src/components/rating.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +interface RatingProps { + rating: number; + usePlace: string; + isInOffer?: boolean; +} + +export function Rating({ + rating, + isInOffer, + usePlace, +}: RatingProps): React.JSX.Element { + return ( +
+
+ + Rating +
+ {isInOffer && ( + {rating} + )} +
+ ); +} diff --git a/src/components/reviews/review-component.tsx b/src/components/reviews/review-component.tsx new file mode 100644 index 0000000..1e72ef5 --- /dev/null +++ b/src/components/reviews/review-component.tsx @@ -0,0 +1,42 @@ +import { getFirstName } from '../../utils/username-utils.ts'; +import { Rating } from '../rating.tsx'; + +interface ReviewProps { + comment: string; + rating: number; + date: Date; + avatarUrl: string; + userName: string; +} + +export function ReviewComponent({ + comment, + rating, + date, + avatarUrl, + userName, +}: ReviewProps): React.JSX.Element { + return ( +
  • +
    +
    + Reviews avatar +
    + {getFirstName(userName)} +
    +
    + +

    {comment}

    + +
    +
  • + ); +} diff --git a/src/components/review-form.tsx b/src/components/reviews/review-form.tsx similarity index 99% rename from src/components/review-form.tsx rename to src/components/reviews/review-form.tsx index b556f0d..e9a25ba 100644 --- a/src/components/review-form.tsx +++ b/src/components/reviews/review-form.tsx @@ -117,7 +117,8 @@ export function ReviewForm(): React.JSX.Element { name="review" placeholder="Tell how was your stay, what you like and what can be improved" onChange={onCommentChange} - > + > +

    To submit review please make sure to set{' '} diff --git a/src/components/reviews/reviews-list.tsx b/src/components/reviews/reviews-list.tsx new file mode 100644 index 0000000..8f463fa --- /dev/null +++ b/src/components/reviews/reviews-list.tsx @@ -0,0 +1,29 @@ +import { Review } from '../../dataTypes/review.ts'; +import { ReviewComponent } from './review-component.tsx'; + +interface ReviewsListProps { + reviews: Review[]; +} + +export function ReviewsList({ reviews }: ReviewsListProps): React.JSX.Element { + return ( + <> +

    + Reviews ·{' '} + {reviews.length} +

    +
      + {reviews.map((review: Review) => ( + + ))} +
    + + ); +} diff --git a/src/components/reviews/reviews.tsx b/src/components/reviews/reviews.tsx new file mode 100644 index 0000000..5578d04 --- /dev/null +++ b/src/components/reviews/reviews.tsx @@ -0,0 +1,30 @@ +import { ReviewForm } from './review-form.tsx'; +import { ReviewsList } from './reviews-list.tsx'; +import { Review } from '../../dataTypes/review.ts'; + +interface ReviewsProps { + reviews: Review[]; +} + +export function Reviews({ reviews }: ReviewsProps): React.JSX.Element { + const reviewsAvailable = reviews && reviews.length !== 0; + return ( +
    + {reviewsAvailable ? ( + + ) : ( + + No reviews available + + )} + +
    + ); +} diff --git a/src/dataTypes/detailed-offer.ts b/src/dataTypes/detailed-offer.ts new file mode 100644 index 0000000..c7e3130 --- /dev/null +++ b/src/dataTypes/detailed-offer.ts @@ -0,0 +1,21 @@ +import { City } from './city.ts'; +import { User } from './user.ts'; +import { Location } from './location.ts'; + +export type DetailedOffer = { + id: string; + title: string; + type: string; + price: number; + city: City; + location: Location; + isFavorite: boolean; + isPremium: boolean; + rating: number; + description: string; + bedrooms: number; + goods: string[]; + host: User; + images: string[]; + maxAdults: number; +}; diff --git a/src/dataTypes/enums/authorization-status.ts b/src/dataTypes/enums/authorization-status.ts index 3e0b810..b24c350 100644 --- a/src/dataTypes/enums/authorization-status.ts +++ b/src/dataTypes/enums/authorization-status.ts @@ -1,5 +1,5 @@ export enum AuthorizationStatus { Authorized, Unauthorized, - Unknown + Unknown, } diff --git a/src/dataTypes/enums/room-type.ts b/src/dataTypes/enums/room-type.ts index 10b288f..86c0b35 100644 --- a/src/dataTypes/enums/room-type.ts +++ b/src/dataTypes/enums/room-type.ts @@ -1,4 +1,4 @@ export enum RoomType { - Apartment = 'apartment', - Room = 'room', + Apartment = 'Apartment', + Room = 'Room', } diff --git a/src/dataTypes/location.ts b/src/dataTypes/location.ts index 5cd6bac..39692f0 100644 --- a/src/dataTypes/location.ts +++ b/src/dataTypes/location.ts @@ -2,4 +2,4 @@ export type Location = { latitude: number; longitude: number; zoom: number; -} +}; diff --git a/src/dataTypes/review.ts b/src/dataTypes/review.ts new file mode 100644 index 0000000..733843c --- /dev/null +++ b/src/dataTypes/review.ts @@ -0,0 +1,9 @@ +import { User } from './user.ts'; + +export type Review = { + id: string; + date: string; + user: User; + comment: string; + rating: number; +}; diff --git a/src/dataTypes/user.ts b/src/dataTypes/user.ts new file mode 100644 index 0000000..3b221f9 --- /dev/null +++ b/src/dataTypes/user.ts @@ -0,0 +1,5 @@ +export type User = { + name: string; + avatarUrl: string; + isPro: boolean; +}; diff --git a/src/mocks/detailed-offer.ts b/src/mocks/detailed-offer.ts new file mode 100644 index 0000000..7d2407e --- /dev/null +++ b/src/mocks/detailed-offer.ts @@ -0,0 +1,141 @@ +export const detailedOfferMocks = [ + { + id: '6af6f711-c28d-4121-82cd-e0b462a27f00', + title: 'Beautiful & luxurious studio at great location', + type: 'apartment', + price: 120, + city: { + name: 'Amsterdam', + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + }, + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + isFavorite: false, + isPremium: false, + rating: 4, + description: + 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', + bedrooms: 3, + goods: ['Heating'], + host: { + name: 'Oliver Conner', + avatarUrl: 'https://url-to-image/image.png', + isPro: false, + }, + images: ['https://url-to-image/image.png'], + maxAdults: 4, + }, + { + id: '6af6f711-c28d-4121-82cd-e0b462a27f11', + title: 'Beautiful & luxurious studio at great location', + type: 'apartment', + price: 120, + city: { + name: 'Amsterdam', + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + }, + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + isFavorite: false, + isPremium: false, + rating: 4, + description: + 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', + bedrooms: 3, + goods: ['Heating'], + host: { + name: 'Oliver Conner', + avatarUrl: 'https://url-to-image/image.png', + isPro: false, + }, + images: ['https://url-to-image/image.png'], + maxAdults: 4, + }, + { + id: '6af6f711-c28d-4121-82cd-e0b462a27f22', + title: 'Beautiful & luxurious studio at great location', + type: 'apartment', + price: 120, + city: { + name: 'Amsterdam', + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + }, + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + isFavorite: false, + isPremium: false, + rating: 4, + description: + 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', + bedrooms: 3, + goods: ['Heating'], + host: { + name: 'Oliver Conner', + avatarUrl: 'https://url-to-image/image.png', + isPro: false, + }, + images: ['https://url-to-image/image.png'], + maxAdults: 4, + }, + { + id: '6af6f711-c28d-4121-82cd-e0b462a27f33', + title: 'Beautiful & luxurious studio at great location', + type: 'apartment', + price: 120, + city: { + name: 'Amsterdam', + location: { + latitude: 52.35514938496378, + longitude: 4.673877537499948, + zoom: 8, + }, + }, + location: { + latitude: 52.3609553943508, + longitude: 4.85309666406198, + zoom: 8, + }, + isFavorite: false, + isPremium: false, + rating: 4, + description: + 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', + bedrooms: 3, + goods: ['Heating', 'plastation 5 pro max', 'Aboba', 'Amogus'], + host: { + name: 'Angelina Noname', + avatarUrl: 'img/avatar-angelina.jpg', + isPro: true, + }, + images: [ + 'img/apartment-01.jpg', + 'img/apartment-02.jpg', + 'img/apartment-03.jpg', + 'img/room.jpg', + 'img/studio-01.jpg', + 'img/apartment-01.jpg', + ], + maxAdults: 4, + }, +]; diff --git a/src/mocks/reviews.ts b/src/mocks/reviews.ts new file mode 100644 index 0000000..10b85f8 --- /dev/null +++ b/src/mocks/reviews.ts @@ -0,0 +1,27 @@ +import { Review } from '../dataTypes/review.ts'; + +export const reviewMocks: Review[] = [ + { + id: 'b67ddfd5-b953-4a30-8c8d-bd083cd6b62a', + date: '2019-05-08T14:13:56.569Z', + user: { + name: 'Oliver Conner', + avatarUrl: 'img/avatar-max.jpg', + isPro: false, + }, + comment: + 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', + rating: 4, + }, + { + id: 'b67ddfd5-b953-4a30-8c8d-bd083cd6b62b', + date: '2019-05-09T14:16:56.569Z', + user: { + name: 'Alice Conner', + avatarUrl: 'img/avatar-angelina.jpg', + isPro: true, + }, + comment: 'aboba amogus', + rating: 5, + }, +]; diff --git a/src/utils/string-utils.ts b/src/utils/string-utils.ts new file mode 100644 index 0000000..f5f0347 --- /dev/null +++ b/src/utils/string-utils.ts @@ -0,0 +1,14 @@ +export function capitalize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +export function pluralize(value: string, count: number): string { + if (count === 1) { + return value; + } + return `${value}s`; +} + +export function pluralizeAndCombine(value: string, count: number): string { + return `${count} ${pluralize(value, count)}`; +} diff --git a/src/utils/username-utils.ts b/src/utils/username-utils.ts new file mode 100644 index 0000000..00f42a3 --- /dev/null +++ b/src/utils/username-utils.ts @@ -0,0 +1,3 @@ +export function getFirstName(name: string): string { + return name.split(' ')[0]; +}