Building Your Startup: Approaching Major Feature Enhancements

This tutorial is part of the Building Your Startup With PHP series on ThemeKeeper Tuts+. In this series, I’m guiding you through launching a startup from concept to reality using my Meeting Planner app as a real-life example. Every step along the way, I’ll release the Meeting Planner code as open-source examples you can learn from. I’ll also address startup-related business issues as they arise.

Building Your Startup: Approaching Major Feature Enhancements

Building Your Startup: Approaching Major Feature Enhancements

How to Approach Major Feature Updates

These days I’m most often working to add small incremental improvements to Meeting Planner. The basics work pretty well, and I’m trying to gradually improve the application based on my vision and people’s feedback. Sometimes, my vision is for a bigger change, and that can be harder now that the codebase has grown so much.

In today’s tutorial, I’m going to talk about ways to think about making bigger changes to an existing codebase. Specifically, I’ll walk you through the impacts of adding the ability for meeting participants to collaboratively brainstorm and decide on activities, i.e. what we should do when we meet up.

If you haven’t yet, do try to schedule a meeting, and now you can also schedule an activity. It will help you understand as you go through the tutorial.

Before we begin, please remember to share your comments and feedback below. I monitor them, and you can also reach me on Twitter @lookahead_io. I’m especially interested if you want to suggest new features or topics for future tutorials.

As a reminder, all of the code for Meeting Planner is written in the Yii2 Framework for PHP. If you’d like to learn more about Yii2, check out our parallel series Programming With Yii2.

The Activity Planning Feature

Building Your Startup: Approaching Major Feature Enhancements

Basically, Meeting Planner and Simple Planner are designed to make scheduling as easy as it can be. You propose a few times and places and share it with one more people for them to weigh in on the options. Then, you decide together, and Meeting Planner keeps track with calendar entries, reminders and simple ways to make adjustments after the fact.

As an example, here’s a video of scheduling for groups:

I wanted to expand the scheduling support provided for times and places to the concept of activities. For example, when planning a meetup with your friends, you’re essentially asking, should we go to the movies, go dancing or snowboarding.

In other words, I wanted to create a panel like the one for times shown below but for activities:

Building Your Startup: Approaching Major Feature Enhancements

The MVC architecture and my code naming scheme are very similar between meeting times and places, so building activities seemed pretty simple on the surface. However, the decision to do so had wide ramifications.

Scoping the Changes

It’s important when adding a big feature to think both about where the code will need to change and also all the places in your application that may be affected to think through the impacts.

Customer-Facing Impacts

From the design side, I thought about how activities will affect the customer-facing service:

  • It will change organizing a meeting to allow a new type, an activity-driven event. There will be an additional panel on the planning page.
  • The activity panel will need to be designed to allow people to choose from defaults or to customize and add their own, e.g. backcountry skiing instead of just skiing, “Go see Star Wars Rogue One” instead of just “Go see a movie.”
  • Email invitations will need to include space to list activity options.
  • Calendar events will want to integrate the chosen activity with the subject of the meetup.
  • Organizers may want to send some activity ideas to a friend or a group without having chosen a place, so I need to allow this. Currently, the system doesn’t let you send an invitation until there’s at least one time and place suggested.
  • If someone requests a change to a meeting, the support for change requests will have to be expanded to support activities.

These were most of the basics. Now, let’s think about the code.

Code Impacts

Source Code Branching

Frequently, it’s helpful to branch your own code in GitHub so you can work on the new feature apart from the stable production-level codebase. This allows you to return and fix bugs or make smaller incremental changes while working on a major change. The GitHub folks are stricter in a way that makes definite sense for teams:

There’s only one rule: anything in the master branch is always deployable.

Since there’s just one of me and I’m pretty good at managing my codebase, I am a bit more laissez faire about this rule.

But branching code is also useful for reviewing code changes when you’re ready for testing. I’ll share a demonstration of this at the end of today’s tutorial.

