Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 9: Users Management

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

In this post we will add the users module to manage the website users, so i will start by adding the backend CRUD in lumen then i will connect this to display them in the Nuxtjs admin panel.

 

 

Users CRUD

Let’s open the lumen project and update User model, update the $fillable property and add the $appends property like this:

app/Models/User.php

protected $fillable = [
        'name', 'email', 'is_super_admin', 'password'
    ];

protected $appends = array('create_date');

public function getCreateDateAttribute()
    {
        return $this->created_at ? date('Y-m-d', strtotime($this->created_at)) : "";
    }

The $appends property used to add custom properties to the response of the Eloquent models, so i added getCreateDateAttribute() method to return the create date of the user then i added it’s name to the $appends property in this case when you retrieve users you will see the create_date in the response.

 

 

Users Controller

Create UsersController.php in app/Http/Controllers directory, with the following code:

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class UsersController extends Controller
{
    public function __construct()
    {
        $this->middleware('super_admin_check:store-update-destroy');
    }

    public function index(Request $request)
    {
        $users = $this->filterAndResponse($request);

        return response()->json(['users' => $users], 200);
    }

    public function store(Request $request)
    {
        $validator = $this->getValidator($request);

        if ($validator->fails()) {
            return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $validator->errors()], 500);
        }

        $bundle = $request->except('password_confirmation');

        $bundle['password'] = app('hash')->make($request->input('password'));

        $user = User::create($bundle);

        return response()->json(['success' => 1, 'message' => 'Created successfully', 'user' => $user], 201);
    }

    public function show($id)
    {
        return response()->json(['user' => User::findOrFail($id)], 200);
    }

    public function update(Request $request, $id)
    {
        $user = User::findOrFail($id);

        $validator = $this->getValidator($request, $id);

        if ($validator->fails()) {
            return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $validator->errors()], 500);
        }

        $user->name = $request->input('name');
        $user->email = $request->input('email');
        $user->is_super_admin = $request->input('is_super_admin');

        if($request->input('password') != "") {
            $user->password = app('hash')->make($request->input('password'));
        }

        $user->save();

        return response()->json(['success' => 1, 'message' => 'Updated successfully', 'user' => $user], 200);
    }

    public function destroy($id)
    {
        User::findOrFail($id)->delete();

        return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200);
    }

    /**
     * @param Request $request
     */
    protected function filterAndResponse(Request $request)
    {
        $query = User::with("products", "orders")->orderBy('id', 'DESC');

        if($request->has('all')) {
            return $query->get();
        }

        if ($request->has('id')) {
            $query->where('id', $request->id);
        }

        if ($request->has('name')) {
            $query->where('name', 'like', "%" . $request->name . "%");
        }

        if ($request->has('email')) {
            $query->where('email', 'like', "%" . $request->email . "%");
        }

        $users = $query->paginate(10);

        return $users;
    }

    private function getValidator($request, $id = null)
    {
        $validate_rules = [
            'name' => 'required|min:3',
            'email' => 'required|email|' . ($id == null ? 'unique:users,email' : 'unique:users,email,' . $id),
        ];

        if($id == null || $request->input('password') != "") {
            $validate_rules += ['password' => 'required|min:4|confirmed'];
        }

        $validator = Validator::make($request->all(), $validate_rules);

        return $validator;
    }
}

The above controller is a simple CRUD controller so i will not go into the details of this controller as you already saw similar controllers in previous posts.

Now update routes/web.php

