Membuat related page pada wordpress

untuk membuat related page , diperlukan pengelompokan data berdasarkan category atau tag. taxonomy category pada wordpress umumnya tersedia pada tipe post posts , namun dimungkinkan juga untuk menambahkannya ke dalam tipe post page atau custom post type , berikut ini cara nya.

Untuk menambahkan ke tipe post page , bisa digunakan fungsi wp register_taxonomy_for_object_type bisa ditambahkan di dalam file functions.php

 /*add tag support to pages*/
function categories_support_page() {
  register_taxonomy_for_object_type('category', 'page');  
}
add_action('init', 'categories_support_page');

setelah ditambahkan maka link untuk categories akan muncul pada halaman menuenter image description here

Kemudian untuk menampilkan nya

<?php
$sql = new WP_Query(array(
 'post_type' => 'page',
'category_name'  => 'villa',
'posts_per_page' => -1,
'post__not_in'   => array( get_queried_object_id() ),
));
if ($sql->have_posts()){
while($sql->have_posts()){
$sql->the_post();
 $thumbnail = wp_get_attachment_image_src( get_post_thumbnail_id($sql->ID), 'medium' );

?>
<?php the_title();?>
<img src="<?php echo $thumbnail[0]?>">
<?php }}?>

source:https://github.com/baliwebmaker/villagjls

Menambahkan reCAPTCHA V3 dalam Form WordPress

Untuk mencegah spam maka biasanya ada mekanisme penangkal yang harus di pasang pada form yang akan mengirimkan email, salah satu yang tersedia dan banyak dipakai adalah recaptcha dari google.

Langkah awal

  • Daftar untuk kode API keys di reCAPTCHA buka https://www.google.com/recaptcha/admin/create
  • Login menggunakan akun google
  • isi Label contoh wp.lokal
  • isi domain , contoh saya pakai wp.lokal

Integrasi Google reCAPTCHA pada website letak kan pada client-side dan the server-side. Pada kasus saya akan meletakan html nya pada functions.php dengan memasukan script API recaptcha ke header tag menggunakan

wp_enqueue_script('recaptcha','https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key', array(), null, false );
Google reCAPTCHA v3 tidak menampikan form recaptcha, berbeda dengan v2 yang akan menampilkan form challenge, jadi diperlukan sebuah script yang akan menangkap google captcha token pada theme yang saya buat ini saya taruh di file reservation-form.js
let captcha = document.getElementById('recaptchaToken');
grecaptcha.ready(() => {
            grecaptcha.execute('reCAPTCHA_site_key', {action: 'submit_reservation_form'})
            .then((token) => {
                this.$refs.recaptchaToken.value = token;
            });
            // refresh token every minute to prevent expiration
            setInterval(() => {
            grecaptcha.execute('reCAPTCHA_site_key', {action: 'submit_reservation_form'})
            .then((token) => {
                this.$refs.recaptchaToken.value = token;
            });
            }, 60000);
        });

Lalu di sisi form html nya yang ada di file reservation-form.php ditambahkan input hidden dengan menambahkan atribute x-ref milik alpine js

<input type="hidden" id="recaptchaToken" name="recaptchaToken" x-ref="recaptchaToken" />

value input ini akan terisi oleh token yang sudah di ambil dari API recaptcha yg dimasukan di header tag, lalu dibaca oleh script grecaptcha , yang valuenya dimasukan kedalam input recaptchaToken memakai $refs alpine js kita akan mengetahui bahwa API script recaptcha sudah ter load pada halaman kita dibagian bawah akan muncul badge recaptcha

enter image description here

token ini akan dikirimkan ke server side php untuk diproses, pada kasus saya ini saya tambahkan +’&captcha=’+captcha.value dalam parameter yg dikirim melalui fetch ke admin-ajax.php milik WP

fetch( SiteParameters.ajax_url, {
 ------------
body: 'action=submit_reservation_form&nonce='+ nonce +'&formdata='+ JSON.stringify( this.formData )+'&captcha='+captcha.value  , 
 })

variabel ini lebih lanjut akan di proses di file utilities.php melalui