Replicating Common Code

There will be two types of meetings now: those based around only dates and times and those around activities, dates, and times. So the meeting model needs to adapt.

For times and places, there are specifically the models MeetingTime and MeetingPlace as well as models for the preferences for these with participants, called MeetingTimeChoices and MeetingPlaceChoices. You can read more about building this in Building Your Startup With PHP: Scheduling Availability and Choices.

So adding activities would essentially require duplicating these, creating MeetingActivity and MeetingActivityChoices and their accompany controllers, models, views, JavaScript and Ajax and database migrations.

Account and Meeting Settings

Also, organizers have the preference of various account settings and per meeting settings for whether participants can suggest and choose the final activity.

Email Templates

Adding activities also affected email templates for invitations and completed meetings.

Event History and Logs

Since every change to a meeting is logged, each activity option and change also needed to be logged.

Other Miscellaneous Areas

The .ics Calendar file should be changed to include the activity. Ultimately, the API would need to be updated—and even the statistics for the administrative dashboard.

While it seemed simple up front, adding activities actually required a lot of new code and testing.

Coding Highlights

While there’s too much new code to cover in one tutorial, let’s go through highlighted aspects from some of the concepts above.

Database Migrations

First, I created the database migrations. Earlier I spoke of replicating code with feature aspects in common. Here’s an example of the MeetingActivity migration vs. the older MeetingTime table migration:

<?php
use yiidbSchema;
use yiidbMigration;

class m161202_020757_create_meeting_activity_table extends Migration
{
  public function up()
  {
      $tableOptions = null;
      if ($this->db->driverName === 'mysql') {
          $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
      }

      $this->createTable('{{%meeting_activity}}', [
          'id' => Schema::TYPE_PK,
          'meeting_id' => Schema::TYPE_INTEGER.' NOT NULL',
          'activity' => Schema::TYPE_STRING.' NOT NULL',          
          'suggested_by' => Schema::TYPE_BIGINT.' NOT NULL',
          'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0',
          'availability' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0',
          'created_at' => Schema::TYPE_INTEGER . ' NOT NULL',
          'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL',
      ], $tableOptions);
      $this->addForeignKey('fk_meeting_activity_meeting', '{{%meeting_activity}}', 'meeting_id', '{{%meeting}}', 'id', 'CASCADE', 'CASCADE');
      $this->addForeignKey('fk_activity_suggested_by', '{{%meeting_activity}}', 'suggested_by', '{{%user}}', 'id', 'CASCADE', 'CASCADE');

  }

  public function down()
  {
    $this->dropForeignKey('fk_activity_suggested_by', '{{%meeting_activity}}');
    $this->dropForeignKey('fk_meeting_activity_meeting', '{{%meeting_activity}}');
      $this->dropTable('{{%meeting_activity}}');
  }
}

Here’s MeetingTime’s migration, and you can see the similarities:

<?php

use yiidbSchema;
use yiidbMigration;

class m141025_215833_create_meeting_time_table extends Migration
{
  public function up()
  {
      $tableOptions = null;
      if ($this->db->driverName === 'mysql') {
          $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
      }

      $this->createTable('{{%meeting_time}}', [
          'id' => Schema::TYPE_PK,
          'meeting_id' => Schema::TYPE_INTEGER.' NOT NULL',
          'start' => Schema::TYPE_INTEGER.' NOT NULL',
          'suggested_by' => Schema::TYPE_BIGINT.' NOT NULL',
          'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0',
          'created_at' => Schema::TYPE_INTEGER . ' NOT NULL',
          'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL',
      ], $tableOptions);
      $this->addForeignKey('fk_meeting_time_meeting', '{{%meeting_time}}', 'meeting_id', '{{%meeting}}', 'id', 'CASCADE', 'CASCADE');
      $this->addForeignKey('fk_participant_suggested_by', '{{%meeting_time}}', 'suggested_by', '{{%user}}', 'id', 'CASCADE', 'CASCADE');      
      
  }