<?php

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->group(['prefix' => 'api'], function () use ($router) {
    $router->post('/login', 'Auth\\LoginController@login');
    $router->post('/register', 'Auth\\RegisterController@register');

    $router->group(['prefix' => 'category'], function () use ($router) {
        $router->get('/', 'CategoriesController@index');
        $router->get('/htmltree', 'CategoriesController@getCategoryHtmlTree');
        $router->get('/{id}', 'CategoriesController@show');
    });

    $router->group(['prefix' => 'brand'], function () use ($router) {
        $router->get('/', 'BrandsController@index');
        $router->get('/{id}', 'BrandsController@show');
    });

    $router->group(['prefix' => 'product'], function () use ($router) {
        $router->get('/', 'ProductsController@index');
        $router->get('/{id}', 'ProductsController@show');
    });

    $router->group(['prefix' => 'user'], function () use ($router) {
        $router->get('/', 'UsersController@index');
        $router->get('/{id}', 'UsersController@show');
    });

    $router->group(['middleware' => 'auth:api'], function () use ($router) {
        $router->get('/me', 'Auth\\LoginController@userDetails');
        $router->get('/logout', 'Auth\\LoginController@logout');
        $router->get('/check-login', 'Auth\\LoginController@checkLogin');

        $router->group(['prefix' => 'category'], function () use ($router) {
            $router->post('/', 'CategoriesController@store');
            $router->put('/{id}', 'CategoriesController@update');
            $router->delete('/{id}', 'CategoriesController@destroy');
        });

        $router->group(['prefix' => 'brand'], function () use ($router) {
            $router->post('/', 'BrandsController@store');
            $router->put('/{id}', 'BrandsController@update');
            $router->delete('/{id}', 'BrandsController@destroy');
        });

        $router->group(['prefix' => 'product'], function () use ($router) {
           $router->post('/', 'ProductsController@store');
            $router->put('/{id}', 'ProductsController@update');
            $router->delete('/delete-image/{id}', 'ProductsController@destroyImage');
            $router->delete('/{id}', 'ProductsController@destroy');
        });

        $router->group(['prefix' => 'user'], function () use ($router) {
            $router->post('/', 'UsersController@store');
            $router->put('/{id}', 'UsersController@update');
            $router->delete('/{id}', 'UsersController@destroy');
        });
    });
});

 

Manipulating Users In Nuxtjs

The second thing we need to do in the Nuxt project in the /admin/ directory is to create the pages responsible for manipulating users.

Users Api

As usual we will start with implementing the apis or services for users so go to /admin/api/ directory and create user.js with following code:

/admin/api/user.js

const UserApi = {
  create: (axios, payload) => {
    return axios.$post('/api/user', payload);
  },
  list: (axios, payload = null) => {

    let payload_arr = [];

    if(payload) {
      for(let key in payload) {
        payload_arr.push(key +"=" + payload[key]);
      }
    }

    return axios.$get('/api/user?' + payload_arr.join("&"));
  },
  getAll: (axios) => {
    return axios.$get('/api/user?all=1');
  },
  delete: (axios, id) => {
    return axios.$delete('/api/user/' + id);
  },
  show: (axios, id) => {
    return axios.$get('/api/user/' + id);
  },
  update(axios, payload, id) {
    return axios.$put('/api/user/' + id, payload);
  }
};

export {UserApi};

Next let’s create the store for users module.

 

Users Vuex Store

As we seen in previous tutorials how the store work in Nuxtjs i prepared the user store, in the same way as we done in previous lesson. So in /admin/store directory create user.js with the below code:

/admin/store/user.js

import { UserApi } from '../api/user';

export const state = () => ({
  user: {
    name: '',
    email: '',
    password: '',
    password_confirmation: '',
    is_super_admin: 0
  },
  filterData: {
    id: '',
    name: '',
    email: ''
  },
  page: 1,
  userList: {},
  allUsers: []
});

export const mutations = {
  setUser(state, payload) {
    state.user[payload.key] = payload.value;
  },
  resetUser(state) {
    state.user = {
      name: '',
      email: '',
      password: '',
      password_confirmation: '',
      is_super_admin: 0
    };
  },
  setFilterData(state, payload) {
    state.filterData[payload.key] = payload.val;
  },
  setPage(state, page) {
    state.page = page;
  },
  setUserList(state, userList) {
    state.userList = userList;
  },
  setAllUsers(state, value) {
    state.allUsers = value;
  }
};

