German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Dieses Tutorial ist Teil des Build Your Startup mit PHP-Serie auf Envato Tuts +. In dieser Serie führe ich Sie durch die Einführung eines Start-ups von der Idee zur Realität mit meiner Meeting-Planer-App als ein echtes Beispiel. Bei jedem Schritt werde ich den Meeting-Planer-Code als Open-Source-Beispiele veröffentlichen, von denen Sie lernen können. Ich werde auch auf geschäftliche Probleme im Zusammenhang mit dem Start eingehen, sobald sie auftreten.
Da der Meeting-Planer sich dem Alpha-Start nähert, brauchen wir eine Möglichkeit, Supportanfragen mit Benutzern zu beantworten und Aktivitäten zu überwachen. Mit anderen Worten, wir müssen ein administratives Dashboard mit Benutzerverwaltung und Berichterstellung erstellen. In Gesprächen mit einem Berater haben wir darüber gesprochen, dass ich, wenn ich mich potenziellen Anlegern annähere, über ausgezeichnete Daten verfügen muss, die das Nutzerverhalten und das Wachstum des Dienstes genau beschreiben.
In der heutigen Folge werden wir die Grundlage für unser Verwaltungs-Dashboard bilden und einige der ersten Live- und historischen Berichte übernehmen und erstellen. Wir wissen zum Beispiel, wie viele Personen sich zu jeder Zeit angemeldet haben, wie viele Meetings geplant wurden und wie viel Prozent der eingeladenen Teilnehmer den Service genug haben, um ein eigenes Meeting zu organisieren. Es hat wirklich Spaß gemacht, dieses Zeug zu bauen und die Daten zu sehen, selbst wenn wir vor dem Start sind.
Wenn Sie Meeting Planner noch nicht ausprobiert haben (und selbst in den aggregierten Daten angezeigt werden möchten), können Sie Ihr erstes Meeting planen. Ich nehme an den Kommentarfäden teil, also sag mir, was du denkst! Sie können mich auch auf Twitter @reifman erreichen. Ich bin besonders interessiert, wenn Sie neue Funktionen oder Themen für zukünftige Tutorials vorschlagen möchten.
Zur Erinnerung: Der gesamte Code für Meeting Planner ist in das Yii2 Framework für PHP geschrieben. Wenn Sie mehr über Yii2 erfahren möchten, schauen Sie sich unsere Parallel-Serie Programmierung mit Yii2 an.
Aufbau der Grundlage des Dashboards
Die Yii Erweiterte Vorlage
Yii2 bietet Front- und Back-Office-Websites im Rahmen seiner erweiterten Anwendungskonfiguration. Sie können mehr darüber in meinem Envato Tuts + Tutorial, Programmieren mit Yii2: Verwenden der Advanced Application Template lesen. Im Wesentlichen bietet die Front-End-Site der erweiterten Vorlage Funktionen für Personen, und die Back-End-Site ist für das Dashboard und die Administrations-Site eines Services vorgesehen.
Um es zu aktivieren, musste ich nur Apache-Sites in meiner
MAMP-Localhost-Umgebung und auf meinem Produktions-Ubuntu-Server einrichten. Zum Beispiel, hier ist die Apache-Konfiguration auf dem Produktionsserver zum Laden der /backend/web
:
<IfModule mod_ssl.c> <VirtualHost *:443> ServerName your-administration-site.com DocumentRoot "/var/www/mp/backend/web" <Directory "/var/www/mp/backend/web"> # use mod_rewrite for pretty URL support RewriteEngine on # If a directory or a file exists, use the request directly RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Otherwise forward the request to index.php RewriteRule . index.php </Directory> SSLCertificateFile /etc/letsencrypt/live/meetingplanner.io/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/meetingplanner.io/privkey.pem Include /etc/letsencrypt/options-ssl-apache.conf SSLCertificateChainFile /etc/letsencrypt/live/meetingplanner.io/chain.pem </VirtualHost> </IfModule>
Konfigurieren unserer Back-End-Site
Als Nächstes habe ich ein neues Layout für die Back-End-Site basierend auf der Front-End-Site erstellt, jedoch mit anderen Menüoptionen. Ich entschied, dass die Homepage auf eine Seite mit Echtzeitstatistiken umgeleitet werden würde. Und die Menüs würden Links zu Echtzeitdaten, Daten von gestern um Mitternacht und historischen Daten bieten. Ich werde ein bisschen mehr davon erklären, während wir fortfahren.