  public function down()
  {
    $this->dropForeignKey('fk_participant_suggested_by', '{{%meeting_time}}');
    $this->dropForeignKey('fk_meeting_time_meeting', '{{%meeting_time}}');
      $this->dropTable('{{%meeting_time}}');
  }
}

Ultimately, I needed five for the following new tables:

  1. m161202_020757_create_meeting_activity_table
  2. m161202_021355_create_meeting_activity_choice_table, for storing the availability preferences of each meeting participant for each activity
  3. m161202_024352_extend_meeting_setting_table_for_activities for a particular meeting’s settings for adding or choosing activities
  4. m161202_024403_extend_user_setting_table_for_activities for the account’s default settings
  5. m161203_010030_extend_meeting_table_for_activities and for adding is_activity to denote the property of a meeting with or without an activity
$ ./yii migrate/up
Yii Migration Tool (based on Yii v2.0.10)

Total 5 new migrations to be applied:
    m161202_020757_create_meeting_activity_table
	m161202_021355_create_meeting_activity_choice_table
	m161202_024352_extend_meeting_setting_table_for_activities
	m161202_024403_extend_user_setting_table_for_activities
	m161203_010030_extend_meeting_table_for_activities

Apply the above migrations? (yes|no) [no]:yes
*** applying m161202_020757_create_meeting_activity_table
    > create table {{%meeting_activity}} ... done (time: 0.032s)
    > add foreign key fk_meeting_activity_meeting: {{%meeting_activity}} (meeting_id) references {{%meeting}} (id) ... done (time: 0.023s)
    > add foreign key fk_activity_suggested_by: {{%meeting_activity}} (suggested_by) references {{%user}} (id) ... done (time: 0.006s)
*** applied m161202_020757_create_meeting_activity_table (time: 0.071s)

*** applying m161202_021355_create_meeting_activity_choice_table
    > create table {{%meeting_activity_choice}} ... done (time: 0.002s)
    > add foreign key fk_mac_meeting_activity: {{%meeting_activity_choice}} (meeting_activity_id) references {{%meeting_activity}} (id) ... done (time: 0.006s)
    > add foreign key fk_mac_user_id: {{%meeting_activity_choice}} (user_id) references {{%user}} (id) ... done (time: 0.004s)
*** applied m161202_021355_create_meeting_activity_choice_table (time: 0.016s)

*** applying m161202_024352_extend_meeting_setting_table_for_activities
    > add column participant_add_activity smallint NOT NULL DEFAULT 0 to table {{%meeting_setting}} ... done (time: 0.013s)
    > add column participant_choose_activity smallint NOT NULL DEFAULT 0 to table {{%meeting_setting}} ... done (time: 0.019s)
*** applied m161202_024352_extend_meeting_setting_table_for_activities (time: 0.034s)

*** applying m161202_024403_extend_user_setting_table_for_activities
    > add column participant_add_activity smallint NOT NULL to table {{%user_setting}} ... done (time: 0.024s)
    > add column participant_choose_activity smallint NOT NULL to table {{%user_setting}} ... done (time: 0.027s)
*** applied m161202_024403_extend_user_setting_table_for_activities (time: 0.055s)

*** applying m161203_010030_extend_meeting_table_for_activities
      > add column is_activity smallint NOT NULL to table {{%meeting}} ... done (time: 0.019s)
*** applied m161203_010030_extend_meeting_table_for_activities (time: 0.022s)


5 migrations were applied.

Migrated up successfully.

Building the MVC Framework for Activities

Building Your Startup: Approaching Major Feature Enhancements

I used Yii’s Gii scaffolding capability to create the model, controller, and initial views. I’ve covered migrations and Gii earlier in the series.

JavaScript and jQuery Changes

There were also substantial additions to the JavaScript and jQuery used, especially now that interacting with planning elements for a meeting is done with Ajax, without refreshing the page.

Here, for example, is the code loop to see if a meeting time is chosen:

// respond to change in meeting_time
$(document).on("click", '[id^=btn_mt_]', function(event) {
  current_id = $(this).attr('id');
  $(this).addClass("btn-primary");
  $(this).removeClass("btn-default");
  $('[id^=btn_mt_]').each(function(index) {
    if ($(this).attr('id')!=current_id) {
      $(this).addClass("btn-default");
      $(this).removeClass("btn-primary");
    }
  });
  $.ajax({
     url: $('#url_prefix').val()+'/meeting-time/choose',
     data: {id:   $('#meeting_id').val(), 'val': current_id},
     success: function(data) {
       displayNotifier('choosetime');
       refreshSend();
       refreshFinalize();
       return true;
     }
  });
});

By using a common naming scheme, writing the code for activities was replicable from this:

// respond to change in meeting_activity
$(document).on("click", '[id^=btn_ma_]', function(event) {
  current_id = $(this).attr('id');
  $(this).addClass("btn-primary");
  $(this).removeClass("btn-default");
  $('[id^=btn_ma_]').each(function(index) {
    if ($(this).attr('id')!=current_id) {
      $(this).addClass("btn-default");
      $(this).removeClass("btn-primary");
    }
  });
  $.ajax({
     url: $('#url_prefix').val()+'/meeting-activity/choose',
     data: {id:   $('#meeting_id').val(), 'val': current_id},
     success: function(data) {
       displayNotifier('chooseactivity');
       refreshSend();
       refreshFinalize();
       return true;
     }
  });
});

Other functions, such as those that display responses to the user, simply needed to be extended for activities:

function displayNotifier(mode) {
    if (notifierOkay) {
      if (mode == 'time') {
        $('#notifierTime').show();
      } else if (mode == 'place') {
         $('#notifierPlace').show();
       } else if (mode == 'chooseplace') {
          $('#notifierChoosePlace').show();
        } else if (mode == 'choosetime') {
           $('#notifierChooseTime').show();
     } else if (mode == 'activity') {
        $('#notifierPlace').show();
      } else if (mode == 'chooseactivity') {
         $('#notifierChooseActivity').show();
     } else {
        alert("We'll automatically notify the organizer when you're done making changes.");
      }
      notifierOkay=false;
    }
  }

Even with a feature so reflective in qualities as existing features, the additions of new code were extensive. Here’s more of the JavaScript, for example. This code covers a lot more of the interactive ajax functionality of meeting times on the planning page:

function showActivity() {
  if ($('#addActivity').hasClass( "hidden")) {
    $('#addActivity').removeClass("hidden");
    $('.activity-form').removeClass("hidden");
  } else {
    $('#addActivity').addClass("hidden");
    $('.activity-form').addClass("hidden");
  }
};

function cancelActivity() {
  $('#addActivity').addClass("hidden");
  $('.activity-form').addClass("hidden");
}

function addActivity(id) {
    activity = $('#meeting_activity').val();
    // ajax submit subject and message
    $.ajax({
       url: $('#url_prefix').val()+'/meeting-activity/add',
       data: {
         id: id,
        activity: encodeURIComponent(activity),
      },
       success: function(data) {
         $('#meeting_activity').val('');
         loadActivityChoices(id);
         insertActivity(id);
         displayAlert('activityMessage','activityMsg1');
         return true;
       }
    });
    $('#addActivity').addClass('hidden');
  }

  function insertActivity(id) {
    $.ajax({
     url: $('#url_prefix').val()+'/meeting-activity/insertactivity',
     data: {
       id: id,
      },
      type: 'GET',
     success: function(data) {
      $("#meeting-activity-list").html(data).removeClass('hidden');
        $("input[name='meeting-activity-choice']").map(function(){
          //$(this).bootstrapSwitch();
          $(this).bootstrapSwitch('onText','<i class="glyphicon glyphicon-thumbs-up"></i>&nbsp;yes');
          $(this).bootstrapSwitch('offText','<i class="glyphicon glyphicon-thumbs-down"></i>&nbsp;no');
          $(this).bootstrapSwitch('onColor','success');
          $(this).bootstrapSwitch('offColor','danger');
          $(this).bootstrapSwitch('handleWidth',50);
          $(this).bootstrapSwitch('labelWidth',1);
          $(this).bootstrapSwitch('size','small');
        });
     },
   });
   refreshSend();
   refreshFinalize();
  }

function getActivities(id) {
  $.ajax({
   url: $('#url_prefix').val()+'/meeting-activity/getactivity',
   data: {
     id: id,
    },
    type: 'GET',
   success: function(data) {
     $('#meeting-activity-list').html(data);
   },
 });
}

Framework Additions

Certainly, there were models, controllers and views that had to be added. Here’s an excerpt from the MeetingActivity.php model which lists a number of default activities the user can quickly typeahead to use:

<?php
namespace frontendmodels;

use Yii;
use yiidbActiveRecord;
use commoncomponentsMiscHelpers;
use frontendmodelsParticipant;

/**
 * This is the model class for table "{{%meeting_activity}}".
 *
 * @property integer $id
 * @property integer $meeting_id
 * @property string $activity
 * @property integer $availability
 * @property integer $suggested_by
 * @property integer $status
 * @property integer $created_at
 * @property integer $updated_at
 *
 * @property User $suggestedBy
 * @property Meeting $meeting
 * @property MeetingActivityChoice[] $meetingActivityChoices
 */
class MeetingActivity extends yiidbActiveRecord
{
  const STATUS_SUGGESTED =0;
  const STATUS_SELECTED =10; // the chosen date time
  const STATUS_REMOVED =20;

  const MEETING_LIMIT = 7;

  public $url_prefix;
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%meeting_activity}}';
    }


    public function behaviors()
    {
        return [
            /*[
                'class' => SluggableBehavior::className(),
                'attribute' => 'name',
                'immutable' => true,
                'ensureUnique'=>true,
            ],*/
            'timestamp' => [
                'class' => 'yiibehaviorsTimestampBehavior',
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
                ],
            ],
        ];
    }
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['meeting_id', 'activity', 'suggested_by',], 'required'],
            [['meeting_id', 'suggested_by', 'status', 'created_at', 'updated_at'], 'integer'],
            [['activity'], 'string', 'max' => 255],
            [['suggested_by'], 'exist', 'skipOnError' => true, 'targetClass' => commonmodelsUser::className(), 'targetAttribute' => ['suggested_by' => 'id']],
            [['meeting_id'], 'exist', 'skipOnError' => true, 'targetClass' => Meeting::className(), 'targetAttribute' => ['meeting_id' => 'id']],
        ];
    }

    public static function defaultActivityList() {
      $activities = [
        Yii::t('frontend','Bachelor party'),
        Yii::t('frontend','Birthday party'),
        Yii::t('frontend','Breakfast'),
        Yii::t('frontend','Brunch'),
        Yii::t('frontend','Coffee, Tea, Juice Bar, et al.'),
        Yii::t('frontend','Concert'),
        Yii::t('frontend','Counseling'),
        Yii::t('frontend','Cycling'),
        Yii::t('frontend','Dessert'),
        Yii::t('frontend','Dinner'),
        Yii::t('frontend','Dog walking'),
        Yii::t('frontend','Drinks'),
        Yii::t('frontend','Dancing'),
        Yii::t('frontend','Bar'),
        Yii::t('frontend','Movies'),
        Yii::t('frontend','Happy hour'),
        Yii::t('frontend','Hiking'),
        Yii::t('frontend','Lunch'),
        Yii::t('frontend','Meditation'),
        Yii::t('frontend','Netflix and chill'),
        Yii::t('frontend','Party'),
        Yii::t('frontend','Protest'),
        Yii::t('frontend','Theater'),        
        Yii::t('frontend','Play board games'),
        Yii::t('frontend','Play scrabble'),
        Yii::t('frontend','Play video games'),
        Yii::t('frontend','Running'),
        Yii::t('frontend','Shopping'),
        Yii::t('frontend','Skiing'),
        Yii::t('frontend','Snowboarding'),
        Yii::t('frontend','Snowshoeing'),
        Yii::t('frontend','Stand up comedy'),
        Yii::t('frontend','Walking'),
        Yii::t('frontend','Watch movies'),
        Yii::t('frontend','Watch sports'),
        Yii::t('frontend','Volunteer'),
        Yii::t('frontend','Yoga'),
      ];
      return $activities;
    }

Building Your Startup: Approaching Major Feature Enhancements

And here’s an excerpt of the /frontend/views/activity/_form.php with the TypeaheadBasic widget using the above defaultActivityList():

<?php
$activities=MeetingActivity::defaultActivityList();
echo $form->field($model, 'activity')->label(Yii::t('frontend','Suggest an activity'))->widget(TypeaheadBasic::classname(), [
'data' => $activities,
'options' => ['placeholder' => Yii::t('frontend','enter your suggestions'),
'id'=>'meeting_activity',
  //'class'=>'input-large form-control'
],
'pluginOptions' => ['highlight'=>true],
]);
?>

But there are numerous changes to the code outside of common framework needs. Here’s the Meeting.php model’s canSend(), the function that determines whether the user is allowed to send an invitation for a meeting. It determines if a meeting’s met the minimum requirements for sending, such as having a time and activity or time and place.

Below, you can see how a new section had to be added for activities:

public function canSend($sender_id) {
  // check if an invite can be sent
  // req: a participant, at least one place, at least one time
  $cntPlaces = 0;
  foreach($this->meetingPlaces as $mp) {
    if ($mp->status!=MeetingPlace::STATUS_REMOVED) {
      $cntPlaces+=1;
    }
  }
  $cntTimes = 0;
  foreach($this->meetingTimes as $mt) {
    if ($mt->status!=MeetingTime::STATUS_REMOVED) {
      $cntTimes+=1;
    }
  }
  $cntActivities =0; // for either type of meeting
  if ($this->is_activity==Meeting::IS_ACTIVITY) {
    foreach($this->meetingActivities as $ma) {
      if ($ma->status!=MeetingActivity::STATUS_REMOVED) {
        $cntActivities+=1;
      }
    }
  }
  if ($this->owner_id == $sender_id
   && count($this->participants)>0
   && ($cntPlaces>0 || $this->isVirtual() || ($this->is_activity == Meeting::IS_ACTIVITY && $cntActivities>0))
   && $cntTimes>0
   && ($this->is_activity == Meeting::NOT_ACTIVITY || ($this->is_activity == Meeting::IS_ACTIVITY && $cntActivities>0))
   ) {
    $this->isReadyToSend = true;
  } else {
    $this->isReadyToSend = false;
  }
  return $this->isReadyToSend;
 }

Email Templates

Updating the email layouts required a tiny bit of thinking about design and how best to present activities in meeting invitations and confirmations. Here’s a sample of the updated email invitation:

Building Your Startup: Approaching Major Feature Enhancements

Essentially, if a meeting has an activity, then the invitation includes a wide row above times and places, again replicating a lot of the existing code for times and places:

<?php if ($is_activity==Meeting::IS_ACTIVITY) {?>
<tr>
<td style="color:#777; font-family:Helvetica, Arial, sans-serif; font-size:14px; line-height:21px; text-align:center; border-collapse:collapse; padding:8px 20px; width:280px" align="center" width="280">
<table cellspacing="0" cellpadding="0" width="100%" style="border-collapse:separate">
  <tr>
    <td style="color:#777; font-family:Helvetica, Arial, sans-serif; font-size:14px; line-height:21px; text-align:center; border-collapse:collapse; background-color:#fff; border:1px solid #ccc; border-radius:5px; padding:12px 15px 15px; width:498px" align="center" bgcolor="#ffffff" width="498">
      <table cellpadding="0" cellspacing="0" width="100%" style="border-collapse:collapse">
        <tr>
          <td style="color:#777; font-family:Helvetica, Arial, sans-serif; font-size:14px; line-height:21px; text-align:left; border-collapse:collapse" align="left">
            <span style="color:#4d4d4d; font-size:18px; font-weight:700; line-height:1.3; padding:5px 0">Possible Activities</span><br>
            <?php
              foreach($activities as $activity) {
                ?>
                    <?php echo $activity->activity; ?><br />
                    <?php
                  }
              ?>
              <br />
              <?php
              if ($meetingSettings->participant_add_activity) { ?>
              <?php echo HTML::a(Yii::t('frontend','suggest an activity'),$links['addactivity']); ?><br />
              <?php
              }
              ?>
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>
</td>
</tr>
<?php } ?>

Reflecting on the Changes

Ultimately, the activity feature required a huge new branch of code. Here’s the pull request:

$ cd /var/www/mp && git pull origin master
remote: Counting objects: 183, done.
remote: Compressing objects: 100% (183/183), done.
remote: Total 183 (delta 115), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (183/183), 111.48 KiB | 0 bytes/s, done.
Resolving deltas: 100% (115/115), done.
From github.com:newscloud/mp
 * branch            master     -> FETCH_HEAD
   923b514..cd16262  master     -> origin/master