export const actions = {
  create({commit, dispatch, state}, payload) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    const dataToSend = {...state.user};

    UserApi.create(this.$axios, dataToSend).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      if(response.success) {
        commit('shared/setStatusMessageParameter', {key: 'success_message', val: response.message}, {root: true});
      }

      setTimeout(() => {
        payload.router.push('/user');
      }, 2000);

    }).catch(err => {
      dispatch('showValidationErrors', err);
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
      }, 10000);
    });
  },
  list({commit}, payload = null) {
    UserApi.list(this.$axios, payload).then(response => {
      commit('setUserList', response.users);
    });
  },
  getAllUsers({commit}) {
    UserApi.getAll(this.$axios).then(response => {
      commit('setAllUsers', response.users);
    }).catch(err => {
      console.log(err);
    });
  },
  show({commit}, id) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});
    commit('resetUser');

    UserApi.show(this.$axios, id).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});

      if(response.user) {
        commit('setUser', {key: 'name', value: response.user.name});
        commit('setUser', {key: 'email', value: response.user.email});
        commit('setUser', {key: 'is_super_admin', value: response.user.is_super_admin});
      }

    }).catch(err => {
      console.log(err);
    });
  },
  update({commit, state, dispatch}, payload) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    const dataToSend = {...state.user};

    UserApi.update(this.$axios, dataToSend, payload.id).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      if(response.success) {
        commit('shared/setStatusMessageParameter', {key: 'success_message', val: response.message}, {root: true});
      }

      setTimeout(() => {
        payload.router.push('/user');
      }, 2000);
    }).catch(err => {
      dispatch('showValidationErrors', err);
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
      }, 10000);
    });
  },
  delete({commit, state, dispatch}, id) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    UserApi.delete(this.$axios, id).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      if(response.success) {

        let userList = {...state.userList};
        userList.data = userList.data.filter(item => item.id !== id);

        commit('setUserList', userList);
        commit('shared/setStatusMessageParameter', {key: 'success_message', val: response.message}, {root: true});
      }
    }).catch(err => {
      dispatch('showValidationErrors', err);
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
      }, 10000);
    });
  },
  showValidationErrors({commit}, err) {
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
    if(err.response.data) {
      commit('shared/setStatusMessageParameter', {key: 'error_message', val: err.response.data.message}, {root: true});
      if(err.response.data.errors) {
        let errors = [];
        for(let key in err.response.data.errors) {
          errors.push(err.response.data.errors[key][0]);
        }

        commit('shared/setStatusMessageParameter', {key: 'validation_errors', val: errors}, {root: true});
      }
    }
  }
}

 

Users Pages

At this point we are ready to create the pages needed for users display and management so go to /admin/pages and create user/ directory and inside it add these pages:

  • pages/user/index.vue
  • pages/user/create.vue
  • pages/user/_edit.vue

Also in the /admin/components directory create user-components/ directory and add these partial components inside it:

  • components/user-components/UserFilter.vue
  • components/user-components/UserForm.vue
  • components/user-components/UserRow.vue

 

Displaying All Users

Let’s display all users in index.vue page

index.vue

