Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building a Simple Real estate App With Laravel and Vuejs 3 Part7

Building a Simple Real estate App With Laravel and Vuejs 3

In the previous we prepared the property form UI and make it working. In this part we will start submitting this form to the backend.

 

 

To begin create a resourceful controller for Properties named “PropertyController”:

php artisan make:controller PropertyController --api

The –api option tells laravel to generate CRUD ready controller for api which is like the -r option for resource controllers except that this controller type doesn’t have create and edit methods.

Next add the route:

routes/api.php

Route::apiResource('\property', \App\Http\Controllers\PropertyController::class);

 

Storing Properties

Storing properties is a matter of implementing the store() method. So open the PropertyController and update it like so:

app/Http/Controllers/PropertyController.php

<?php

namespace App\Http\Controllers;

use App\Models\Property;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Validator;

class PropertyController extends Controller
{
    private $editMode = false;

    public function __construct()
    {
        $this->middleware("auth:sanctum")->except(["show"]);
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function store(Request $request)
    {
        if(($validate = $this->validateProperties($request)) !== true) {
            return $validate;
        }

        $payload = [
            'user_id' => auth()->user()->id
        ];

        foreach ($request->except(['features', 'pictures', 'accept_terms']) as $key => $val) {
            $payload[$key] = $val;
        }

        $property = Property::create($payload);

        $property->features()->attach($request->features);

        $this->storeImages($request, $property);

        return response()->json(['status' => true, 'message' => 'Property Created Successfully', 'property' => $property]);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function show($id)
    {
        
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(Request $request, $id)
    {
        
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function destroy($id)
    {
        
    }

    public function userProperties(Request $request)
    {
        
    }

    private function validateProperties(Request $request)
    {
        $rules = [
            'title' => 'required|min:5|string',
            'status' => 'required',
            'price' => 'required|numeric',
            'rent_amount_per' => 'required_if:status,==,Rent',
            'area' => 'required|numeric',
            'country' => 'required|gt:0',
            'state' => 'required|gt:0',
            'bedrooms' => 'integer',
            'bathrooms' => 'integer',
            'rooms' => 'integer',
            'garages' => 'integer',
            'units' => 'integer',
            'floor_number' => 'integer',
            'year_built' => 'integer',
            'features' => 'required|array',
            'features.*' => 'integer',
            'youtube_video' => 'nullable|sometimes|url'
        ];

        if(!$this->editMode) {
            $rules = array_merge($rules, [
                'pictures' => 'required|array',
                'pictures.*' => 'image|mimes:jpeg,png,jpg,gif',
            ]);
        }

        $validator = Validator::make($request->all(), $rules, [
            'features.required' => 'You have to choose at least one or two features',
            'pictures.required' => 'At least one picture is required',
            'country.gt' => 'The country required',
            'state.gt' => 'The state required'
        ]);

        if($validator->fails()) {
            return response()->json(['status' => false, 'errors' => $validator->errors()], 500);
        }

        return true;
    }

    private function storeImages(Request $request, $property)
    {
        if(!file_exists(public_path("/uploads/property/$property->id"))) {
            @mkdir(public_path("/uploads/property"), 0777);
            @mkdir(public_path("/uploads/property/$property->id"), 0777);
        }

        if($request->hasFile('pictures')) {
            $files = $request->file('pictures');

            foreach($files as $file) {
                $extension = $file->getClientOriginalExtension();
                $md5Name = md5_file($file->getRealPath()) . '_' . time().'.'.$extension;
                $file->move(public_path("uploads/property/$property->id"), $md5Name);
                $property->pictures()->create(['picture' => $md5Name]);
            }
        }
    }
}

At first i declared a private property $editMode to indicate if this is an edit or create. In the store() method we walk on a couple of steps which includes validating property params, then preparing the payload to be passed in the Property::create(), then saving the features, then saving the pictures. At the end i return the success response of the created property.

The private method validateProperties() is straightforward, we use laravel validator to validate the incoming data for required or numeric fields, etc. 

The private method storeImages() responsible for iterating over the incoming array of pictures and uploads them to public/uploads/ directory.

Note: You have to create the uploads/ directory in public/ directory and give it a permission of writable.

 

Update the below models below:

For the Property model add the $fillable protected property like so:

app/Models/Property.php

.....
.....

class Property extends Model
{
    .....
    .....

    protected $fillable = ['user_id', 'agency_id', 'title', 'description', 'status', 'price', 'rent_amount_per',
        'area', 'country', 'state', 'city', 'bedrooms', 'bathrooms', 'rooms', 'garages', 'units', 'floor_number',
        'year_built', 'property_finalizing', 'phone', 'youtube_video'];


   ....
   ....
}

Also update the PropertyPicture model as below:

app/Models/PropertyPicture.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class PropertyPicture extends Model
{
    use HasFactory;

    protected $fillable = ['picture', 'property_id'];

    protected $appends = ["image_url"];

    public function getImageUrlAttribute()
    {
        return $this->picture ? url("/uploads/property/$this->property_id/$this->picture") : "";
    }
}

Update the routes/api.php add the user_properties endpoint:

Route::get('/user_properties', [\App\Http\Controllers\PropertyController::class, 'userProperties'])->middleware('auth:sanctum');

Now we finished the backend side, let’s move to the frontend side.

 

Submitting The Property Form

In the previous part we prepared the Form UI and added the composable file useSetupFormWizard.js.

Using the same approach i will create a new composable file for handling for submission.

Create resources/js/composables/useSubmitPropertyForm.js

import {reactive, ref} from "vue";
import {useToast} from "vue-toastification";
import {useRouter, useRoute} from "vue-router";

export const useSubmitPropertyForm = () => {
    const toast = useToast();
    const router = useRouter();
    const route = useRoute();

  const form = reactive({
      title: '',
      description: '',
      status: 'Sale',
      price: '',
      rent_amount_per: '',
      area: '',
      country: 0,
      state: 0,
      city: 0,
      bedrooms: 0,
      bathrooms: 0,
      rooms: 0,
      garages: 0,
      units: 0,
      floor_number: 0,
      year_built: 0,
      property_finalizing: '',
      phone: '',
      youtube_video: '',
      features: [],
      pictures: [],
      accept_terms: (!!window.localStorage.getItem("terms_accepted"))
  });

  const validationErrors = ref([]);

  const pictureFiles = ref([]);

  const handleSelectMainPicture = e => {
        
  }

  const handleSelectOtherPictures = e => {
      
  }

  const handleOnSelectFeature = val => {
      
  }

  const handleOnDeselectFeature = val => {
        
  }

  const handleAcceptTerms = val => {
      
  }

    const handleRefuseTerms = val => {
    
    }

  const handleSubmit = () => {
      
  };

  return {
      form,
      handleSelectMainPicture,
      handleSelectOtherPictures,
      handleOnSelectFeature,
      handleOnDeselectFeature,
      handleAcceptTerms,
      handleRefuseTerms,
      handleSubmit,
      validationErrors
  }
};

In this code i declared a reactive property form which hold the full form object, we will bind these form data with the component inputs below.

Next i defined several handlers which we need like handleSubmit, handleSelectMainPicture, handleOnSelectFeature, handleAcceptTerms, etc. These handlers need to be filled with the appropriate code, but first let’s update the PropertyForm.vue:

resources/js/components/partials/PropertyForm.vue

<template>
    <div class="wizard-container">
        <div class="wizard-card ct-wizard-orange" id="wizardProperty">
            <form action="#" method="post" @submit.prevent="handleSubmit" novalidate="novalidate">
                <div class="wizard-header">
                    <h3>
                        <b>Submit</b> YOUR PROPERTY <br />
                        <small>Please fill all the fields marked as required</small>
                    </h3>
                    <div v-if="validationErrors.length" class="alert alert-danger">
                        <ul>
                            <li v-for="error of validationErrors" :key="error">{{error}}</li>
                        </ul>
                    </div>
                </div>
                <ul class="nav nav-pills">
                    <li style="width: 25%;" :class="{'active': shownStep===1}"><a>Step 1 </a></li>
                    <li style="width: 25%;" :class="{'active': shownStep===2}"><a>Step 2 </a></li>
                    <li style="width: 25%;" :class="{'active': shownStep===3}"><a>Step 3 </a></li>
                    <li style="width: 25%;" :class="{'active': shownStep===4}"><a>Finished </a></li>
                </ul>
                <div class="tab-content">
                    <div class="tab-pane" :class="{'active': shownStep===1}" id="step1" v-show="shownStep === 1">
                        <div class="row p-b-15">
                            <h4 class="info-text">Let's start with the basic information</h4>
                            <div class="col-sm-4 col-sm-offset-1">
                                <div class="picture-container">
                                    <div class="picture">
                                        <img v-if="!pictureFiles.length" src="/template/assets/img/default-property.jpg" class="picture-src" title="select property image" />
                                        <img v-if="pictureFiles.length > 0" :src="pictureFiles[0].image_url" class="picture-src" title="update property image" />
                                        <input type="file" id="wizard-picture" name="mainPicture" accept="image/*" @input="handleSelectMainPicture" />
                                    </div>
                                </div>
                            </div>
                            <div class="col-sm-6">
                                <div class="form-group">
                                    <label>Property title <small>(required)</small></label>
                                    <input name="title" type="text" class="form-control" placeholder="Super villa ..." v-model="form.title" />
                                </div>
                                <div class="form-group">
                                    <label>Property Type: (required)</label>
                                    <select name="status" class="form-control" v-model="form.status">
                                        <option v-for="type in propertyTypes" v-bind:key="type" :value="type">{{type}} </option>
                                    </select>
                                </div>
                                <div class="row">
                                    <div class="col-md-7">
                                        <div class="form-group">
                                            <label>Property Price / Rent <small>(required)</small></label>
                                            <input name="price" type="number" class="form-control" placeholder="3330000" v-model="form.price" />
                                        </div>
                                    </div>
                                    <div class="col-md-5" v-if="form.status === 'Rent'">
                                        <label>Rent Per <small>(for rent only)</small></label>
                                        <select name="rent_amount_per" class="form-control" v-model="form.rent_amount_per">
                                            <option>- Rent Per -</option>
                                            <option v-for="rentPer in rentAmountPerList" :key="rentPer.id" :value="rentPer.id">{{rentPer.name}}</option>
                                        </select>
                                    </div>
                                </div>
                                <div class="form-group">
                                    <label>Phone <small>(empty if you wanna use default phone number)</small></label>
                                    <input name="phone" type="text" class="form-control" placeholder="+1 473 843 7436" v-model="form.phone" />
                                </div>
                                <div class="form-group">
                                    <label>Area <small>(required)</small></label>
                                    <input name="area" type="number" class="form-control" placeholder="200 Square Meter" v-model="form.area" />
                                </div>
                            </div>
                        </div>
                    </div>
                    <!--  End step 1 -->
                    <div class="tab-pane" :class="{'active': shownStep===2}" id="step2" v-show="shownStep === 2">
                        <h4 class="info-text">How much your Property is Beautiful ?</h4>
                        <div class="row">
                            <div class="col-sm-12">
                                <div class="col-sm-12">
                                    <div class="form-group"><label>Property Description :</label>
                                        <textarea name="description" class="form-control" v-model="form.description"></textarea>
                                    </div>
                                </div>
                            </div>
                            <div class="col-sm-12">
                                <div class="col-sm-3">
                                    <div class="form-group">
                                        <label>Property Country :</label>
                                        <SelectPicker title="Select your country" :options="countries" name="country" :selected="form.country" @onChange="onCountryChange" />
                                    </div>
                                </div>
                                <div class="col-sm-3">
                                    <div class="form-group">
                                        <label>Property State :</label>
                                        <SelectPicker title="Select your state" :options="states" name="state" :selected="form.state" @onChange="onStateChange" />
                                    </div>
                                </div>
                                <div class="col-sm-3">
                                    <div class="form-group">
                                        <label>Property City :</label>
                                        <SelectPicker title="Select your city" :options="cities" name="city" :selected="form.city" @onChange="onCityChange" />
                                    </div>
                                </div>
                            </div>
                            <div class="col-sm-12 padding-top-15">
                                <div class="col-sm-2">
                                    <div class="form-group">
                                        <label>Beds :</label>
                                        <input type="number" name="bedrooms" class="form-control" v-model="form.bedrooms" >
                                    </div>
                                </div>
                                <div class="col-sm-2">

                                    <div class="form-group">
                                        <label>Baths :</label>
                                        <input type="number" name="bathrooms" class="form-control" v-model="form.bathrooms" />
                                    </div>
                                </div>
                                <div class="col-sm-2">

                                    <div class="form-group">
                                        <label>Rooms :</label>
                                        <input type="number" name="rooms" class="form-control" v-model="form.rooms" />
                                    </div>
                                </div>
                                <div class="col-sm-2">

                                    <div class="form-group">
                                        <label>Units :</label>
                                        <input type="number" name="units" class="form-control" v-model="form.units" />
                                    </div>
                                </div>
                                <div class="col-sm-2">

                                    <div class="form-group">
                                        <label>Floor number :</label>
                                        <input type="number" name="floor_number" class="form-control" v-model="form.floor_number" />
                                    </div>
                                </div>
                                <div class="col-sm-2">

                                    <div class="form-group">
                                        <label>Garages :</label>
                                        <input type="number" name="garages" class="form-control" v-model="form.garages" />
                                    </div>
                                </div>
                                <div class="col-sm-6">

                                    <div class="form-group">
                                        <label>Year Built:</label>
                                        <select name="year_built" class="form-control" v-model="form.year_built">
                                            <option value="0">- Year Built -</option>
                                            <option v-for="year in getYears()" :key="year" :value="year">{{year}}</option>
                                        </select>
                                    </div>
                                </div>
                                <div class="col-sm-6">

                                    <div class="form-group">
                                        <label>Property Finalizing:</label>
                                        <select name="property_finalizing" class="form-control" v-model="form.property_finalizing">
                                            <option>- Property Finalizing -</option>
                                            <option v-for="finalizing in propertyFinalizingList" :key="finalizing" :value="finalizing">{{finalizing}}</option>
                                        </select>
                                    </div>
                                </div>
                            </div>
                            <br /><br />
                            <h4 style="margin-left:26px">Features</h4>
                            <div class="col-sm-12 padding-top-15 padding-bottom-15">
                                <div class="col-sm-3" v-for="feature in features" v-bind:key="feature.id">
                                    <div class="form-group">

                                        <ICheckInput type="checkbox" :name="'feature'+feature.id" :value="feature.id" :text="feature.title" v-on:onChecked="handleOnSelectFeature" v-on:onUnchecked="handleOnDeselectFeature" :defaultChecked="form.features.includes(feature.id)" />

                                    </div>
                                </div>

                            </div>

                            <br />
                        </div>
                    </div>
                    <!-- End step 2 -->
                    <div class="tab-pane" :class="{'active': shownStep===3}" id="step3" v-show="shownStep === 3">
                        <h4 class="info-text">Give us some images and video</h4>
                        <div class="row">
                            <div class="col-sm-6">
                                <div class="form-group">
                                    <label>Choose Images :</label>
                                    <input class="form-control" type="file" name="pictures" multiple accept="image/*" @input="handleSelectOtherPictures" />
                                    <p class="help-block">Select multiple images for your property .</p>
                                    <ul v-if="pictureFiles.length > 1" class="pictures">
                                        <li v-for="picture in pictureFiles.slice(1, pictureFiles.length)" :key="picture.id">
                                            <img :src="picture.image_url" width="100" height="80" />
                                        </li>
                                    </ul>
                                </div>
                            </div>
                            <div class="col-sm-6">
                                <div class="form-group">
                                    <label>Property video :</label>
                                    <input class="form-control" placeholder="http://www.youtube.com" name="youtube_video" type="text" v-model="form.youtube_video" />
                                </div>
                            </div>
                        </div>
                    </div>
                    <!--  End step 3 -->
                    <div class="tab-pane" :class="{'active': shownStep===4}" id="step4" v-show="shownStep === 4">
                        <h4 class="info-text">Finished and submit</h4>
                        <div class="row">
                            <div class="col-sm-12">
                                <div class="">
                                    <p>
                                        <label><strong>Terms and Conditions</strong></label>
                                        By accessing or using  GARO ESTATE services, such as
                                        posting your property advertisement with your personal
                                        information on our website you agree to the
                                        collection, use and disclosure of your personal information
                                        in the legal proper manner
                                    </p>

                                    <ICheckInput type="checkbox" name="accept_terms" value="1" text="Accept terms and conditions." v-on:onChecked="handleAcceptTerms" v-on:onUnchecked="handleRefuseTerms" :default-checked="form.accept_terms" />

                                </div>
                            </div>
                        </div>
                    </div>
                    <!--  End step 4 -->
                </div>
                <div class="wizard-footer">
                    <div class="pull-right">
                        <input type='button' class='btn btn-next btn-primary' name='next' value='Next' @click="moveForward" v-show="shownStep >= 1 && shownStep < 4" />
                        <input type='submit' class='btn btn-primary ' name='finish' :value="!this.$route.params.id ? 'Finish' : 'Update'" :disabled="!form.accept_terms" v-show="shownStep === 4" />
                    </div>

                    <div class="pull-left">
                        <input type='button' class='btn btn-previous btn-default' name='previous' value='Previous' @click="moveBackward" v-show="shownStep > 1" />
                    </div>
                    <div class="clearfix"></div>
                </div>
            </form>
        </div>
        <!-- End submit form -->
    </div>
</template>

<script>
import {useRoute} from "vue-router";
import ICheckInput from "./ICheckInput";
import SelectPicker from "./SelectPicker";
import {useSetupFormWizard} from "../../composables/useSetupFormWizard";
import {useSubmitPropertyForm} from "../../composables/useSubmitPropertyForm";
import {useLoadStaticData} from "../../composables/useLoadStaticData";

export default {
    name: "PropertyForm",
    components: {SelectPicker, ICheckInput},
    setup(props) {
        const route = useRoute();

        const {form, handleSubmit, handleSelectMainPicture,
            handleSelectOtherPictures, handleAcceptTerms, handleRefuseTerms,
            handleOnDeselectFeature, handleOnSelectFeature, validationErrors, pictureFiles} = useSubmitPropertyForm();

        const {getYears, rentAmountPerList, propertyFinalizingList, propertyTypes,
            features, countries, states, cities, handleSelectCountry, handleSelectState,
            handleSelectCity, selectedCountry, selectedState, selectedCity, loadStates, loadCities} = useLoadStaticData({selectedCountryId: form.country, selectedStateId: form.state, selectedCityId: form.city});

        const {shownStep, moveForward, moveBackward} = useSetupFormWizard();

        const onCountryChange = e => {
            handleSelectCountry(e);
            form.country = selectedCountry.value;
            form.state = '';
            form.city = '';
        }

        const onStateChange = e => {
            handleSelectState(e);
            form.state = selectedState.value;
            form.city = '';
        }

        const onCityChange = e => {
            handleSelectCity(e);
            form.city = selectedCity.value;
        }

        return {
            getYears,
            shownStep,
            moveForward,
            moveBackward,
            propertyTypes,
            rentAmountPerList,
            propertyFinalizingList,
            features,
            countries,
            states,
            cities,
            onCountryChange,
            onStateChange,
            onCityChange,
            form,
            handleSubmit,
            handleSelectMainPicture,
            handleSelectOtherPictures,
            handleAcceptTerms,
            handleRefuseTerms,
            handleOnSelectFeature,
            handleOnDeselectFeature,
            validationErrors,
            pictureFiles
        }
    }
}
</script>

<style scoped>
 .btn[disabled] {
     background-color: #ccc;
     color: #000;
}

 .pictures li {
     display: inline;
 }
</style>

Same as before i imported the newly composable useSubmitPropertyForm.js. Then i obtained the form object, and the handlers and returned them along side the other data to be available in the template.

Then i bound the form inputs using two way model binding with v-model in each input. There are specific inputs that need specific behavior instead of using v-model like the custom component <SelectPicker /> and <ICheckInput /> which i added two handlers v-on:onChecked and v-on:onUnchecked.

Also for the input type file i am using the @input event to get the captured picture to be uploaded.

At the end the finish button which when clicked submits the form i added a disabled attribute in case the user didn’t accept the terms.

Now to make this code fully functional we have to update the missing handlers in useSubmitPropertyForm.js

const handleSelectMainPicture = e => {
        form.pictures = [...form.pictures, e.target.files[0]];
  }

  const handleSelectOtherPictures = e => {
      form.pictures = [...form.pictures, ...e.target.files];
  }

  const handleOnSelectFeature = val => {
      form.features = form.features.find(v => v === parseInt(val)) !== undefined ? form.features : [...form.features, parseInt(val)];
  }

  const handleOnDeselectFeature = val => {
        form.features = form.features.filter(v => v !== parseInt(val));
  }

  const handleAcceptTerms = val => {
      form.accept_terms = true;
  }

    const handleRefuseTerms = val => {
       form.accept_terms = false;
    }

   const handleSubmit = () => {
      const { formData, headers } = getPayload();

      window.localStorage.setItem("terms_accepted", true);

      window.axios.post("/api/property", formData, { headers }).then(response => {
          handleResponse(response);
      }).catch(error => {
          handleResponse(error.response);
      });
  };

  const getPayload = () => {
      const formData = new FormData();

      for(let field in form) {
          if(field === 'features' || field === 'pictures') {
              form[field].forEach(key => {
                  formData.append(`${field}[]`, key);
              });
          } else {
              formData.append(field, form[field]);
          }
      }

      const headers = { 'Content-Type': 'multipart/form-data' };

      return {formData, headers};
  }

  const handleResponse = response => {
      validationErrors.value = [];

      if(response.data.status) {
          toast.success(response.data.message);
          router.push("/user/my-properties");
          return;
      }

      if(response.data.errors) {
          for(let field in response.data.errors) {
              validationErrors.value = [...validationErrors.value, response.data.errors[field][0]];
          }
      }
  }

That’s it now restart your server and run:

npm run watch

Go to the submit property page and fill the form and submit. If everything is ok the form will be saved, you can check your db tables. Try to add some properties to be displayed later in the user properties page.

In the above code when user submits the form successfully he will be redirected to his properties page. In the next part we will display the current user properties.

 

Continue to part 8: Displaying User & Home Properties>>>

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments