In this post of this series we will implement the property details Api and use it to display the property details page.
Property Details Api
As usual let’s start by adding the web service for property details. This is where the show() method of the PropertyController controller.
app/Http/Controllers/PropertyController.php
/** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\JsonResponse */ public function show($id) { $property = Property::with(["user", "pictures", "features", "country", "state", "city"])->find($id); return response()->json(['status' => true, 'property' => $property]); }
That’s it, now we don’t need to add a route as the route is available automatically as “/api/property/{id}” because this controller an Api controller.
Updating Vue Component
To display the property details in the Vue page, typically in PropertySingle.vue page.
resources/js/components/pages/PropertySingle.vue
<template> <div class="fa-3x" v-if="loading"> <i class="fa fa-spinner fa-spin"></i> </div> <div v-if="propertyDetails"> <div class="page-head"> <div class="container"> <div class="row"> <div class="page-head-content"> <h1 class="page-title">{{propertyDetails.title}} </h1> </div> </div> </div> </div> <!-- End page header --> <!-- property area --> <div class="content-area single-property" style="background-color: #FCFCFC;"> <div class="container"> <div class="clearfix padding-top-40" > <div class="col-md-8 single-property-content prp-style-1 "> <div class="row"> <div class="light-slide-item"> <div class="clearfix"> <div class="favorite-and-print"> <a class="add-to-fav" href="#" title="add to favorites"> <i class="fa fa-star-o"></i> </a> <a class="printer-icon " href="javascript:window.print()"> <i class="fa fa-print"></i> </a> </div> <ul id="image-gallery" class="gallery list-unstyled cS-hidden"> <li v-for="picture in propertyDetails.pictures" :key="picture.id" :data-thumb="picture.image_url"> <img :src="picture.image_url" :alt="propertyDetails.title" /> </li> </ul> </div> </div> </div> <div class="single-property-wrapper"> <div class="single-property-header"> <h1 class="property-title pull-left">{{propertyDetails.title}} in {{propertyDetails.country.name}}</h1> <span class="property-price pull-right">{{getFormattedPrice(propertyDetails)}}</span> </div> <div class="property-meta entry-meta clearfix "> <div class="col-xs-6 col-sm-3 col-md-3 p-b-15"> <span class="property-info-icon icon-tag"> <img :src="`/template/assets/img/icon/${propertyDetails.status === 'sale'?'sale-orange.png':'rent-orange.png'}`"> </span> <span class="property-info-entry"> <span class="property-info-label">Status</span> <span class="property-info-value">For {{propertyDetails.status}}</span> </span> </div> <div class="col-xs-6 col-sm-3 col-md-3 p-b-15" v-if="propertyDetails.area"> <span class="property-info icon-area"> <img src="/template/assets/img/icon/room-orange.png"> </span> <span class="property-info-entry"> <span class="property-info-label">Area</span> <span class="property-info-value">{{propertyDetails.area}} <b class="property-info-unit">Sq Ft</b></span> </span> </div> <div class="col-xs-6 col-sm-3 col-md-3 p-b-15" v-if="propertyDetails.bedrooms"> <span class="property-info-icon icon-bed"> <img src="/template/assets/img/icon/bed-orange.png"> </span> <span class="property-info-entry"> <span class="property-info-label">Bedrooms</span> <span class="property-info-value">{{propertyDetails.bedrooms}}</span> </span> </div> <div class="col-xs-6 col-sm-3 col-md-3 p-b-15" v-if="propertyDetails.bathrooms"> <span class="property-info-icon icon-garage"> <img src="/template/assets/img/icon/shawer-orange.png"> </span> <span class="property-info-entry"> <span class="property-info-label">Bathrooms</span> <span class="property-info-value">{{propertyDetails.bathrooms}}</span> </span> </div> <div class="col-xs-6 col-sm-3 col-md-3 p-b-15" v-if="propertyDetails.rooms"> <span class="property-info-icon icon-garage"> <img src="/template/assets/img/icon/room-orange.png"> </span> <span class="property-info-entry"> <span class="property-info-label">Other rooms</span> <span class="property-info-value">{{propertyDetails.rooms}}</span> </span> </div> <div class="col-xs-6 col-sm-3 col-md-3 p-b-15" v-if="propertyDetails.garages"> <span class="property-info-icon icon-bed"> <img src="/template/assets/img/icon/cars-orange.png"> </span> <span class="property-info-entry"> <span class="property-info-label">Car garages</span> <span class="property-info-value">{{propertyDetails.garages}}</span> </span> </div> </div> <!-- .property-meta --> <div class="section" v-if="propertyDetails.description"> <h4 class="s-property-title">Description</h4> <div class="s-property-content"> <p>{{propertyDetails.description}}</p> </div> </div> <!-- End description area --> <div class="section additional-details"> <h4 class="s-property-title">Additional Details</h4> <ul class="additional-details-list clearfix"> <li v-if="propertyDetails.units"> <span class="col-xs-6 col-sm-4 col-md-4 add-d-title">Units</span> <span class="col-xs-6 col-sm-8 col-md-8 add-d-entry">{{propertyDetails.units}}</span> </li> <li v-if="propertyDetails.year_built"> <span class="col-xs-6 col-sm-4 col-md-4 add-d-title">Built In</span> <span class="col-xs-6 col-sm-8 col-md-8 add-d-entry">{{propertyDetails.year_built}}</span> </li> <li v-if="propertyDetails.floor_number"> <span class="col-xs-6 col-sm-4 col-md-4 add-d-title">Floor number</span> <span class="col-xs-6 col-sm-8 col-md-8 add-d-entry">{{propertyDetails.floor_number}}</span> </li> <li v-if="propertyDetails.property_finalizing"> <span class="col-xs-6 col-sm-4 col-md-4 add-d-title">Property Finalizing</span> <span class="col-xs-6 col-sm-8 col-md-8 add-d-entry">{{propertyDetails.property_finalizing}}</span> </li> <li v-if="propertyDetails.phone"> <span class="col-xs-6 col-sm-4 col-md-4 add-d-title">Phone</span> <span class="col-xs-6 col-sm-8 col-md-8 add-d-entry">{{propertyDetails.phone}}</span> </li> </ul> </div> <!-- End additional-details area --> <div class="section property-features" v-if="propertyDetails.features.length > 0"> <h4 class="s-property-title">Features</h4> <ul> <li v-for="feature in propertyDetails.features" :key="feature.id"><a href="#">{{feature.title}}</a></li> </ul> </div> <!-- End features area --> <div class="section property-video" v-if="propertyDetails.youtube_video"> <h4 class="s-property-title">Property Video</h4> <div class="video-thumb"> <a class="video-popup" :href="propertyDetails.youtube_video" title="Virtual Tour" target="_blank"> <img :src="propertyDetails.pictures[0].image_url" class="img-responsive wp-post-image" alt="Video"> <i class="fa fa-play-circle"></i> </a> </div> </div> <!-- End video area --> <div class="section property-share"> <h4 class="s-property-title">Share width your friends </h4> <div class="roperty-social"> <ul> <li><a title="Share this on facebok " href="#"><img src="/template/assets/img/social_big/facebook_grey.png"></a></li> <li><a title="Share this on delicious " href="#"><img src="/template/assets/img/social_big/delicious_grey.png"></a></li> <li><a title="Share this on tumblr " href="#"><img src="/template/assets/img/social_big/tumblr_grey.png"></a></li> <li><a title="Share this on twitter " href="#"><img src="/template/assets/img/social_big/twitter_grey.png"></a></li> <li><a title="Share this on linkedin " href="#"><img src="/template/assets/img/social_big/linkedin_grey.png"></a></li> </ul> </div> </div> <!-- End video area --> </div> </div> <div class="col-md-4 p0"> <aside class="sidebar sidebar-property blog-asside-right"> <PropertyUserInfo :property-details="propertyDetails" /> <div class="panel panel-default sidebar-menu wow fadeInRight animated"> <div class="panel-heading"> <h3 class="panel-title">Ads her </h3> </div> <div class="panel-body recent-property-widget"> <img src="/template/assets/img/ads.jpg"> </div> </div> </aside> </div> </div> </div> </div> </div> </template> <script> import {onMounted, ref} from "vue"; import {useRoute} from "vue-router"; import {useToastError} from "../../composables/useToastError"; import {formatMoney} from "../../api/helpers"; import {rentAmountPerList} from "../../api/static_data"; import PropertyUserInfo from "../partials/PropertyUserInfo"; export default { name: "PropertySingle", components: {PropertyUserInfo}, setup() { const route = useRoute(); const {displayErrors} = useToastError(); let propertyDetails = ref(null); let loading = ref(true); loading.value = true; window.axios.get(`/api/property/${route.params.id}`).then(response => { loading.value = false; if(!response.data.status) { displayErrors({data: {message: 'Unable to fetch property'}}); return; } propertyDetails.value = response.data.property; setTimeout(initializeSlider, 300); }).catch(err => { loading.value = false; console.error(err); }); function getFormattedPrice(property) { if(property.status === 'Sale') { return '$' + formatMoney(property.price); } else { return '$' + formatMoney(property.price) +' ' + (rentAmountPerList.find(l => l.id == property.rent_amount_per)).name; } } function initializeSlider() { $(document).ready(function () { var slide = $('#image-gallery').lightSlider({ gallery: true, item: 1, thumbItem: 9, slideMargin: 0, speed: 500, auto: true, loop: true, onSliderLoad: function () { $('#image-gallery').removeClass('cS-hidden'); } }); }); } return { propertyDetails, loading, getFormattedPrice } } } </script> <style scoped> .gallery .lslide { background-color: #ccc; } .single-property-header .property-title { font-size: 23px; } .single-property-header .property-price { font-size: 27px; } .video-popup { position: relative; } .video-popup .fa-play-circle { position: absolute; top: 143px; left: 181px; font-size: 56px; } .fav-selected { color: #FDC600; border-color: #FDC600; } </style>
In this component i extracted the underlying component html from the html template we downloaded before. The setup() contains the necessary code to fetch the property details and return the required data to be displayed in the template.
The reactive properties propertyDetails and loading defined using the ref() function to hold the property details object and loading status respectively.
Then i made an axios request to the property details endpoint. Once retrieved the response i binded it to the propertyDetails.value.
As an important point to notice is jquery image slider, it must initialized after fetching the details. This is where the initializeSlider() function do.
The getFormattedPrice() function returns the formatted rent or price string depending on the property type.
Finally in the template i displayed each data from the propertyDetails object like the title, pictures, features, etc.
If you see in the template above there is a partial component <PropertyUserInfo /> which display the property owner details, so let’s create it:
resources/js/components/partials/PropertyUserInfo.vue
<template> <div class="dealer-widget"> <div class="dealer-content"> <div class="inner-wrapper"> <div class="clear"> <div class="col-xs-4 col-sm-4 dealer-face"> <a href="#"> <i class="fa fa-user"></i> </a> </div> <div class="col-xs-8 col-sm-8 "> <h3 class="dealer-name"> <a href="#">{{propertyDetails.user.name}}</a><br/> <span>Real Estate Agent</span> </h3> </div> </div> <div class="clear"> <ul class="dealer-contacts"> <li><i class="pe-7s-mail strong"> </i> {{propertyDetails.user.email}}</li> <li v-if="propertyDetails.user.phone"><i class="pe-7s-call strong"> </i> {{propertyDetails.user.phone}}</li> </ul> </div> </div> </div> </div> </template> <script> export default { name: "PropertyUserInfo", props: ["propertyDetails"] } </script> <style scoped> .dealer-face .fa-user { font-size: 96px; color: #fff; } </style>
To test this click on any property on the home page or search page, you should be redirected to property details page with url similar to this “/property/id/slug” with property details.
In the next part we will return back to the property form and implement the edit and delete property.
Continue to part 13: Edit and Delete Property>>>