function submit_reservation_form() {
        $captcha = filter_input(INPUT_POST, 'captcha', FILTER_SANITIZE_STRING);

        if(!$captcha){
            echo 'please check captcha form';
        }       

        $secretKey='-----taruh secretkey------';
        $url = 'https://www.google.com/recaptcha/api/siteverify?secret='.urlencode($secretKey).'&response='.urlencode($captcha);
        $response = file_get_contents($url);
        $responseKey = json_decode($response, true);

WordPress form dengan TailwindCSS dan Alpine JS

Form wordpress yang dibuat dari awal dengan mengaplikasikan tailwindcss untuk styling nya dan alpine js , file ini adalah bagian dari sebuah theme wordpress yang sedang saya coba bangun tanpa menggunakan Jquery dan bootstrap css yang umumnya digunakan

Halaman Villa Page

<!-- template-parts/villa-page -->
<div class="md:flex md:container md:mx-auto md:-mt-24">
<div class="w-full md:w-7/12">
<main class="p-5 text-sm text-gray-500">
    <div class="villadetail">
    <?php if (have_posts()) : while (have_posts()) : the_post(); ?>
    <h1 class="md:text-2xl text-md md:text-white font-medium uppercase md:text-3xl tracking-wider">
       <?php the_title() ?>
    </h1>
    <div class="text-gray-500 text-xs my-10" aria-label="Breadcrumb">
        <ol class="list-none p-0 inline-flex">
            <li class="flex items-center">
                <a href="<?php echo esc_url( home_url( '/' ) ); ?>">Home</a><svg class="fill-current w-3 h-3 mx-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/></svg>
            </li>
            <li class="flex items-center">
                <a href="<?php echo esc_url( home_url( '/villas/' ) ); ?>">Villas</a>
            </li>
        </ol> 
    </div>
    <?php the_content() ?>
    <?php endwhile; endif; ?>
    </div>
</main>
</div>
<!--sidebar -->
<div class="w-full md:w-5/12">
<aside class="p-5">
<div class="bg-white w-full m-auto boder-1 border-dashed border-gray-100 shadow-md rounded-lg overflow-hidden">
    <div class="p-4 border-2">
        <h2 class="mb-1 text-gray-700 font-semibold text-sm pb-3 uppercase tracking-wide">
            Reservation
        </h2>
        <p class="text-sm pb-3">Questions?</p>
        <div class="flex">
            <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24" style=" fill:#000000;"><path d="M 12.011719 2 C 6.5057187 2 2.0234844 6.478375 2.0214844 11.984375 C 2.0204844 13.744375 2.4814687 15.462563 3.3554688 16.976562 L 2 22 L 7.2324219 20.763672 C 8.6914219 21.559672 10.333859 21.977516 12.005859 21.978516 L 12.009766 21.978516 C 17.514766 21.978516 21.995047 17.499141 21.998047 11.994141 C 22.000047 9.3251406 20.962172 6.8157344 19.076172 4.9277344 C 17.190172 3.0407344 14.683719 2.001 12.011719 2 z M 12.009766 4 C 14.145766 4.001 16.153109 4.8337969 17.662109 6.3417969 C 19.171109 7.8517969 20.000047 9.8581875 19.998047 11.992188 C 19.996047 16.396187 16.413812 19.978516 12.007812 19.978516 C 10.674812 19.977516 9.3544062 19.642812 8.1914062 19.007812 L 7.5175781 18.640625 L 6.7734375 18.816406 L 4.8046875 19.28125 L 5.2851562 17.496094 L 5.5019531 16.695312 L 5.0878906 15.976562 C 4.3898906 14.768562 4.0204844 13.387375 4.0214844 11.984375 C 4.0234844 7.582375 7.6067656 4 12.009766 4 z M 8.4765625 7.375 C 8.3095625 7.375 8.0395469 7.4375 7.8105469 7.6875 C 7.5815469 7.9365 6.9355469 8.5395781 6.9355469 9.7675781 C 6.9355469 10.995578 7.8300781 12.182609 7.9550781 12.349609 C 8.0790781 12.515609 9.68175 15.115234 12.21875 16.115234 C 14.32675 16.946234 14.754891 16.782234 15.212891 16.740234 C 15.670891 16.699234 16.690438 16.137687 16.898438 15.554688 C 17.106437 14.971687 17.106922 14.470187 17.044922 14.367188 C 16.982922 14.263188 16.816406 14.201172 16.566406 14.076172 C 16.317406 13.951172 15.090328 13.348625 14.861328 13.265625 C 14.632328 13.182625 14.464828 13.140625 14.298828 13.390625 C 14.132828 13.640625 13.655766 14.201187 13.509766 14.367188 C 13.363766 14.534188 13.21875 14.556641 12.96875 14.431641 C 12.71875 14.305641 11.914938 14.041406 10.960938 13.191406 C 10.218937 12.530406 9.7182656 11.714844 9.5722656 11.464844 C 9.4272656 11.215844 9.5585938 11.079078 9.6835938 10.955078 C 9.7955938 10.843078 9.9316406 10.663578 10.056641 10.517578 C 10.180641 10.371578 10.223641 10.267562 10.306641 10.101562 C 10.389641 9.9355625 10.347156 9.7890625 10.285156 9.6640625 C 10.223156 9.5390625 9.737625 8.3065 9.515625 7.8125 C 9.328625 7.3975 9.131125 7.3878594 8.953125 7.3808594 C 8.808125 7.3748594 8.6425625 7.375 8.4765625 7.375 z"></path></svg> 
            <p class="text-sm ml-2 text-gray-600">+62 361 0000 000</p>
        </div>

        <div class="flex">
            <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 50 50" style=" fill:#000000;"><path d="M 14 3.9902344 C 8.4886661 3.9902344 4 8.4789008 4 13.990234 L 4 35.990234 C 4 41.501568 8.4886661 45.990234 14 45.990234 L 36 45.990234 C 41.511334 45.990234 46 41.501568 46 35.990234 L 46 13.990234 C 46 8.4789008 41.511334 3.9902344 36 3.9902344 L 14 3.9902344 z M 14 5.9902344 L 36 5.9902344 C 40.430666 5.9902344 44 9.5595687 44 13.990234 L 44 35.990234 C 44 40.4209 40.430666 43.990234 36 43.990234 L 14 43.990234 C 9.5693339 43.990234 6 40.4209 6 35.990234 L 6 13.990234 C 6 9.5595687 9.5693339 5.9902344 14 5.9902344 z M 18.048828 11.035156 C 16.003504 10.946776 14.45113 11.723922 13.474609 12.658203 C 12.986349 13.125343 12.633832 13.625179 12.392578 14.091797 C 12.151324 14.558415 11.998047 14.943108 11.998047 15.443359 C 11.998047 15.398799 11.987059 15.632684 11.980469 15.904297 C 11.973869 16.17591 11.97507 16.542045 12 16.984375 C 12.04996 17.869036 12.199897 19.065677 12.597656 20.484375 C 13.393174 23.321771 15.184446 27.043821 19.070312 30.929688 C 22.95618 34.815554 26.678014 36.606575 29.515625 37.402344 C 30.93443 37.800228 32.130881 37.949937 33.015625 38 C 33.457997 38.02503 33.822105 38.026091 34.09375 38.019531 C 34.365395 38.012931 34.601049 38.001953 34.556641 38.001953 C 35.056892 38.001953 35.441585 37.848676 35.908203 37.607422 C 36.374821 37.366168 36.874657 37.013651 37.341797 36.525391 C 38.276078 35.54887 39.053222 33.996496 38.964844 31.951172 C 38.922907 30.975693 38.381316 30.111858 37.582031 29.599609 C 36.96435 29.203814 36.005458 28.589415 34.753906 27.789062 C 33.301811 26.861299 31.44451 26.795029 29.929688 27.625 L 30.015625 27.582031 L 28.837891 28.087891 L 28.751953 28.148438 C 28.465693 28.349428 28.111154 28.386664 27.789062 28.251953 C 26.886813 27.874649 25.480985 27.133329 24.173828 25.826172 C 22.866671 24.519015 22.125351 23.113186 21.748047 22.210938 C 21.613336 21.888845 21.650568 21.534307 21.851562 21.248047 L 21.912109 21.162109 L 22.417969 19.984375 L 22.375 20.070312 C 23.204764 18.555868 23.140248 16.698619 22.210938 15.246094 C 21.410584 13.994542 20.796186 13.03565 20.400391 12.417969 C 19.888142 11.618684 19.02431 11.077096 18.048828 11.035156 z M 17.962891 13.033203 C 18.243409 13.045263 18.533045 13.209378 18.716797 13.496094 C 19.113001 14.114413 19.727696 15.07377 20.527344 16.324219 C 21.058033 17.153694 21.09533 18.243821 20.621094 19.109375 L 20.597656 19.152344 L 20.115234 20.279297 L 20.214844 20.097656 C 19.623835 20.939396 19.505055 22.032514 19.902344 22.982422 C 20.35304 24.060173 21.214923 25.695392 22.759766 27.240234 C 24.304608 28.785077 25.939827 29.64696 27.017578 30.097656 C 27.967486 30.494945 29.060604 30.376165 29.902344 29.785156 L 29.720703 29.884766 L 30.847656 29.402344 L 30.890625 29.378906 C 31.755801 28.904877 32.845877 28.944375 33.675781 29.474609 L 33.675781 29.472656 C 34.92623 30.272304 35.885587 30.886999 36.503906 31.283203 C 36.790622 31.466955 36.954736 31.756591 36.966797 32.037109 C 37.032417 33.555785 36.504954 34.506599 35.896484 35.142578 C 35.59225 35.460568 35.262335 35.691348 34.990234 35.832031 C 34.718133 35.972715 34.457889 36.001953 34.556641 36.001953 C 34.373232 36.001953 34.276633 36.013981 34.046875 36.019531 C 33.817117 36.025131 33.509144 36.025436 33.128906 36.003906 C 32.368431 35.960876 31.318757 35.831053 30.054688 35.476562 C 27.526547 34.767581 24.137509 33.168759 20.484375 29.515625 C 16.831241 25.862491 15.232169 22.473167 14.523438 19.945312 C 14.169071 18.681386 14.039037 17.631464 13.996094 16.871094 C 13.974624 16.490908 13.974899 16.18286 13.980469 15.953125 C 13.986069 15.72339 13.998047 15.626918 13.998047 15.443359 C 13.998047 15.542109 14.027287 15.281867 14.167969 15.009766 C 14.308652 14.737665 14.539432 14.40775 14.857422 14.103516 C 15.493401 13.495046 16.444215 12.967581 17.962891 13.033203 z"></path></svg>
            <p class="text-sm ml-2 text-gray-600">+62 888 0000 000</p>
        </div>
        <p class="text-sm pt-5 text-gray-600">
            information : [email protected]
        </p>
        <div class="mt-8 mb-3">
            <div class="relative" x-data="{ toggle:null }"> 

            <button type="button"
            @click="toggle !== 1 ? toggle = 1 : toggle = null"
            x-ref="reservationbutton"
            class="px-4 py-2 bg-blue-600 shadow-lg border rounded-lg text-white uppercase text-sm tracking-wider focus:outline-none focus:shadow-outline hover:bg-blue-800 active:bg-blue-400">
            Reservation
            </button>

            <div class="relative overflow-hidden transition-all max-h-0 duration-700"
            x-ref="reservation"
            :style="toggle == 1 ? 'max-height: '+ $refs.reservation.scrollHeight + 'px' :''"
            >          
                <div class="p-2">
                <p class="text-xs">
                 This is only an enquiry regarding the villa; it does not constitute a reservation.
                 <br />Fields (*) required.
                </p>
                <!--reservation-form-->
                <?php get_template_part( 'template-parts/reservation-form');?>
                </div>
            </div>
            </div>
        </div>
    </div>
</div>
<!--Gallery -->
<?php get_template_part( 'template-parts/gallery');?>
</aside>
</div>
</div>

Reservation-form PHP

<form 
    method="post" 
    id="form-reservation" 
    data-nonce="<?php echo wp_create_nonce('submit_reservation_form_nonce');?>"
    @submit.prevent="submitForm"
    x-data="ReservationForm"
>
<!--modal success -->
<?php get_template_part('template-parts/success-modal');?>

<input type="hidden" name="subject" value="Reservation for <?php the_title() ?>" />
<div class="mt-8 max-w-md">
     <div class="grid grid-cols-1 gap-6 text-sm">
        <label class="block">
            <span class="text-gray-700">Full name *</span>
            <input type="text"
            class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm 
            focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
            name="fullname" autocomplete="off" placeholder="" x-model="formData.fullname" x-ref="fullname"/>
            <p 
            class="text-xs text-red-600"
            x-text="errors.fullname?'Please fill fullname':''" 
            >
            </p><span class="text-lg font-bold"></span>
        </label>
        <label class="block">
            <span class="text-gray-700">Email address *</span>
            <input type="email"
            class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm
                focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 placeholder-gray-400"
                name="email" autocomplete="off"
                placeholder="[email protected]"
                x-model="formData.email" 
                x-ref="email"
            />
            <p 
            class="text-xs text-red-600"
            x-text="errors.email?'Please fill email':''"
            >
            </p>
        </label>
        <label class="block">
            <span class="text-gray-700">Phone *</span>
            <input type="text"                   
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm
                focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
             name="phone" placeholder="" x-model="formData.phone" x-ref="phone"/>
            <p 
            class="text-xs text-red-600"
            x-text="errors.phone?'Please fill phone':''"
            >
            </p>
        </label>
        <div class="relative" @keydown.escape="datepicker.closeDatepicker()" @click.away="datepicker.closeDatepicker()">
        <label class="block">
            <span class="text-gray-700">Check IN</span>
            <input type="text"                
            class="mt-1 block w-full rounded-md border-gray-300 shadow-sm
                focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 text-sm"
                name="checkin" autocomplete="off"
                @click="datepicker.endToShow = 'from'; datepicker.initDate(); datepicker.showDatepicker = true" 
                x-model="datepicker.dateFromValue" x-ref="checkin"
                :class="{'font-semibold': datepicker.endToShow == 'from' }"
                />
        </label>
        <p 
          class="text-xs text-red-600"
          x-text="errors.checkin?'Please select date':''"
        >
        </p>
        <label class="block">
            <span class="text-gray-700">Check Out</span>
            <input type="text"
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm
                focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                name="checkout" autocomplete="off"
                @click="datepicker.endToShow = 'to'; datepicker.initDate(); datepicker.showDatepicker = true" 
                x-model="datepicker.dateToValue" 
                :class="{'font-semibold': datepicker.endToShow == 'to' }"
                />
        </label>
        <?php get_template_part( 'template-parts/date-modal');?>
        </div>
        <label class="block">
            <span class="text-gray-700">Number of guests</span>
            <input type="number" min="0" step="1"             
            class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm
                focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                name="number_of_guest" placeholder="" x-model="formData.number_of_guest"/>
        </label>
        <label class="block">
            <span class="text-gray-700">Request</span>
            <textarea                
                class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm
                focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 placeholder-gray-400"
                rows="6" 
                name="request"
                placeholder="Please advise flight details for airport pick-up, or estimated arrival time at Villa, if travelling with infant(s), and any special requests, eg kid’s equipment / dietary needs / villa set-up."
                x-model="formData.request"
            >

resevationform.js

'use strict';

window.ReservationForm  = function () {

    const formReservation = document.getElementById('form-reservation');
    let nonce = formReservation.dataset.nonce;
    let fields = ['fullname','email','phone','checkin','checkout','number_of_guest','request'];
    let datepicker = new datePicker();

    return {

        buttonLabel: 'Submit',
        loading: false,
        status: false,

        modalHeaderText:'',
        modalBodyText:'',

        datepicker,

        //create formdata objects from array fields
        formData: Object.assign({}, ...Object.entries({...fields}).map(([a,b]) => ({ [b]: '' }))),

        //create errors objects from array fields
        errors : Object.assign({}, ...Object.entries({...fields}).map(([a,b]) => ({ [b]: false }))),

        isEmail(email){
            const rgx = /^(([^<>()[].,;:s@"]+(.[^<>()[].,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/
            return rgx.test(email);
        },

        validation(q,w){
            var validate=false;
            if (!q){
                this.errors[w] = true;
                this.$refs[w].scrollIntoView();
                validate=true;
            }else{
                this.errors[w]= false;
            }
            return validate;
        },

        submitForm() {  
            //compensate height of reservation when error messages display
            this.$refs.reservation.style.maxHeight = '100%';

            //validations
            if(
                this.validation(this.formData.fullname.length>0, 'fullname')||
                this.validation(this.isEmail(this.formData.email), 'email')||
                this.validation(this.formData.phone.length > 0, 'phone')||
                this.validation(this.datepicker.dateFromValue.length > 0, 'checkin')
            ){ return; }

            this.formData.checkin = this.datepicker.dateFromValue;
            this.formData.checkout = this.datepicker.dateToValue;

            this.buttonLabel = 'Submitting...';
            this.loading = true;

            fetch( SiteParameters.ajax_url, {
                method: "POST",
                headers: { 
                    'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
                    'Cache-Control': 'no-cache',
                    'Accept': 'application/json' 
                },
                credentials: 'same-origin',
                body: 'action=submit_reservation_form&nonce='+ nonce +'&formdata='+ JSON.stringify( this.formData ) ,
            })
            .then( async (response) => {
                let data = await response.json();

                if (data.status == 'true') {
                    this.$nextTick(() => {
                        //scroll up reservation form container
                        this.$refs.reservationbutton.dispatchEvent(new Event("click"));
                        this.status = true;
                        this.modalHeaderText = "Thank You!";
                        this.modalBodyText = "Your form have been successfully submited!";
                    });
                 } else { 
                    throw new Error("Your registration failed");
                 }
                 //clear forms data after form submitted
                 Object.keys(this.formData).forEach(key => this.formData[key]='');
                 //clear submitted date picker
                 this.datepicker.unsetDateValues();

            })
            .catch(() => {

            })
            .finally(() => {
                this.loading = false;
                this.buttonLabel = 'Submit'
                this.status = false;
                document.getElementById("form-reservation").reset();
            });        
        },
    }
}

success-modal.php pop up window container sebagai alert window setelah fetch ajax success return true

 <!-- modal starts -->
    <div 
    x-cloak
    x-show.transition="status" 
    class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
    >
    <div 
    class="max-w-3xl px-6 py-4 mx-auto text-left bg-white rounded shadow-lg"
    @click.away="status = false"
    x-transition:enter="motion-safe:ease-out duration-300"
    x-transition:enter-start="opacity-0 scale-90"
    x-transition:enter-end="opacity-100 scale-100"
    >
        <div class="bg-white rounded px-8 py-8 max-w-lg mx-auto">
            <div class="flex items-center justify-between">
            <h5 class="mr-3 text-black max-w-none" x-text="modalHeaderText"></h5>          
            <span class="mr-3 text-black max-w-none" x-text="modalBodyText"></span>          
          <button type="button" class="z-50 cursor-pointer" @click="status = false">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
          </svg>
          </button>
            </div>
        </div>         
    </div>
    </div>
<!-- modal ends -->

datepicker.js , date range component yang di implementasikan kedalam form

const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

window.datePicker  = function () {
            return {
                MONTH_NAMES, 
                DAYS ,
                showDatepicker: false,
                dateFromYmd: '',
                dateToYmd: '',
                outputDateFromValue: '',
                outputDateToValue: '',
                dateFromValue: '',
                dateToValue: '',
                currentDate: null,
                dateFrom: null,
                dateTo: null,
                endToShow: '',
                selecting: false,
                month: '',
                year: '',
                no_of_days: [],
                blankdays: [],

                convertFromYmd(dateYmd) {
                    const year = Number(dateYmd.substr(0, 4));
                    const month = Number(dateYmd.substr(5, 2)) - 1;
                    const date = Number(dateYmd.substr(8, 2));

                    return new Date(year, month, date);
                },

                convertToYmd(dateObject) {
                    const year = dateObject.getFullYear();
                    const month = dateObject.getMonth() + 1;
                    const date = dateObject.getDate();

                    return year + "-" + ('0' + month).slice(-2) + "-" +  ('0' + date).slice(-2);
                },

                initDate() {
                    this.selecting = ( this.endToShow === 'to' && this.dateTo ) || ( this.endToShow === 'from' && this.dateFrom);
                    if ( ! this.dateFrom ) {
                        if ( this.dateFromYmd ) {
                            this.dateFrom = this.convertFromYmd( this.dateFromYmd );
                        }
                    }
                    if ( ! this.dateTo ) {
                        if ( this.dateToYmd ) {
                            this.dateTo = this.convertFromYmd( this.dateToYmd );
                        }
                    }
                    if ( ! this.dateFrom ) {
                        this.dateFrom = this.dateTo;
                    }
                    if ( ! this.dateTo ) {
                        this.dateTo = this.dateFrom;
                    }
                    if ( this.endToShow === 'from' && this.dateFrom ) {
                        this.currentDate = this.dateFrom;
                    } else if ( this.endToShow === 'to' && this.dateTo ) {
                        this.currentDate = this.dateTo;
                    } else {
                        this.currentDate = new Date();
                    }
                    currentMonth = this.currentDate.getMonth();
                    currentYear = this.currentDate.getFullYear();
                    if ( this.month !== currentMonth || this.year !== currentYear ) {
                        this.month = currentMonth;
                        this.year = currentYear;
                        this.getNoOfDays();
                    }
                    this.setDateValues();
                },

                isToday(date) {
                    const today = new Date();
                    const d = new Date(this.year, this.month, date);

                    return today.toDateString() === d.toDateString();
                },

                isDateFrom(date) {
                    const d = new Date(this.year, this.month, date);

                    if ( !this.dateFrom ) {
                        return false;
                    }

                    return d.getTime() === this.dateFrom.getTime();
                },

                isDateTo(date) {
                    const d = new Date(this.year, this.month, date);

                    if ( !this.dateTo ) {
                        return false;
                    }

                    return d.getTime() === this.dateTo.getTime();
                },

                isInRange(date) {
                    const d = new Date(this.year, this.month, date);

                    return d > this.dateFrom && d < this.dateTo;
                },

                outputDateValues() {
                    if (this.dateFrom) {
                        this.outputDateFromValue = this.dateFrom.toDateString();
                        this.dateFromYmd = this.convertToYmd(this.dateFrom);
                    }
                    if (this.dateTo) {
                        this.outputDateToValue = this.dateTo.toDateString();
                        this.dateToYmd = this.convertToYmd(this.dateTo);
                    }
                },

                setDateValues() {
                    if (this.dateFrom) {
                        this.dateFromValue = this.dateFrom.toDateString();
                    }
                    if (this.dateTo) {
                        this.dateToValue = this.dateTo.toDateString();
                    }
                },

                getDateValue(date, temp) {
                    // if we are in mouse over mode but have not started selecting a range, there is nothing more to do.
                    if (temp && !this.selecting) {
                        return;
                    }
                    let selectedDate = new Date(this.year, this.month, date);
                    if ( this.endToShow === 'from' ) {
                        this.dateFrom = selectedDate;
                        if ( ! this.dateTo ) {
                            this.dateTo = selectedDate;
                        } else if ( selectedDate > this.dateTo ) {
                            this.endToShow = 'to';
                            this.dateFrom = this.dateTo;
                            this.dateTo = selectedDate;
                        }
                    } else if ( this.endToShow === 'to' ) {
                        this.dateTo = selectedDate;
                        if ( ! this.dateFrom ) {
                            this.dateFrom = selectedDate;
                        } else if ( selectedDate < this.dateFrom ) {
                            this.endToShow = 'from';
                            this.dateTo = this.dateFrom;
                            this.dateFrom = selectedDate;
                        }
                    }
                    this.setDateValues();

                    if (!temp) {
                        if (this.selecting) {
                            this.outputDateValues();
                            this.closeDatepicker();
                        }
                        this.selecting = !this.selecting;
                    }
                },

                getNoOfDays() {
                    let daysInMonth = new Date(this.year, this.month + 1, 0).getDate();

                    // find where to start calendar day of week
                    let dayOfWeek = new Date(this.year, this.month).getDay();
                    let blankdaysArray = [];
                    for ( var i=1; i <= dayOfWeek; i++) {
                        blankdaysArray.push(i);
                    }

                    let daysArray = [];
                    for ( var i=1; i <= daysInMonth; i++) {
                        daysArray.push(i);
                    }

                    this.blankdays = blankdaysArray;
                    this.no_of_days = daysArray;
                },

                closeDatepicker() {
                    this.endToShow = '';
                    this.showDatepicker = false;
                },

                unsetDateValues() {
                    this.dateFromYmd= '';
                    this.dateToYmd= '';
                    this.outputDateFromValue= '';
                    this.outputDateToValue= '';
                    this.dateFromValue= '';
                    this.dateToValue= '';
                    this.currentDate= null;
                    this.dateFrom= null;
                    this.dateTo= null;
                    this.endToShow= '';
                    this.selecting= false;
                },

            }
        }

datemodal php, pop up window yang menampung tabel tanggal dari datepicker.js

<div 
class="absolute bg-white mt-0 rounded-lg shadow p-4  w-full z-80" 
x-show.transition="datepicker.showDatepicker"
>
<div class="flex flex-col items-center">
    <div class="w-full flex justify-between items-center mb-2">

    <div>
        <span x-text="datepicker.MONTH_NAMES[datepicker.month]" class="text-sm font-bold text-gray-800"></span>
        <span x-text="datepicker.year" class="ml-1 text-sm text-gray-600 font-normal"></span>
    </div>

    <div>
        <button 
            type="button"
            class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-gray-200 p-1 rounded-full" 
            @click="if (datepicker.month == 0) {datepicker.year--; datepicker.month=11;} else {datepicker.month--;} datepicker.getNoOfDays()">
            <svg class="h-4 w-4 text-gray-500 inline-flex"  fill="none" viewBox="0 0 20 20" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
            </svg>  
        </button>        
        <button 
            type="button"
            class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-gray-200 p-1 rounded-full" 
            @click="if (datepicker.month == 11) {datepicker.year++; datepicker.month=0;} else {datepicker.month++;} datepicker.getNoOfDays()">
            <svg class="h-4 w-4 text-gray-500 inline-flex"  fill="none" viewBox="0 0 20 20" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
            </svg>  
        </button>
    </div>            
    </div>
    <div class="w-full flex flex-wrap mb-3 -mx-1">
        <template x-for="(day, index) in datepicker.DAYS" :key="index">    
            <div style="width: 14.26%" class="px-1">
            <div
                x-text="day" 
                class="text-gray-800 font-medium text-center text-xs"
            ></div>
            </div>
        </template>
    </div>
    <div class="flex flex-wrap -mx-1">
        <template x-for="blankday in datepicker.blankdays">
        <div 
        style="width: 14.28%"
        class="text-center border p-1 border-transparent text-sm"   
        ></div>
        </template> 
        <template x-for="(date, dateIndex) in datepicker.no_of_days" :key="dateIndex"> 
            <div style="width: 14.28%" >
                <div
                    @click="datepicker.getDateValue(date, false)"
                    /*@mouseenter="datepicker.getDateValue(date, true)"*/
                    x-text="date"
                    class="p-0 cursor-pointer text-center text-xs leading-loose transition ease-in-out duration-100"
                    :class="{'font-bold': datepicker.isToday(date) == true, 'bg-blue-800 text-white rounded-l-full': datepicker.isDateFrom(date) == true, 'bg-blue-800 text-white rounded-r-full': datepicker.isDateTo(date) == true, 'bg-blue-200': datepicker.isInRange(date) == true }"  
                ></div>
            </div>
        </template>
    </div>
    <div>
        <span class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-gray-200 p-1" @click="datepicker.unsetDateValues()">Clear</span>
    </div>
    </div>
</div>

kode php untuk memproses data yang dikirimkan dari form

/******* SENDING MAIL *******/
    function wpdocs_set_html_mail_content_type() {
        return 'text/html';
    }
    add_filter('wp_mail_content_type', 'wpdocs_set_html_mail_content_type' );
    add_action('wp_ajax_submit_reservation_form', 'submit_reservation_form');
    add_action('wp_ajax_nopriv_submit_reservation_form', 'submit_reservation_form');

    function submit_reservation_form() {

        $nonce = $_REQUEST['nonce'];
        if (! wp_verify_nonce($nonce , 'submit_reservation_form_nonce') ) {
            die('No naughty business please.');
        }
        else{
            $formdata = stripslashes($_POST['formdata']); 
            $formdata = json_decode($formdata , true);

            $fullname = $formdata['fullname'];
            $email = sanitize_email($formdata['email']);
            $subject = $formdata['subject'];
            $phone = $formdata['phone'];
            $checkin = $formdata['checkin'];
            $checkout = $formdata['checkout'];
            $number_of_guest = $formdata['number_of_guest'];
            $request = $formdata['request'];

            //$recipient_email = base64_decode($formdata['sendto']);

            /* Email Message */
            $body = "<h2>Reservation Message</h2>";
            $body .= "<strong>Villa Name:</strong> ".$subject."<br/>";
            $body .= "<strong>Nama:</strong> ".$fullname."<br/>";
            $body .= "<strong>E-mail:</strong> ".$email."<br/>";
            $body .= "<strong>Phone:</strong> ".$phone."<br/>";
            $body .= "<strong>Check in date:</strong> ".$checkin."<br/>";
            $body .= "<strong>Check out date:</strong> ".$checkout."<br/>";
            $body .= "<strong>Number of guest:</strong> ".$number_of_guest."<br/>";
            $body .= "<strong>Request:</strong> ".$request."<br/>";

            if( $_SERVER['REMOTE_ADDR'] === '127.0.0.1' ){
                $result['status'] = 'true';
            } else{

                if( function_exists('wp_mail') ) {

                    $headers = 'From: ' . $fullname . ' <' . $email . '>' . "rn";

                    if(wp_mail($recipient_email, $subject, $body, $headers)) {
                        $result['status'] = 'true';
                    } else {
                        $result['status'] = 'false';
                    }

                } else {
                    $result['status'] = 'false';
                }

            }

            echo json_encode($result);

            die();

        }
    }

kode php untuk memasukan ajax url parameter yg dibutuhkan sebagai alamat url pada fetch di reservationform js

 /* Make site url available to JS scripts */
        $site_parameters = array(
            'site_url' => get_site_url(),
            'theme_directory' => get_template_directory_uri(),
            'ajax_url' => admin_url('admin-ajax.php')
        );
        wp_localize_script( 'tw-js', 'SiteParameters', $site_parameters );

kode php untuk memasukan source javascript dalam element dengan menambahkan attribute defer

if( is_page_template( 'villa-page.php' )){
 //add script into theme and inject defer attributes in the <script>s
    $scripts_ids = array('datepicker','reservation-form','alpinejs');
    $scripts_srcs = array('datepicker.js','reservationform.js','alpinejs.js');

    foreach( array_combine( $scripts_ids , $scripts_srcs ) as $id => $src) {
        echo wp_get_script_tag( 
            array(
            'id'=> $id,
            'src'=> get_template_directory_uri( ).'/dist/js/'. $src,
            'defer'=> true,
            )
        ); 
    }

}

untuk mengkompilasi script reservationform dan datepicker menggunakan laravel mix ditambahkan kedalam webpack.mix.js

let mix = require('laravel-mix');

mix.setPublicPath('./dist');
mix.postCss('src/css/theme.css','css')
.options(
    {
        postCss:[
            require('tailwindcss')('tailwind.config.js')
        ],
        processCssUrls: false
    }
).postCss('src/css/tiny-slider.css','css');

mix.js('src/js/main.js','js')
.js('src/js/tiny-slider.js','js')
.js('src/js/alpinejs.js','js')
.js('src/js/reservationform.js','js')
.js('src/js/datepicker.js','js');

Implementing jquery tabs into wordpress

jquery tabs in wordpress

Sometimes i wonder how things are done so i right click my mouse and select view source, but they are already out put as html code not actual code that build it. so the journey begins one day i saw a website with a animated tabs ,ahh that’s cool i want one that on my website! and after searching a while with uncle google, i found jquery tabs and i don’t even know what is jquery :O but best part is; it is packed with examples. Next problem is how to make it works with WordPress, so here this is my quick n dirty resolution.

Continue reading “Implementing jquery tabs into wordpress”

usefull plug-in

I’ve used these lovely plug-in of WordPress in my wordpress box an they work as expected

WordPress Widgets:

This is probably the first plugin you’ll want to grab. It allows you to easily arrange the contents of your sidebar. There are also tons of great “sidebar widget” plugins available.
SEO Title Tag:

One of the most important plugins for (SEO) search engine optimization. It allows you to reverse the WordPress blog name and title to show better keyword prominence.

Google Sitemap Generator:

Automatically generates Google Sitemaps for your Blog.

WP Cache Plugin:

As you start to build traffic to your blog, you’ll soon experience a need to reduce the load on your server. The WPCache plugin for WordPress alows you to serve many more pages per second. It does this by caching each page to the server upon the first load of the page so the page doesn’t have to be compiled the next time it is served.

And below are recommended plug-in i might install in the future

WP Contact Form:

Allows people to contact you via a submission form. This means that you don’t have to expose your e-mail address anymore.

WP Database Manager:

This magnificent tool allows you to manage your SQL database directly from within WordPress.

WP E-Mail:

Enable your visitors to send an E-Mail of the post directly to their friends.

Akismet, I thank you

“Spam! anoying e-mail that is both unsolicited by the recipient and sent in substantively identical form to many recipients.” Thanks Akismet for their great Anti Spam …now no more unsolicited comments on my web . Recently, Blog Herald’s Abe Olandres wrote “Are We Betting Our Blogs on Akismet?” and he’s right.

“Most of us running WordPress practically rely on Akismet to combat spam and according to its overall stats, 93% of all comments are spam (the real figures would be a bit higher than that). Today alone, there are close to 2 million spam being filtered out by Akismet. But what if Akismet fails again and the next time would be more disastrous? Thousands upon thousands of comments to either manually moderate, delete or flag as spam. That task could take hours and maybe even days especially for blog networks with dozens or hundreds of blogs to maintain. ”

So I must say that this plug-in is Rock! if you’re using wordpress activate this plug-in.

Word Press 2.2 released

I love this open source blog script ! yesterday i mananged to upgrade my WordPress and If you must updated your wordpress – please always back up your install and your database first.

When a developer says they’ve added “We now protect you from activating a plugin or editing a file that will break your blog” it could mean that something you have now, that works just great for you- may not work the moment you upgrade.

Continue reading “Word Press 2.2 released”

Extra tools for WordPress 2.1 editor

It is always interesting to see how WordPress developer makes improvement on their editor, i recently upgrade my WordPress to 2.13 and find by pressing alt-shift-v you can extend your Tiny MCE editor


to

PC usersjust hit alt-shift-v (Firefox) or alt-v (IE) to toggle it. Mac users– use cntrl-v (Firefox)

(Not seeing either of those? Visit Users → Your Profile and make sure that “Use the visual editor when writing” is checked. Still having problems? Visit the WordPress support forums.)

Pasting from word processors just became a whole lot easier. Two of the new buttons are “Paste as Plain Text” and “Paste from Word.” Nice for those who use such things. I like the custom character thing too. Now I can easily add ∞ whenever I need it…