Updating 923b514..cd16262
Fast-forward
 common/components/MiscHelpers.php                                                 |   8 +--
 common/mail/finalize-html.php                                                     |  28 ++++++++-
 common/mail/invitation-html.php                                                   |  39 ++++++++++++-
 composer.lock                                                                     | 228 +++++++++++++++++++++++++++++++++++++-----------------------------------
 console/migrations/m161202_020757_create_meeting_activity_table.php               |  35 +++++++++++
 console/migrations/m161202_021355_create_meeting_activity_choice_table.php        |  34 +++++++++++
 console/migrations/m161202_024352_extend_meeting_setting_table_for_activities.php |  22 +++++++
 console/migrations/m161202_024403_extend_user_setting_table_for_activities.php    |  24 ++++++++
 console/migrations/m161203_010030_extend_meeting_table_for_activities.php         |  22 +++++++
 frontend/assets/AppAsset.php                                                      |   2 +-
 frontend/assets/HomeAsset.php                                                     |   2 +-
 frontend/assets/MapAsset.php                                                      |   2 +-
 frontend/assets/MeetingAsset.php                                                  |   2 +-
 frontend/config/main.php                                                          |   1 +
 frontend/controllers/DaemonController.php                                         |   2 +-
 frontend/controllers/MeetingActivityChoiceController.php                          |  70 ++++++++++++++++++++++
 frontend/controllers/MeetingActivityController.php                                | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 frontend/controllers/MeetingController.php                                        |  94 ++++++++++++++++++++++++++++--
 frontend/controllers/MeetingTimeController.php                                    |   4 +-
 frontend/models/Fix.php                                                           |   9 +--
 frontend/models/Meeting.php                                                       | 125 ++++++++++++++++++++++++++++++++++------
 frontend/models/MeetingActivity.php                                               | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 frontend/models/MeetingActivityChoice.php                                         | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++
 frontend/models/MeetingLog.php                                                    |  43 +++++++++++++-
 frontend/models/MeetingPlaceChoice.php                                            |   3 +-
 frontend/models/MeetingTimeChoice.php                                             |   3 +-
 frontend/models/Participant.php                                                   |   2 +
 frontend/views/meeting-activity/_choices.php                                      |  42 ++++++++++++++
 frontend/views/meeting-activity/_form.php                                         |  46 +++++++++++++++
 frontend/views/meeting-activity/_list.php                                         |  78 +++++++++++++++++++++++++
 frontend/views/meeting-activity/_panel.php                                        |  81 ++++++++++++++++++++++++++
 frontend/views/meeting-activity/_search.php                                       |  39 +++++++++++++
 frontend/views/meeting-activity/_thread.php                                       |  15 +++++
 frontend/views/meeting-activity/create.php                                        |  21 +++++++
 frontend/views/meeting-activity/index.php                                         |  42 ++++++++++++++
 frontend/views/meeting-activity/update.php                                        |  23 ++++++++
 frontend/views/meeting-setting/_form.php                                          |   2 +
 frontend/views/meeting-time/_panel.php                                            |   2 +-
 frontend/views/meeting-time/view.php                                              |   4 +-
 frontend/views/meeting/_command_bar_planning.php                                  |  11 +++-
 frontend/views/meeting/_grid.php                                                  |   9 ++-
 frontend/views/meeting/_panel_what.php                                            |   1 +
 frontend/views/meeting/view.php                                                   |  22 +++++--
 frontend/views/meeting/view_confirmed.php                                         |  19 ++++++
 frontend/views/meeting/viewactivity.php                                           |  40 +++++++++++++
 frontend/views/participant/_panel.php                                             |   4 +-
 frontend/views/user-setting/_form.php                                             |   7 ++-
 frontend/web/css/site.css                                                         |   9 ++-
 frontend/web/js/meeting.js                                                        | 284 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
 49 files changed, 2027 insertions(+), 257 deletions(-)
 create mode 100644 console/migrations/m161202_020757_create_meeting_activity_table.php
 create mode 100644 console/migrations/m161202_021355_create_meeting_activity_choice_table.php
 create mode 100644 console/migrations/m161202_024352_extend_meeting_setting_table_for_activities.php
 create mode 100644 console/migrations/m161202_024403_extend_user_setting_table_for_activities.php
 create mode 100644 console/migrations/m161203_010030_extend_meeting_table_for_activities.php
 create mode 100644 frontend/controllers/MeetingActivityChoiceController.php
 create mode 100644 frontend/controllers/MeetingActivityController.php
 create mode 100644 frontend/models/MeetingActivity.php
 create mode 100644 frontend/models/MeetingActivityChoice.php
 create mode 100644 frontend/views/meeting-activity/_choices.php
 create mode 100644 frontend/views/meeting-activity/_form.php
 create mode 100644 frontend/views/meeting-activity/_list.php
 create mode 100644 frontend/views/meeting-activity/_panel.php
 create mode 100644 frontend/views/meeting-activity/_search.php
 create mode 100644 frontend/views/meeting-activity/_thread.php
 create mode 100644 frontend/views/meeting-activity/create.php
 create mode 100644 frontend/views/meeting-activity/index.php
 create mode 100644 frontend/views/meeting-activity/update.php
 create mode 100644 frontend/views/meeting/viewactivity.php

It was so large that I decided to make a fun video scrolling through all the changes in GitHub with appropriate music playing in the background… enjoy:

Overall, building the activities feature was challenging and helpful for me to think about the site architecture and how to make fast, stable progress on the codebase of a one-person startup. Use replication, but reflect first on the overall scope.

The activity feature ended up touching more areas than I had anticipated.

Planning ahead will help you avoid getting stuck in never-ending coding traps for new features. If you do find yourself in some deep trench, a coding nightmare that just won’t end, check in your changes to your feature branch, switch back to master, and work on something else. It helps to clear your head.

I definitely took a slightly different approach to guiding you in this episode, and I hope it was helpful.

Have your own thoughts? Ideas? Feedback? You can always reach me on Twitter @lookahead_io directly. Watch for upcoming tutorials here in the Building Your Startup With PHP series. There’s a lot of surprising stuff ahead.

Again, if you haven’t tried out Meeting Planner or Simple Planner yet, go ahead and schedule your first meeting:

Related Links