Hier ist das \ backend \ views \ layouts \ main.php mit dem Menü:
<body> <?php $this->beginBody() ?> <div class="wrap"> <?php NavBar::begin([ 'brandLabel' => Yii::t('backend','Meeting Planner'), 'brandUrl' => 'https://meetingplanner.io', 'options' => [ 'class' => 'navbar-inverse navbar-fixed-top', ], ]); $menuItems[] = [ 'label' => 'Real Time', 'items' => [ ['label' => Yii::t('frontend','Usage'), 'url' => ['/data/current']], ] ]; $menuItems[] = [ 'label' => 'Yesterday', 'items' => [ ['label' => Yii::t('frontend','User Data'), 'url' => ['/user-data']], ] ]; $menuItems[]=[ 'label' => 'Historical', 'items' => [ ['label' => Yii::t('frontend','Statistics'), 'url' => ['/historical-data']], ], ]; if (Yii::$app->user->isGuest) { $menuItems[] = ['label' => 'Login', 'url' => ['/site/login']]; } else { $menuItems[] = [ 'label' => 'Account', 'items' => [ ['label' => 'Logout (' . Yii::$app->user->identity->username . ')', 'url' => ['/site/logout'], 'linkOptions' => ['data-method' => 'post'], ], ], ]; } echo Nav::widget([ 'options' => ['class' => 'navbar-nav navbar-right'], 'items' => $menuItems, ]); NavBar::end(); ?> <div class="container"> <?= Breadcrumbs::widget([ 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], ]) ?> <?= $content ?> </div> </div>
Erstellen der Erstberichterstellung
Bei meiner anfänglichen Statistikberichterstattung habe ich mich auf einfache Echtzeitdaten und detaillierte historische Daten konzentriert. Bei meiner anfänglichen Statistikberichterstattung habe ich mich auf einfache Echtzeitdaten und detaillierte historische Daten konzentriert.
Echtzeitdaten würden beispielsweise die Anzahl der Benutzer und Besprechungen anzeigen, die auf dem System erstellt wurden, und deren Status. Die historischen Daten würden Ihnen sagen, wie viele Benutzer und Meetings im Laufe der Zeit abgeschlossen wurden, sowie andere interessante Daten - insbesondere Wachstumskurven, die ich und potenzielle Investoren interessieren könnten.
Echtzeitdaten
Die Echtzeitdatenseite muss einen Live-Snapshot dessen anzeigen, was auf der Site passiert. Anfangs wollte ich wissen:
- Wie viele Meetings gibt es im System?
- Wie viele Benutzer gibt es?
- Wie ist ihr Status?
Um dies zu erreichen, habe ich ein Back-End-Modell DataController.php und Data.php erstellt. Ich machte auch einen Schritt vorwärts und anstatt HTML-Rohdaten in meiner Ansicht zu erstellen, um dies anzuzeigen, habe ich ActiveDataProvider aus meinen Abfragen erstellt und sie Yi's Grid-Widgets zugeführt.
Das Ergebnis sieht besser aus und ist einfacher zu erstellen und zu warten. Dieser Code fragt die Anzahl der Besprechungen im System nach ihrem Status ab:
public static function getRealTimeData() { $data = new \stdClass(); $data->meetings = new ActiveDataProvider([ 'query' => Meeting::find() ->select(['status,COUNT(*) AS dataCount']) //->where('approved = 1') ->groupBy(['status']), 'pagination' => [ 'pageSize' => 20, ], ]);
Dieser Code in /backend/views/data/current.php zeigt es an:
<?php /* @var $this yii\web\View */ use yii\grid\GridView; use common\models\User; use frontend\models\Meeting; $this->title = Yii::t('backend','Meeting Planner'); ?> <div class="site-index"> <div class="body-content"> <h1>Real Time Data</h1> <h3>Meetings</h3> <?= GridView::widget([ 'dataProvider' => $data->meetings, 'columns' => [ [ 'label'=>'Status', 'attribute' => 'status', 'format' => 'raw', 'value' => function ($model) { return '<div>'.Meeting::lookupStatus($model->status).'</div>'; }, ], 'dataCount', ], ]); ?>
Es sieht so aus (die Daten sind klein, da die Site noch nicht gestartet wurde!):

Dann habe ich ein paar weitere Echtzeitabfragen erstellt und der Rest der Seite sieht folgendermaßen aus:

Wenn Sie in den oben aufgeführten Spalten "Aktive Personen" und "Via Einladung" eine Person zu einer Besprechung einladen, zählen wir sie als Benutzer über die Einladung, bis sie ein Kennwort erstellen oder ihr soziales Konto verknüpfen. Bis dahin haben sie nur Zugriff auf den Besprechungsplaner E-Mail-Einladungslink und seine Authentifizierungs-ID Natürlich werde ich die Echtzeit-Reporting-Optionen erweitern, während sich das Projekt weiterentwickelt.
Natürlich werde ich die Echtzeit-Reporting-Optionen erweitern, während sich das Projekt weiterentwickelt.
Historische Daten melden
Die Erstellung historischer Berichte für systemweite Aktivitäten erwies sich als etwas komplizierter. Ich beschloss, einige abhängige Datenerfassungsschichten zu erstellen.
Die unterste Ebene ist eine UserData-Tabelle, die den Status der bisherigen Kontoaktivität einer Person bis zu einem bestimmten Tag um Mitternacht zusammenfasst. Im Wesentlichen werden wir das jeden Abend machen.
Die oberste Ebene ist die Tabelle "HistoricalData", die ihre Berechnungen mit der Tabelle "UserData" der vorherigen Nacht erstellt.
Ich musste auch einen Code schreiben, der die beiden Tabellen von Grund auf neu erstellte, da unser Dienst einige Monate lang aktiv war.
Ich werde dich durch das führen, wie ich das gemacht habe. Das Ergebnis ist ganz gut ausgegangen.
Tabellenmigrationen erstellen
Hier ist die Tabellenmigration für UserData. Sie enthält die Daten, die ich nachts berechnen möchte, um die historischen Berechnungen zu unterstützen:
public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%user_data}}', [ 'id' => Schema::TYPE_PK, 'user_id' => Schema::TYPE_BIGINT.' NOT NULL', 'is_social' => Schema::TYPE_SMALLINT.' NOT NULL', 'invite_then_own' => Schema::TYPE_SMALLINT.' NOT NULL', 'count_meetings' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meetings_last30' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meeting_participant' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meeting_participant_last30' => Schema::TYPE_INTEGER.' NOT NULL', 'count_places' => Schema::TYPE_INTEGER.' NOT NULL', 'count_friends' => Schema::TYPE_INTEGER.' NOT NULL', 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', ], $tableOptions); $this->addForeignKey('fk_user_data_user_id', '{{%user_data}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE'); }
Beispiel: count_meeting_participant_last30
gibt an, zu wie vielen Besprechungen diese Person in den letzten 30 Tagen eingeladen wurde.
Hier ist die Tabellenmigration für HistoricalData
- fast alle Spalten in dieser Tabelle müssen aus verschiedenen Datenschichten berechnet werden:
public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%historical_data}}', [ 'id' => Schema::TYPE_PK, 'date'=> Schema::TYPE_INTEGER.' NOT NULL', 'percent_own_meeting' => Schema::TYPE_FLOAT.' NOT NULL', 'percent_own_meeting_last30' => Schema::TYPE_FLOAT.' NOT NULL', // % of users invited by others who own a meeting 'percent_invited_own_meeting' => Schema::TYPE_FLOAT.' NOT NULL', 'percent_participant' => Schema::TYPE_FLOAT.' NOT NULL', 'percent_participant_last30' => Schema::TYPE_FLOAT.' NOT NULL', 'count_users' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meetings_completed' => Schema::TYPE_INTEGER.' NOT NULL', 'count_meetings_planning' => Schema::TYPE_INTEGER.' NOT NULL', 'count_places' => Schema::TYPE_INTEGER.' NOT NULL', 'average_meetings' => Schema::TYPE_FLOAT.' NOT NULL', 'average_friends' => Schema::TYPE_FLOAT.' NOT NULL', 'average_places' => Schema::TYPE_FLOAT.' NOT NULL', 'source_google' => Schema::TYPE_INTEGER.' NOT NULL', 'source_facebook' => Schema::TYPE_INTEGER.' NOT NULL', 'source_linkedin' => Schema::TYPE_INTEGER.' NOT NULL', ], $tableOptions);
Im Gespräch mit meinem Berater haben wir festgestellt, dass
potenzielle Investoren wissen möchten, wie die Leute auf die Website reagieren. Ich habe eine Messung für eine Metrik namens percent_invited_own_meeting erstellt
, kurz für den Prozentsatz der zu ihrer ersten Besprechung eingeladenen Benutzer, denen der Dienst so gut gefallen hat um in Zukunft ein eigenes Treffen zu planen. Ich werde mehr über die Berechnungen ein wenig weiter unten lesen.
Die Migrationen befinden sich alle in /console/migrations. So sieht es aus, wenn Sie die Datenbankmigrationen ausführen.
$ ./yii migrate/up Yii Migration Tool (based on Yii v2.0.8) Total 2 new migrations to be applied: m160609_045838_create_user_data_table m160609_051532_create_historical_data_table Apply the above migrations? (yes|no) [no]:yes *** applying m160609_045838_create_user_data_table > create table {{%user_data}} ... done (time: 0.003s) > add foreign key fk_user_data_user_id: {{%user_data}} (user_id) references {{%user}} (id) ... done (time: 0.004s) *** applied m160609_045838_create_user_data_table (time: 0.013s) *** applying m160609_051532_create_historical_data_table > create table {{%historical_data}} ... done (time: 0.003s) *** applied m160609_051532_create_historical_data_table (time: 0.005s) 2 migrations were applied. Migrated up successfully.
Sammeln der Berichtsdaten
Jede Nacht nach Mitternacht berechnet eine Hintergrundaufgabe die Statistiken der vorherigen Nacht. Hier ist die Hintergrundmethode:
public function actionOvernight() { $since = mktime(0, 0, 0); $after = mktime(0, 0, 0, 2, 15, 2016); UserData::calculate(false,$after); HistoricalData::calculate(false,$after); }
Ich habe einen Cron-Job eingerichtet, um ActionOvernight
täglich um 1:15 Uhr zu starten. Hinweis: Wenn Sie sich bei Tag und Nacht intensiv auf einen Startup-Vorgang konzentrieren, geht es bei einem Cron-Job um all die Action, die Sie über Nacht bekommen.
Um die Geschichte der Vergangenheit aufzubauen, habe ich eine one-timerecalc()
-Funktion erstellt. Dies spült die Tische und baut jeden Tisch so auf, als ob er Tag für Tag passiert.
public static function recalc() { UserData::reset(); HistoricalData::reset(); $after = mktime(0, 0, 0, 2, 15, 2016); $since = mktime(0, 0, 0, 4, 1, 2016); while ($since < time()) { UserData::calculate($since,$after); HistoricalData::calculate($since,$after); // increment a day $since+=24*60*60; } }
Hinweis: Die Nachlaufzeit
ist eine Problemumgehung, um einige der frühen Benutzer auszuschließen, die sich angemeldet haben, bevor sie ein Meeting planen konnten. Ich wollte, dass die historischen Daten eine genauere Darstellung der letzten Aktivität widerspiegeln (derzeit gibt es ein paar hundert ältere Konten ohne) Aktivität). Ich werde das wahrscheinlich zu einem späteren Zeitpunkt entfernen.
Berechnung der Benutzerdatentabelle
Hier ist der Code, der die UserData
-Tabelle jeden Abend füllt:
public static function calculate($since=false,$after = 0) { if ($since===false) { $since = mktime(0, 0, 0); } $monthago = $since-(60*60*24*30); $all = User::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->all(); foreach ($all as $u) { // create new record for user or update old one $ud = UserData::find()->where(['user_id'=>$u->id])->one(); if (is_null($ud)) { $ud = new UserData(); $ud->user_id = $u->id; $ud->save(); } $user_id = $u->id; // count meetings they've organized $ud->count_meetings = Meeting::find()->where(['owner_id'=>$user_id])->andWhere('created_at<'.$since)->count(); $ud->count_meetings_last30 = Meeting::find()->where(['owner_id'=>$user_id])->andWhere('created_at<'.$since)->andWhere('created_at>='.$monthago)->count(); // count meetings they were invited to $ud->count_meeting_participant = Participant::find()->where(['participant_id'=>$user_id])->andWhere('created_at<'.$since)->count(); $ud->count_meeting_participant_last30 = Participant::find()->where(['participant_id'=>$user_id])->andWhere('created_at<'.$since)->andWhere('created_at>='.$monthago)->count(); // count places and Friends $ud->count_places = UserPlace::find()->where(['user_id'=>$user_id])->andWhere('created_at<'.$since)->count(); $ud->count_friends = Friend::find()->where(['user_id'=>$user_id])->andWhere('created_at<'.$since)->count(); // calculate invite than Own - participant first, then organizer $first_invite = Participant::find()->where(['participant_id'=>$user_id])->andWhere('created_at<'.$since)->orderby('created_at asc')->one(); $first_organized = Meeting::find()->where(['owner_id'=>$user_id])->andWhere('created_at<'.$since)->orderby('created_at asc')->one(); $ud->invite_then_own =0; if (!is_null($first_invite) && !is_null($first_organized)) { if ($first_invite->created_at < $first_organized->created_at && $first_organized->created_at < $since) { // they were invited as a participant earlier than they organized their own meeting $ud->invite_then_own =1; } } if (Auth::find()->where(['user_id'=>$user_id])->count()>0) { $ud->is_social =1; } else { $ud->is_social =0; } $ud->update(); } }
Es ist meist nur die Summen für Benutzer von Meetings, Orten, Freunden und in einigen Fällen in Zeitbereichen der letzten 30 Tage.
Hier ist der Code, der feststellt, ob dieser Benutzer nach der Einladung ein Meeting mit dem Dienst geplant hat:
$ud->invite_then_own =0; if (!is_null($first_invite) && !is_null($first_organized)) { if ($first_invite->created_at < $first_organized->created_at && $first_organized->created_at < $since) { // they were invited as a participant earlier than they organized their own meeting $ud->invite_then_own =1; } }
Die HistoricalData berechnen
Hier ist der Code, der UserData
zum Auffüllen vonHistoricalData
nutzt:
public static function calculate($since = false,$after=0) { if ($since === false) { $since = mktime(0, 0, 0); } // create new record for date or update existing $hd = HistoricalData::find()->where(['date'=>$since])->one(); if (is_null($hd)) { $hd = new HistoricalData(); $hd->date = $since; $action = 'save'; } else { $action = 'update'; } // calculate $count_meetings_completed $hd->count_meetings_completed = Meeting::find()->where(['status'=>Meeting::STATUS_COMPLETED])->andWhere('created_at<'.$since)->count();; // calculate $count_meetings_planning $hd->count_meetings_planning = Meeting::find()->where('status<'.Meeting::STATUS_COMPLETED)->andWhere('created_at<'.$since)->count();; // calculate $count_places $hd->count_places = Place::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->count(); // calculate $source_google $hd->source_google = Auth::find()->where(['source'=>'google'])->count(); // calculate $source_facebook $hd->source_facebook = Auth::find()->where(['source'=>'facebook'])->count(); // calculate $source_linkedin $hd->source_linkedin = Auth::find()->where(['source'=>'linkedin'])->count(); // total users $total_users = UserData::find()->count(); // calculate $count_users $hd->count_users = $total_users; //User::find()->where('status<>'.User::STATUS_DELETED)->andWhere('created_at>'.$after)->count(); $total_friends = Friend::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->count(); $total_places = Place::find()->where('created_at>'.$after)->andWhere('created_at<'.$since)->count(); if ($total_users >0) { $hd->average_meetings = ($hd->count_meetings_completed+$hd->count_meetings_planning)/$total_users; $hd->average_friends = $total_friends/$total_users; $hd->average_places = $total_places/$total_users; $hd->percent_own_meeting = UserData::find()->where('count_meetings>0')->count() / $total_users; $hd->percent_own_meeting_last30 = UserData::find()->where('count_meetings_last30>0')->count() / $total_users; $hd->percent_participant = UserData::find()->where('count_meeting_participant>0')->count() / $total_users; $hd->percent_participant_last30 = UserData::find()->where('count_meeting_participant_last30>0')->count() / $total_users; $query = (new \yii\db\Query())->from('user_data'); $sum = $query->sum('invite_then_own'); $hd->percent_invited_own_meeting=$sum/$total_users; } if ($action=='save') { $hd->save(); } else { $hd->update(); } }
Es fasst Summen zusammen und berechnet Prozentsätze und Durchschnittswerte.
So sieht das fertige Produkt aus:

Obwohl wir die Analyse nur der Verwendung vor dem Alpha sehen, sind die Daten faszinierend, und die potenzielle Nützlichkeit scheint hervorragend zu sein. Und natürlich wird es einfach sein, die Datensammlung und Analyse mit dem Grundlagentext, den ich heute mit Ihnen geteilt habe, zu erweitern.
Der Prozentsatz der eingeladenen Benutzer, die ihre eigenen Meetings planen, beträgt übrigens etwa 9% (aber es ist ein kleiner Datensatz).
Sie fragen sich wahrscheinlich, ob wir diese Spalten darstellen können. Ich hoffe, dass ich das in einem Nachfolge-Tutorial anspreche, das immer eine Interaktion mit den redaktionellen Göttinnen erfordert. Just FYI, nicht jeder geht von diesen Gesprächen weg. Ich werde sie auch bitten, mir zu erlauben, über Verwaltungsfunktionen wie das Deaktivieren von Benutzern, das erneute Senden von Passwörtern usw. zu schreiben.

Wenn du nicht mehr von mir hörst, dann wisse, dass der Herr des Lichts einen Nutzen für mich gefunden hat.
Was kommt als nächstes?
Wie bereits erwähnt, arbeite ich fieberhaft daran, Meeting Planner für die Alpha-Version vorzubereiten. Ich konzentriere mich hauptsächlich auf die wichtigsten Verbesserungen und Funktionen, die das Alpha-Release reibungslos funktionieren lassen.
Ich verfolge jetzt alles in Asana, worüber ich in einem kommenden Tutorial schreiben werde; es war unglaublich hilfreich. Es sind auch einige interessante neue Features auf dem Weg. (Als Yogalehrerin denke ich, dass Asana der schlechteste Produktname aller Zeiten ist. Sie haben im Yoga einen Ausdruck genommen, der āsana oder ah-sana ausgesprochen hat und die Aussprache in a-sauna geändert hat - und dies in ihren Einführungsvideos war letztes Jahr nicht einfach, mit den Teammitgliedern darüber zu sprechen, was sie in eine Sauna getan haben und mit Yogis über Āsana gesprochen haben. Aber ich schweife ab.)
Ich fange auch an, mich mehr auf das bevorstehende Sammeln von Investitionen mit Meeting Planner zu konzentrieren. Ich fange gerade an, mit WeFunder zu experimentieren, basierend auf der Implementierung der neuen Crowdfunding-Regeln der SEC. Bitte beachten Sie unser Profil. Darüber werde ich in einem zukünftigen Tutorial noch mehr schreiben.
Während Sie auf weitere Folgen warten, planen Sie Ihr erstes Meeting und testen Sie die Vorlagen mit Ihren Freunden mit Google Mail-Postfächern. Ich würde es auch schätzen, wenn Sie Ihre Erfahrungen in den Kommentaren teilen und ich bin immer an Ihren Vorschlägen interessiert. Sie können mich auch auf Twitter @reifman direkt erreichen. Sie können sie auch auf der Meeting Planner-Support-Website veröffentlichen.
Achten Sie auf kommende Tutorials in der Build Your Startup With PHP-Serie.
verwandte Links •
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post