7/14/2014

WatuPRO Coupon Code

This is not a tutorial (ha! surprise). I just want to share with you 10% coupon code for the best WordPress quiz plugin - WatuPRO. The promo code is:

HVERAGERDI

It will give you 10% discount of ANY version.
That's it! Use it and enjoy. Don't forget to thank me and check my tutorials :D

11/21/2013

Build WordPress Email Wrapper (With Attachments)

WordPress default email sending function wp_mail is way too basic. It uses "Wordpress" as sender name and doesn't allow you send HTML and plain text mail at the same time. If you are writing a plugin that sends emails this is going to be a problem.

In this quick tutorial I will show you how plugins like BroadFast for Wordpress manage to overwrite the default sender and to send emails with plain text and HTML versions. You simply need a small wrapper function. See the full code first and I will explain it below:

  1. function send($sender$receiver$subject$message$ctype = 'text/html'$attachments = NULL) {  
  2.     $plain_text = strip_tags(str_replace("<br>""\n"$message));  
  3.       
  4.     // handle text-only and "both" mail types  
  5.     if($ctype=='text/plain'$message = $plain_text;  
  6.     else $message=wpautop($message);  
  7.               
  8.     if($ctype=='both') {  
  9.         // thanks to http://www.tek-tips.com/faqs.cfm?fid=2681                    
  10.         $semi_rand = md5(time());  
  11.         $mime_boundary = "==MULTIPART_BOUNDARY_$semi_rand";  
  12.         $mime_boundary_header = chr(34) . $mime_boundary . chr(34);  
  13.           
  14.         // construct the body  
  15.         $body = "This is a multi-part message in MIME format. 
  16.  
  17.         --$mime_boundary 
  18.         Content-Type: text/plain; charset=\"UTF-8\" 
  19.         Content-Transfer-Encoding: 8bit 
  20.          
  21.         $plain_text 
  22.          
  23.         --$mime_boundary 
  24.         Content-Type: text/html; charset=utf8 
  25.         Content-Transfer-Encoding: 8bit 
  26.          
  27.         $message 
  28.          
  29.         --$mime_boundary--";  
  30.           
  31.         $body = str_replace("\t""" ,$body);  
  32.           
  33.         // now replace the vars  
  34.         $message = $body;  
  35.         $ctype = "multipart/alternative;\n" .   
  36.    "     boundary=" . $mime_boundary_header;              
  37.     }  
  38.       
  39.     $headers=array();  
  40.     $headers[] = "Content-Type: $ctype";  
  41.     $headers[] = 'From: '.$sender;  
  42.     $headers[] = 'sendmail_from: '.$sender;  
  43.       
  44.     // prepare attachments if any     
  45.     if($attachments and is_array($attachments)) {  
  46.         $atts = array();  
  47.         foreach($attachments as $attachment$atts[] = $attachment->file_path;  
  48.         $attachments = $atts;  
  49.     }  
  50.        
  51.     $message = do_shortcode($message);        
  52.     $result = wp_mail($receiver$subject$message$headers$attachments);                 
  53.     return $result;  
  54. }  

Now, let's have a look at this code:

Line 1. The function definition: The function accepts sender's email address that can contain just email address or a string like My Name <myemail@dot.com>. This allows you to send emails with different name than "Wordpress" (how exactly this happens is explained later below. Then $receiver is the receiver email address and $subject and $message are obvious parameters.

The parameter $ctype is the email content type - it can be either "text/html", "text/plain" or "both". Using "both" will generate email with two alternative parts so the email client program can choose the best one. Note that "both" doesn't work together with attachments. This is currently unsolvable - so if you want to send attachments, choose text/html or text/plain.

Now, let's go on with the code.

Line 2. Create a plain text version of the message to be used when sending the email as plain text.

Lines 4 - 5 take care to further prepare the message depending on the selected content type.


We have more work to do when sending both HTML and plain text version. It starts after line 8. First we generate the mime boundary by preparing a pseudo random number. Then from line 15 we start constructing the body of the message. First add the intro text, then the boundary, and then the two headers:

Content-Type: text/plain; charset=\"UTF-8\"
Content-Transfer-Encoding: 8bit


before the plain text version (lines 17 - 21) and:

Content-Type: text/html; charset=utf8
Content-Transfer-Encoding: 8bit


Before the HTML version (lines 23 - 27). Then closing the mime boundary.

Please pay attention to all the formatting and new lines, they are very important.

Then we have to remove any tabulators which come from our code indentation so we do it on line 31.

Lines 34 - 36 replace the proper content type for the email header.

Then we need to construct the headers to pass to wp_mail() function in WordPress.

The headers "from" and "sendmail_from" (lines 41 and 42) are very important. They are the lines that ensure when you set "John <email@dot.com>" as email sender, the email will be sent from "John" and not from "WordPress".

The next lines simply add the attachments (assuming you have the object coming from database or so).

Process shortcodes. I prefer to do this (line 51) because this enables the email author to include shortcodes from other plugins in the email contents. Be careful with this if you are letting end users send emails.

Then line 52 calls wp_mail and the function returns the result. Simple, isn't it?

3/20/2013

How To Conditionally Add Fields in MySQL DB Table

The Problem

When building an installation script for your software you usually have to create the required database tables. This is easy to do with running simple DB queries. Works great when installing first version. But once you have to start upgrades it can quickly become a nightmare. Of course you can't always create the tables from scratch because the user might already have some data in them! So you have to update them by adding the new fields only.

 

The Typical Solution

The usual way of handling this is storing the current version in the database, then running ALTER TABLE queries only if the version installed is older than the version when the new fields are added. This is a decent approach but rather error-prone because you have to be very careful of updating version numbers and assigning new fields to them.

 

The Better Approach

Instead of this, I am suggesting you the simpler approach we follow with our Wordpress plugins - for example BroadFast for Wordpress. We do frequent upgrades there and instead of trying to match ALTER TABLE statements to version numbers, we choose a conditional approach to adding fields in the tables.

Conditional simply means that we'll check if a field is already in the table, and if not, we'll add it only then. Let's see the function:

  1. // $fields - array of arrays. Each field is associative array having name and type.  
  2. // see the sample call after the function to get better idea  
  3. // $table - the name of the target table  
  4. function add_db_fields($fields$table) {  
  5.     global $wpdb// this is in WordPress, you may use some other object in your system  
  6.       
  7.     // get existing fields  
  8.     $table_fields = $wpdb->get_results("SHOW COLUMNS FROM `$table`");  
  9.     // let's store the names only in this array  
  10.     $table_field_names = array();  
  11.     foreach($table_fields as $f$table_field_names[] = $f->Field;  
  12.       
  13.     // and this is the array of fields that we'll need to add         
  14.     $fields_to_add=array();  
  15.       
  16.     // let's fill $fileds_to_add  
  17.     foreach($fields as $field) {  
  18.          if(!in_array($field['name'], $table_field_names)) {  
  19.               $fields_to_add[] = $field;  
  20.          }   
  21.     }  
  22.       
  23.     // now if there are fields to add, run the query  
  24.     if(!empty($fields_to_add)) {  
  25.          $sql = "ALTER TABLE `$table` ";  
  26.            
  27.          foreach($fields_to_add as $cnt => $field) {  
  28.              if($cnt > 0) $sql .= ", ";  
  29.              $sql .= "ADD $field[name] $field[type]";  
  30.          }   
  31.            
  32.          $wpdb->query($sql);  
  33.     }  
  34. }  
This is the whole function you need. It gets your array with fields, for each field check if it exists, and if not, adds it. The function can be called with multiple fields, but should be called once for each table where you are adding fields. Here is how to call the function:
  1. $fields = array(  
  2.       array("name"=>"sender""type"=>"VARCHAR(255) NOT NULL DEFAULT ''"),  
  3.       array("name"=>"require_name""type"=>"TINYINT UNSIGNED NOT NULL DEFAULT 0"),  
  4.       array("name"=>"auto_subscribe""type"=>"VARCHAR(255) NOT NULL DEFAULT ''")  
  5. );  
  6. add_db_fields($fields"mailing_lists");  
There isn't much to comment here. The table is called "mailing_lists" and we are conditionally adding three fields to it. Note that $fields is array of associative arrays, and each of them has name and type. The field "type" contains the full list of SQL arguments for the fields, not only the type. Obviously this function works only for adding new fields. This is the most common operation when releasing upgrades. Of course, you can expand it further to support CHANGE queries (but it won't change field names).

1/12/2012

Better PHP Calendar Class

I've dealt with creating monthly calendars in PHP and it has always been a bit of pain. How exactly to show the days in the month in a table without creating ugly and hard to maintain code? I believe this time I was able to find a good solution.

A PHP Class

It's a PHP class that will output the dates of the given month in array. But in order to make the dates fit their place in the week, the class outputs zeros in the place of "empty" days. So the first week of a month starting in Wednesday would be this array:

[0, 0, 1, 2, 3, 4, 5]

This allows you to loop through the returned arrays without worrying where to start the week. If you use a table, you could simply output " " when the date is 0 and output the number (and colored background) when the date is a number. Here is an example in HTML:

Using the output in HTML:


<table>
    <tr><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr>
    <?php for($i=1;$i<=$num_weeks;$i++):?>
        <tr>
            <?php $days=$_calendar->days($m[0], $m[1], $i, $num_weeks);
            foreach($days as $day):?>
                <td><?=$day?$day:"&nbsp;"?></td>
            <?php endforeach;?>
        </tr>
    <?php endfor;?>
    </table>


Easy, eh? We just output a table row for every week in the month ($num_weeks is also a method in the class as you'll see below) and for the days with date we output the date (otherwise just blank space).

You can see how it works live in this period calendar.

The PHP Code

Now, here is how it all works. I created a class to hold 3 functions:

num_weeks($month, $year) - to calculate the number of weeks in the given month and year

first_day($month, $year) - to find out which day of the week is the first day of a given month.year

days($month, $year, $week, $num_weeks=0) - this is the main method that returns the array of 7 days. The week starts in Monday.

Here is the full code below and some comments to it:

function num_weeks($month, $year)
    {
        // every month has at least 4 weeks
        $num_weeks=4;
    
        // finding where it starts
        $first_day = $this->first_day($month, $year);  
        
        // if the first week doesn't start on monday 
        // we are sure that the month has at minimum 5 weeks
        if($first_day!=1) $num_weeks++;
        
        // find the "widow" days (i.e. empty cells in the 1st week)
        $widows=$first_day-1;  
        $fw_days=7-$widows;
        if($fw_days==7) $fw_days=0;       
        
        // number of days in the month
        $numdays=date("t",mktime(2, 0, 0, $month, 1, $year));
        
        if( ($numdays - $fw_days) > 28 ) $num_weeks++;
         // that's it!
        return $num_weeks;                  
    }


Then the first_day function:

function first_day($month, $year)
    {
        $first_day= date("w", mktime(2, 0, 0, $month, 1, $year));
        if($first_day==0) $first_day=7; # convert Sunday
        
        return $first_day;
    }


And here is the most important one:
// here $week is the week number when we go in a loop
// (see the html code that I posted earlier to get better idea)
function days($month, $year, $week, $num_weeks=0)
{
        $days=array();

        // this is just to avoid calling num_weeks every time 
        // when you loop through the weeks        
        if($num_weeks==0) $num_weeks=$this->num_weeks($month, $year);
        
        // find which day of the week is 1st of the given month        
        $first_day = $this->first_day($month, $year);
                
        // find widow days (first week)
        $widows=$first_day-1;
        
        // first week days
        $fw_days=7-$widows;
        
        // if $week==1 don't do further calculations
        if($week==1)
        {
            for($i=0;$i<$widows;$i++) $days[]=0;            
            for($i=1;$i<=$fw_days;$i++) $days[]=$i;            
            return $days;
        }
        
        // any other week
        if($week!=$num_weeks)
        {
            $first=$fw_days+(($week-2)*7);
            for($i=$first+1;$i<=$first+7;$i++) $days[]=$i;            
            return $days;
        }
        
        
        # only last week calculations below
        
        // number of days in the month
        $numdays=date("t",mktime(2, 0, 0, $month, 1, $year));
                
        // find orphan days (last week)  
        $orphans=$numdays-$fw_days-(($num_weeks-2)*7);                     
        $empty=7-$orphans;
        for($i=($numdays-$orphans)+1;$i<=$numdays;$i++) $days[]=$i;
        for($i=0;$i<$empty;$i++) $days[]=0;
        return $days;
    }


That's it! Any questions?

5/19/2011

Simple HTML5 Drawing App + Saving The Files With Ajax

This is a simple drawing board app that allows user draw on a canvas, pick several brush sizes and colors and save his image (I'll leave part of the last to you).

You can see how this app works live on the Draw a Robot site. It uses HTML5 canvas and works in all modern browsers except Internet Explorer.

Some important parts of the javascript code are taken from this tutorial. William's app is more impressive. Mine is simpler and aims to explain it in understandable way (William's tutorial is a bit complex). Let's start:

Creating the canvas

<canvas id="drawingCanvas" width="550" height="450" style="border:1pt solid black;margin:auto;cursor:crosshair;clear:both;">
</canvas>


If you don't care about Internet explorer, that's it. We just add some styling to it and make the cursor crossed.

Create the brush sizes and color selectors


<div style="float:left;">Colors:</div> <a href="#" class="colorPicker" onclick="setColor('#FFF');return false;" style="background:#FFF;">&nbsp;</a>
<a class="colorPicker" href="#" onclick="setColor('#000');return false;" style="background:#000;">&nbsp;</a>
<a class="colorPicker" href="#" onclick="setColor('#FF0000');return false;" style="background:#FF0000;">&nbsp;</a>
<a class="colorPicker" href="#" onclick="setColor('#00FF00');return false;" style="background:#00FF00;">&nbsp;</a>
<a class="colorPicker" href="#" onclick="setColor('#0000FF');return false;" style="background:#0000FF;">&nbsp;</a>
<a class="colorPicker" href="#" onclick="setColor('#FFFF00');return false;" style="background:#FFFF00;">&nbsp;</a>
<a class="colorPicker" href="#" onclick="setColor('#00FFFF');return false;" style="background:#00FFFF;">&nbsp;</a>

<div style="clear:both;">&nbsp;</div>

<div style="float:left;">Sizes:</div>
<a href="#" class="colorPicker" onclick="setSize(2);return false;" style="width:2px;height:2px;margin-left:15px;">&nbsp;</a>
<a href="#" class="colorPicker" onclick="setSize(5);return false;" style="width:5px;height:5px;margin-left:15px;">&nbsp;</a>
<a href="#" class="colorPicker" onclick="setSize(10);return false;" style="width:10px;height:10px;margin-left:15px;">&nbsp;</a>
<a href="#" class="colorPicker" onclick="setSize(25);return false;" style="width:25px;height:25px;margin-left:15px;">&nbsp;</a>

<div style="clear:both;">&nbsp;</div>

<p style="clear:both;"><input type="button" value="Clear Canvas" onclick="clearCanvas();">
<input type="button" value="Save My Drawing" onclick="centerElt('saveDrawing',400,300);"></p>


This code adds size selection buttons, color selectiors, and button for saving.

Obviously you can use better CSS to save some code.

Note that Save my drawing users a function that is not published here to keep things simpler. It centers a popup on the screen. You can find or code such function yourself, or simply display the saving form under the canvas without fancy effects. (Let me know in the comments if you need clarification.

The save form

The save form is not important for this tutorial either. You can see the one at the Draw a Robot site, but the form can contain any fields you wish. Maybe image name, description, author name and so on.

The Javascript

After opening a javascript tag, you'll need the following code. I'll input all the explanations to it as javascript comments so you can directly copy the code and use it.

Please note: the javascript below depends on jQuery! If you don't store local copy of jQuery, you need to insert this code in the header of your page:
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>

Now your javascript follows:

<script type="text/javascript">
/* some code used from http://www.williammalone.com/articles/create-html5-canvas-javascript-drawing-app/ */

/* Some global initializations follow. The first 2 arays will store all mouse positions 
on X and Y, the 3rd one stores the dragged positions. 
The variable paint is a boolean, and then follow the default values 
which we use to start */
var clickX = new Array();
var clickY = new Array();
var clickDrag = new Array();
var paint;
var defaultColor="#000";
var defaultShape="round";
var defaultWidth=5;

// creating the canvas element
var canvas = document.getElementById('drawingCanvas');

if(canvas.getContext) 
{
    // Initaliase a 2-dimensional drawing context
    var context = canvas.getContext('2d');
    
    // set the defaults
    context.strokeStyle = defaultColor;
    context.lineJoin = defaultShape;
    context.lineWidth = defaultWidth;
}

// binding events to the canvas
$('#drawingCanvas').mousedown(function(e){
  var mouseX = e.pageX - this.offsetLeft;
  var mouseY = e.pageY - this.offsetTop;
  
  paint = true; // start painting
  addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop);

  // always call redraw
  redraw();
});

$('#drawingCanvas').mousemove(function(e){
  if(paint){
    addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop, true);
    redraw();
  }
});

// when mouse is released, stop painting, clear the arrays with dots
$('#drawingCanvas').mouseup(function(e){
  paint = false;
  
  clickX = new Array();
  clickY = new Array();
  clickDrag = new Array();
});

// stop painting when dragged out of the canvas
$('#drawARobot').mouseleave(function(e){
  paint = false;
});

// The function pushes to the three dot arrays
function addClick(x, y, dragging)
{
  clickX.push(x);
  clickY.push(y);
  clickDrag.push(dragging);
}

// this is where actual drawing happens
// we add dots to the canvas
function redraw(){
    
  for(var i=0; i < clickX.length; i++)
  {  
    context.beginPath();
    if(clickDrag[i] && i){
      context.moveTo(clickX[i-1], clickY[i-1]);
     }else{
       context.moveTo(clickX[i]-1, clickY[i]);
     }
     context.lineTo(clickX[i], clickY[i]);
     context.closePath();
     context.stroke();
  }
}

// this is called when "clear canvas" button is pressed
function clearCanvas()
{
    // both these lines are required to clear the canvas properly in all browsers
    context.clearRect(0,0,canvas.width,canvas.height);
    canvas.width = canvas.width;
    
    // we need to flush the arrays too
    clickX = new Array();
    clickY = new Array();
    clickDrag = new Array();
}

/* Two simple functions, they just assign the selected color and size 
to the canvas object properties */ 
function setColor(col)
{
    context.strokeStyle = col;
}

function setSize(px)
{
    context.lineWidth=px;
}

/* Finally this will send your image to the server-side script which will 
save it to the database or where ever you want it saved.
Note that this function should be called when the button in your save 
form is pressed. The variable frm is the form object. 
Basically the HTML will look like this:
<input type="button" value="Save Drawing" onclick="saveDrawing(this.form);">
 */
function saveDrawing(frm)
{       
    // converting the canvas to data URI
    var strImageData = canvas.toDataURL();  
        
    $.ajax({
        url: "", /* You need to enter the URL of your server side script*/
        type: "post",
          /* add the other variables here or serialize the entire form. 
          Image data must be URI encoded */
        data: "save=1&pic="+encodeURIComponent(strImageData), 
        success: function(msg)
        {
           // display some message and/or redirect
        }
    });
}

Your server side script

It really depends on you. In the Draw a Robot site we just save the robots to the database and then display them in a gallery. You can save the image data in a blob field in the DB. Just have in mind that data / URI images are displayed with the following HTML code:

<img src="encoded image data">

3/25/2010

Building a Fair and Convincing Banner Rotator

I have seen several tutorials how to write a banner rotator online. They are technically correct but looks like the programmers haven't really put their rotators in real environment. They assume that selecting a random banner from the list (or database) will do the work and show the banners equally. Good idea and correct in general.

But as I have run such a rotator in a live site where more than 7-8 banners were rotating, I know this isn't a good enough solution. Why so? Although computer random algos or the SQL RAND() ensure all banners will be shown almost the same number of times in the long run, during 10-20 page refreshes you can keep getting the same 3-4 banners and other ones may not appear at all. This is a problem because the clients who purcased banners complain "Hey, I refreshed more than 10 times and my banner isn't showing even once!!!". Go explain them about random computer algos and that someone might be seeing their banner at the same in some other computer at the other side of the globe.

To avoid such problem, we need not only fair, but also convincing banner rotator - such that will show you the banners equal times when you are refreshing the page.

Let's see what we need:

1. A list or DB table of banners
2. Fair selection
3. Counting the views and clicks

That's in short. We will write two PHP scripts - one will select and output the banner (we'll assume you'll put it in an iframe or will add some other useful stuff to it so you can have a full page with content); and one will redirect the visitors to the targed URL and count the clicks. Let's start:

1. A list of banners

Because we need to save the number of clicks to our banners we'll use a MySQL DB table:

Table banners:
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
image VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL,
clicks INT UNSIGNED NOT NULL;

In the real life you will probably have also a field for customer name, email etc. but let's not worry about this now.

2. Fair selection
This is the most interesting part. Let's call our script rotator.php. It will need not only to select a supposingly random banner, but to ensure that the rotation will be equal even for every single user who is seeing the page. Here's how to do this: we will store the IDs of the banners shown in a session. When selecting, we will make sure we get a random banner from those which have not been shown yet. When all the banners have been shown once in the current session, we'll empty the data and start over. Let's talk code:

rotator.php

// in this array we'll store the shown banners
if(!isset($_SESSION['banner_ids'])) $_SESSION['banner_ids']=array(0);

// let's built the conditional SQL now
$id_sql=implode(",",$_SESSION['banner_ids']);

// now select random banner instead of these that were shown
$q="SELECT * FROM banners WHERE id NOT IN ($id_sql) ORDER BY RAND() LIMIT 1";
$banner=mysql_fetch_array(mysql_query($q));

// enter this banner ID in the already seen ones
$_SESSION['banner_ids'][]=$banner['id'];

// now we need to check whether all the banners were shown once
$q="SELECT COUNT(id) FROM banners";
$num_banners=mysql_fetch_row(mysql_query($q));

// we use "<" because our session array always contains one more ID - the zero which is
// there to prevent mysql error
if($num_banners[0] < sizeof($_SESSION['banner_ids']))
{
unset($_SESSION['banner_ids']);
}

// that's it! now just display the banner:
echo "<a href='go.php?id=$banner[id]'><img src='$banner[image]'></a>";


Counting the views and clicks
Now this is pretty straightforward:

go.php

// prevent hacking
if(!is_numeric($_GET['id'])) exit;

// select banner
$q="SELECT * FROM banners WHERE id='$_GET[id]'";
$banner=mysql_fetch_array(mysql_query($q));

// update clicks
$q="UPDATE banners SET clicks=clicks+1 WHERE id='$banner[id]'";
mysql_query($q);

// redirect
header("Location: $banner[url]");


That's all!

2/22/2010

Designing a database for a personality quiz script

Since Blogger does not offer categories, I'll have to use tags for posts like this. And what's special about it? I'm not going to talk PHP this time - instead of that I'll talk databases.

Let's discuss a database for a personality quiz script - you know, one that will build tests which will say "you are a good worker", "you like to spend too much money", "you are a loving person" etc.

What is specific for the personality quizzes? The main part is that answers to questions must be matched to personality types. Some quizzes use point systems but they are less accurate because you may have answers for contrary personality types and the system may calculate that you belong to the middle one. For personality quizzes a lot more accurate is a system which will directly match the answer to the specific personality type and at the end show the personality type which collected the most answers.

So here is my proposition for a database along with short explanations:

Table Quizzes:
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
title VARCHAR(255)
description TEXT
num_users INT UNSIGNED NOT NULL

This table will contain one record for each quiz you want to run. You may want to add extra columns like date, tags etc. In num_users we will store the number of users who took the quiz.

Table Questions:
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
quiz_id INT UNSIGNED FOREIGH KEY to quizzes.id
question TEXT
question_type

This obviously is the table with questions. We need a foreign key to the table with quizzes and of course a field for the question itself. If you plan to have single choice and multiple choice questions, the field question_type will store the difference.

Table Answers:

id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
quiz_id INT UNSIGNED FOREIGH KEY to quizzes.id
question_id INT UNSIGNED FOREIGH KEY to questions.id
answer TEXT
result INT UNSIGNED FOREIGH KEY to results.id

The table will have foreign keys to both Quizzes and Questions table. I know giving a key to Questions is logically enough, but I always prefer to have all the relations explicitly given in the table. This gives a lot more clarity especially if you are using ER diagrams.
The "result" column may contain things like A, B, C which will

Table Results:
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
quiz_id INT UNSIGNED FOREIGH KEY to quizzes.id
value VARCHAR
description TEXT

This is the last table you need. It will contain the personality types. And because the Answers table has a foreign key to it, it will be very easy to calculate which result (personality type) has the most answers given.

I'm leaving the PHP or other language implementation of this DB to you. If you want to check such thing in action, check out this php quiz script.