<template>
  <div class="main-content">
    <div class="section__content section__content--p30">
      <div class="container-fluid">
        <div class="row">
          <div class="col-md-12">
            <loader></loader>
            <status-messages></status-messages>

            <UserFilter v-on:Filtering="handleFiltering"></UserFilter>

          </div>
          <div class="col-md-12">
            <!-- DATA TABLE-->
            <div class="table-responsive m-b-40">
              <table class="table table-borderless table-data3">
                <thead>
                <tr>
                  <th>#</th>
                  <th>Name</th>
                  <th>Email</th>
                  <th>Admin</th>
                  <th>Create Date</th>
                  <th>Count Products</th>
                  <th>Count Order</th>
                  <th>Options</th>
                </tr>
                </thead>
                <tbody>
                  <UserRow v-if="userList.data" v-for="user of userList.data" v-bind:user="user" v-bind:key="user.id" v-on:removeUser="removeUser"></UserRow>
                </tbody>
              </table>
            </div>
            <Pagination :data="userList" v-if="userList.data" v-on:handlePagination="handlePagination"></Pagination>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import Loader from "../../components/helpers/loader";
  import statusMessages from "../../components/helpers/statusMessages";
  import UserRow from "../../components/user-components/UserRow";
  import Pagination from "../../components/helpers/Pagination";
  import UserFilter from "../../components/user-components/UserFilter";

  export default {
    name: "index",
    middleware: "auth",
    components: {
      UserFilter,
      Pagination,
      UserRow,
      Loader,
      statusMessages
    },
    fetch() {
      this.$store.dispatch('user/list');
    },
    computed: {
      userList() {
        return this.$store.state.user.userList;
      }
    },
    methods: {
      handlePagination(page_number) {
        this.$store.commit('user/setPage', page_number);

        this.$store.dispatch('user/list', this.getPayload());
      },
      handleFiltering(field, value) {
        this.$store.commit('user/setFilterData', {key: field, val: value});
        this.$store.commit('user/setPage', 1);

        this.$store.dispatch('user/list', this.getPayload());
      },
      getPayload() {
        let payload = {};
        for(let field in this.$store.state.user.filterData) {
          if(this.$store.state.user.filterData.hasOwnProperty(field) && this.$store.state.user.filterData[field] !== '')
            payload[field] = this.$store.state.user.filterData[field];
        }
        payload.page = this.$store.state.user.page;

        return payload;
      },
      removeUser(id) {
        if(confirm("Are you sure?")) {
          this.$store.dispatch('user/delete', id);
        }
      }
    }
  }
</script>

<style scoped>

</style>

/admin/components/user-components/UserFilter.vue

<template>
  <form method="get" action="#" style="margin-bottom: 10px">
    <h4>Filter</h4>
    <div class="row">
      <div class="col-1">
        <input type="text" class="form-control" placeholder="Id" name="id" @change="handleFiltering($event)" :value="this.$store.state.user.filterData.id">
      </div>
      <div class="col-3">
        <input type="text" class="form-control" placeholder="Name" name="name" @change="handleFiltering($event)" :value="this.$store.state.user.filterData.name">
      </div>
      <div class="col-3">
        <input type="text" class="form-control" placeholder="Email" name="email" @change="handleFiltering($event)" :value="this.$store.state.user.filterData.email">
      </div>

      <div class="col-5">
        <nuxt-link to="/user/create" class="btn btn-success pull-right"><i class="fa fa-plus-square"></i> Create</nuxt-link>
      </div>
    </div>
  </form>
</template>

<script>
  export default {
    name: "UserFilter",
    methods: {
      handleFiltering(event) {
        this.$emit('Filtering', event.target.name, event.target.value);
      }
    }
  }
</script>

<style scoped>

</style>

/admin/components/user-components/UserRow.vue

<template>
  <tr>
    <td>{{ this.user.id }}</td>
    <td>{{ this.user.name }}</td>
    <td>{{ this.user.email }}</td>
    <td>
      <span class="badge badge-info" v-if="this.user.is_super_admin == 1">Yes</span>
      <span class="badge badge-warning" v-if="this.user.is_super_admin == 0">No</span>
    </td>
    <td>{{ this.user.create_date }}</td>
    <td>{{ this.user.products.length > 0 ? this.user.products.length : 'none' }}</td>
    <td>{{ this.user.orders.length > 0 ? this.user.orders.length : 'none' }}</td>
    <td>
      <nuxt-link :to="'/user/' + this.user.id" class="btn btn-info btn-sm"><i class="fa fa-edit"></i></nuxt-link>
      <a href="#" class="btn btn-danger btn-sm" @click.prevent="removeUser(user.id)"><i class="fa fa-remove"></i></a>
    </td>
  </tr>
</template>

<script>
  export default {
    name: "UserRow",
    props: ['user'],
    methods: {
      removeUser(userId) {
        this.$emit('removeUser', userId);
      }
    }
  }
</script>

<style scoped>

</style>

 

Creating Users

Open pages/user/create.vue and add the code below

pages/user/create.vue

<template>
    <div class="main-content">
      <div class="section__content section__content--p30">
        <div class="container-fluid">
          <loader></loader>
          <status-messages></status-messages>
          <form method="post" action="#" @submit.prevent="save()">
            <UserForm></UserForm>

            <div class="row">
              <button type="submit" class="btn btn-lg btn-info btn-block">
                <i class="fa fa-save fa-lg"></i> Save
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
</template>

<script>
    import Loader from '../../components/helpers/loader';
    import statusMessages from '../../components/helpers/statusMessages';
    import UserForm from '../../components/user-components/UserForm';

    export default {
        name: "CreateUser",
        middleware: "auth",
        components: {
          Loader,
          statusMessages,
          UserForm
        },
        methods: {
          save() {
            this.$store.dispatch('user/create', {router: this.$router});
          }
        },
        mounted() {
          this.$store.commit('user/resetUser');
        }
    }
</script>

<style scoped>

</style>

As you see in this code we extracted the form into a separate component <UserForm /> like we did in the previous part with products to make things simple.

components/user-components/UserForm.vue

<template>
  <div class="row">
    <div class="col-lg-12">
      <div class="card">
        <div class="card-header">
          <strong>{{ this.$route.params.edit?'Edit User #' + this.$route.params.edit : 'Create New User' }}</strong>
        </div>
        <div class="card-body card-block">
          <div class="form-group">
            <label for="name" class=" form-control-label">Name <span class="required-in">*</span></label>
            <input type="text" id="name" name="name" placeholder="Name" class="form-control" v-model="name" />
          </div>
          <div class="form-group">
            <label for="email" class=" form-control-label">Email <span class="required-in">*</span></label>
            <input type="text" id="email" name="email" placeholder="Email" class="form-control" v-model="email" />
          </div>
          <button type="button" class="btn btn-info" v-if="this.$route.params.edit" @click="togglePasswordBlock()">Change Password</button>
          <div v-if="!this.$route.params.edit || showEditPassword">
            <div class="form-group">
              <label for="password" class=" form-control-label">Password <span class="required-in">*</span></label>
              <input type="password" id="password" name="password" placeholder="Password" class="form-control" v-model="password" />
            </div>
            <div class="form-group">
              <label for="password_confirmation" class=" form-control-label">Password Confirmation <span class="required-in">*</span></label>
              <input type="password" id="password_confirmation" name="password_confirmation" placeholder="Password Confirmation" class="form-control" v-model="password_confirmation" />
            </div>
          </div><br/>
          <div class="form-group">
            <label for="is_super_admin" class=" form-control-label">Is Super Admin</label>
            <label class="switch switch-default switch-primary mr-2" size="lg">
              <input type="checkbox" class="switch-input" name="is_super_admin" id="is_super_admin" value="1" v-model="is_super_admin">
              <span class="switch-label"></span>
              <span class="switch-handle"></span>
            </label>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
    export default {
        name: "UserForm",
        data() {
          return {
            showEditPassword: false
          }
        },
        computed: {
          name: {
            get() {
              return this.$store.state.user.user.name;
            },
            set(value) {
              this.$store.commit('user/setUser', {key: 'name', value});
            }
          },
          email: {
            get() {
              return this.$store.state.user.user.email;
            },
            set(value) {
              this.$store.commit('user/setUser', {key: 'email', value});
            }
          },
          password: {
            get() {
              return this.$store.state.user.user.password;
            },
            set(value) {
              this.$store.commit('user/setUser', {key: 'password', value});
            }
          },
          password_confirmation: {
            get() {
              return this.$store.state.user.user.password_confirmation;
            },
            set(value) {
              this.$store.commit('user/setUser', {key: 'password_confirmation', value});
            }
          },
          is_super_admin: {
            get() {
              return this.$store.state.user.user.is_super_admin;
            },
            set(value) {
              value = value == true ? 1 : 0;
              this.$store.commit('user/setUser', {key: 'is_super_admin', value});
            }
          }
        },
        methods: {
          togglePasswordBlock() {
            this.showEditPassword = !this.showEditPassword;
          }
        }
    }
</script>

<style scoped>

</style>

 

Updating Users

The last page to update is the _edit.vue page which enable us to update users.

pages/user/_edit.vue

<template>
  <div class="main-content">
    <div class="section__content section__content--p30">
      <div class="container-fluid">
        <loader></loader>
        <status-messages></status-messages>
        <form method="post" action="#" @submit.prevent="save()">
          <UserForm></UserForm>

          <div class="row">
            <button type="submit" class="btn btn-lg btn-info btn-block">
              <i class="fa fa-save fa-lg"></i> Save
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
  import Loader from '../../components/helpers/loader';
  import statusMessages from '../../components/helpers/statusMessages';
  import UserForm from '../../components/user-components/UserForm';

  export default {
    name: "EditUser",
    middleware: "auth",
    components: {
      Loader,
      statusMessages,
      UserForm
    },
    fetch() {
       if(this.$route.params.edit) {
         this.$store.dispatch('user/show', this.$route.params.edit);
       }
    },
    methods: {
      save() {
        this.$store.dispatch('user/update', {router: this.$router, id: this.$route.params.edit});
      }
    },
    mounted() {
    }
  }
</script>

<style scoped>

</style>

 

Updating Navbar

Let’s update the navbar to display the link for user module so go to components/partials/NavMenu.vue and update as shown below:

components/partials/NavMenu.vue

<template>
  <ul :class="ulclass">
    <li :class="{active: this.$route.path === '/'}">
      <nuxt-link to="/">
        <i class="fas fa-tachometer-alt"></i>Dashboard
      </nuxt-link>
    </li>
    <li :class="{active: this.$route.path.indexOf('category') !== -1}">
      <nuxt-link to="/category">
        <i class="fas fa-list"></i>Categories
      </nuxt-link>
    </li>
    <li :class="{active: this.$route.path.indexOf('brand') !== -1}">
    <nuxt-link to="/brand">
      <i class="fas fa-television"></i>Brands
    </nuxt-link>
    </li>
    <li :class="{active: this.$route.path.indexOf('product') !== -1}">
      <nuxt-link to="/product">
        <i class="fas fa-shopping-cart"></i>Products
      </nuxt-link>
    </li>
    <li>
      <a href="#">
        <i class="fas fa-gears"></i>Orders</a>
    </li>
    <li>
      <a href="#">
        <i class="fas fa-gears"></i>Pending Orders</a>
    </li>
    <li :class="{active: this.$route.path.indexOf('user') !== -1}">
      <nuxt-link to="/user">
        <i class="fas fa-users"></i>Users
      </nuxt-link>
    </li>
    <li>
      <a href="#">
        <i class="fas fa-sign-out-alt"></i>Logout</a>
    </li>
  </ul>
</template>

<script>
    export default {
        name: "nav-menu",
        props: ['ulclass'],
        mounted() {
        }
    }
</script>

<style scoped>

</style>

Now give this a try by runing “npm run dev” and go to http://localhost:4000/user.

In the next part we will move on by starting to prepare the html template we will be using on the website.

 

Continue to Part10: Preparing Website Html Template

 

0 0 vote
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
Inline Feedbacks
View all comments