Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения.
Постоянное развитие групповых встреч
Добро пожаловать! Это продолжение серии « Создание стартапа: встречи с несколькими участниками» . Сегодня я закончу работу, которую мы начали в этом эпизоде: планирование нескольких встреч участников.
Краткое освежение
Планирование встреч с несколькими участниками всегда было главной целью для Планировщика собраний, который запускался только с расписанием 1: 1. Встречи с несколькими участниками являются наиболее сложными для людей, чтобы попытаться составить расписание друг с другом, и поэтому являются одной из наиболее ценных функций, предоставляемых службой Планировщик собраний.
В сегодняшнем уроке я расскажу о рассмотрении всех областей сайта, затронутых несколькими собраниями участников, обработке и умном отображении списков получателей различного статуса, правильном управлении уведомлениями и фильтрации уведомлений для групп и, наконец, обновлении недавно запущенного запроса. особенность изменений встречи .
Запланируйте свою первую групповую встречу
Пожалуйста, запланируйте свою собственную групповую встречу сегодня ! Пригласите нескольких друзей встретиться с вами для kombucha или kava. Поделитесь своими мыслями и отзывами об опыте каждого в комментариях ниже. Я участвую в обсуждениях, но вы также можете связаться со мной @reifman в Twitter. Я всегда открыт для новых идей для планировщика собраний, а также предложений для будущих серий.
Напоминаем, что весь код для Meeting Planner предоставляется с открытым исходным кодом и написан на Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с моей параллельной серией Программирование с Yii2 .
Просмотр кода
Как вы можете себе представить, преобразование Планировщика собраний из собраний 1: 1 в групповые собрания коснулось почти всего кода. Мне пришлось продумать все области, пересмотреть код и внести небольшие или умеренные изменения во многих местах. В других областях я разработал несколько участников, и изменения не были нужны или были незначительными.
Например, групповое планирование коснулось:
- Отправка приглашений и завершение (подтверждение) расписаний встреч
- Внесение изменений, повторная отправка, повторение, перенос расписания встречи
- Отправка с опозданием
- Напоминания о встречах
- Запрос изменений встречи
- Уведомления
В основном, когда я ранее просто отправлял участнику [0], первому и единственному участнику, мне теперь нужно было обработать массив участников. И при этом мне нужно было проверить:
- Является ли этот участник организатором?
- Этот человек был удален или отклонен самостоятельно?
- В будущем этот человек отказался от уведомлений об этой встрече?
Проблемы тестирования
Имея больше ресурсов, я мог бы справиться с этим более полно с помощью автоматического тестирования. Тем не менее, работая в одиночку с поставкой в качестве цели, я полностью проверил все вручную.
Я использовал электронную почту домена catchall, чтобы пригласить n1, n2, n3, n4 и n5 @ mytestdomain.com на собрания моей группы образцов. К счастью, приглашения Планировщика собраний позволяют быстро войти в систему с любой учетной записью, щелкнув каждое приглашение на собрание; это помогло моему тестированию.
Важно было просмотреть почти весь код планирования собраний.
Но вернемся к более конкретным задачам кодирования второй половины функции группового планирования.
Умное отображение списков участников
Некоторое время назад я MiscHelpers
метод MiscHelpers
для грамматического отображения списков на английском языке с «и» перед фамилией, как показано в указателе собрания ниже:
Однако я хотел упростить отображение даты, времени и места доступности. Например, вместо того, чтобы перечислять пять имен людей, которые приняли встречу в Herkimer Coffee, я обновил MiscHelpers::listNames
чтобы сказать «все остальные»:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public static function listNames($items,$everyoneElse=false,$total_count=0,$anyoneElse=false) {
$temp =»;
$x=1;
$cnt = count($items);
if ($everyoneElse && $cnt >= ($total_count-1)) {
if (!$anyoneElse) {
$temp = Yii::t(‘frontend’,’everyone else’);
} else {
$temp = Yii::t(‘frontend’,’anyone else’);
}
} else {
foreach ($items as $i) {
$temp.= MiscHelpers::getDisplayName($i);
if ($x == ($cnt-1)) {
$temp.=’ and ‘;
} else if ($x < ($cnt-1)) {
$temp.=’, ‘;
}
$x+=1;
}
}
return $temp;
}
|
Вы можете увидеть это в действии ниже:
Но вместо того, чтобы сказать «нет ответа от всех остальных», правильнее будет сказать «нет ответа от кого-либо еще», что делает код.
Ниже вы можете увидеть, что MeetingPlace::getWhereStatus()
подготавливает эти строки для каждого места на панели мест собраний:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public static function getWhereStatus($meeting,$viewer_id) {
// get an array of textual status of meeting places for $viewer_id
// Acceptable / Rejected / No response:
$whereStatus[‘style’] = [];
$whereStatus[‘text’] = [];
foreach ($meeting->meetingPlaces as $mp) {
// build status for each place
$acceptableChoice=[];
$rejectedChoice=[];
$unknownChoice=[];
// to do — add meeting_id to MeetingPlaceChoice for sortable queries
foreach ($mp->meetingPlaceChoices as $mpc) {
if ($mpc->user_id == $viewer_id) continue;
switch ($mpc->status) {
case MeetingPlaceChoice::STATUS_UNKNOWN:
$unknownChoice[]=$mpc->user_id;
break;
case MeetingPlaceChoice::STATUS_YES:
$acceptableChoice[]=$mpc->user_id;
break;
case MeetingPlaceChoice::STATUS_NO:
$rejectedChoice[]=$mpc->user_id;
break;
}
}
// to do — integrate current setting for this user in style setting
$temp =»;
// count those still in attendance
$cntP = Participant::find()
->where([‘meeting_id’=>$meeting->id])
->andWhere([‘status’=>Participant::STATUS_DEFAULT])
->count()+1;
if (count($acceptableChoice)>0) {
$temp.=’Acceptable to ‘.MiscHelpers::listNames($acceptableChoice,true,$cntP).’.
$whereStatus[‘style’][$mp->place_id]=’success’;
}
if (count($rejectedChoice)>0) {
$temp.=’Rejected by ‘.MiscHelpers::listNames($rejectedChoice,true,$cntP).’.
$whereStatus[‘style’][$mp->place_id]=’danger’;
}
if (count($unknownChoice)>0) {
$temp.=’No response from ‘.MiscHelpers::listNames($unknownChoice,true,$cntP,true).’.’;
$whereStatus[‘style’][$mp->place_id]=’warning’;
}
$whereStatus[‘text’][$mp->place_id]=$temp;
}
return $whereStatus;
}
|
У каждого пользователя есть строка MeetingPlaceChoice
связанная с MeetingPlace
которой записывается, является ли место приемлемым, неприемлемым или еще не отвеченным. MeetingTimeChoice
также существует аналогичным образом. Эта информация передается в listNames()
.
Отклонение и выход из встречи
Группам также требовалось больше сложности с отклонением приглашения на встречу. Ранее, если один участник отказался, собрание было фактически отменено. Теперь можно отказаться, оставив еще трех.
Поэтому, если участник получает приглашение на встречу, он может отклонить его. Но если встреча уже была подтверждена и завершена, то они, по сути, выводятся, как вы можете видеть ниже:
Примечание. На изображении выше Сара Смитерс видит кнопку « Снять» ; Роберт МакСмит был удален другим организатором, Джеффом или Алексом .
Однако, если это организатор (владелец собрания или назначенный участник-организатор), они могут просто отменить собрание. Ниже от _command_bar_confirmed.php. Он определяет, какие кнопки представить:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
if (!$isPast) {
if ($model->isOrganizer()) {
echo Html::a(‘<i class=»glyphicon glyphicon-remove-circle»></i> ’.Yii::t(‘frontend’, ‘Cancel’), [‘cancel’, ‘id’ => $model->id],
[‘class’ => ‘btn btn-primary btn-danger’,
‘title’=>Yii::t(‘frontend’,’Cancel’),
‘data-confirm’ => Yii::t(‘frontend’, ‘Are you sure you want to cancel this meeting?’)
]) ;
}
else {
if ($model->getParticipantStatus(Yii::$app->user->getId())==Participant::STATUS_DEFAULT) {
echo Html::a(‘<i class=»glyphicon glyphicon-remove-circle»></i> ’.Yii::t(‘frontend’, ‘Withdraw’), [‘decline’, ‘id’ => $model->id],
[‘class’ => ‘btn btn-primary btn-danger’,
‘title’=>Yii::t(‘frontend’,’Withdraw from the meeting’),
‘data-confirm’ => Yii::t(‘frontend’, ‘Are you sure you want to decline attendance to this meeting?’)
]) ;
} else {
// to do — offer rejoin meeting option
}
|
Расстановка приоритетов является ключевым элементом построения стартапа. Поэтому, хотя я хотел предложить пользователю, который отошел, присоединиться к собранию, я решил добавить его в список задач Asana на потом. Пользователю, который присоединяется, потребуются обновленные уведомления и, возможно, некоторые обновления их структур данных планирования.
Думая через уведомления
В то время как при собраниях 1: 1 каждое изменение нужно было отправлять другой стороне, это не обязательно имеет смысл для собраний из 12 человек — или так? По-разному.
Изначально я создал общие рекомендации. Если участник обновлял свои предпочтения для определенной даты или времени, об этом нужно было сообщить только владельцу и другим организаторам.
Я создал массив $groupSkip
в MeetingLog
который определял события, которые не следует отправлять другим участникам:
1
2
3
4
5
6
7
8
|
public static $groupSkip=[
MeetingLog::ACTION_ACCEPT_ALL_PLACES,
MeetingLog::ACTION_ACCEPT_PLACE,
MeetingLog::ACTION_REJECT_PLACE,
MeetingLog::ACTION_ACCEPT_ALL_TIMES,
MeetingLog::ACTION_ACCEPT_TIME,
MeetingLog::ACTION_REJECT_TIME
];
|
В MeetingLog::getHistory
мы пропускаем уведомление участника об этих событиях, но всегда уведомляем организаторов:
1
2
3
4
5
6
7
8
|
if (
…
// skip over availability response events in multi participant meetings
($isGroup && !$isOrganizer && in_array($e->action,MeetingLog::$groupSkip))
) {
$num_events-=1;
continue;
}
|
В одном необычном примере код был на самом деле упрощен для нескольких участников: Meeting::findFresh()
, который ищет обновления изменений собрания, чтобы поделиться ими по электронной почте.
Ранее нам нужно было определить, кто из двух пользователей выполнил действия и стоит ли уведомлять либо. Теперь мы просто уведомляем владельца, а затем уведомляем участников:
01
02
03
04
05
06
07
08
09
10
11
12
|
if ((time()-$m->logged_at) > MeetingLog::TIMELAPSE && $m->status>=Meeting::STATUS_SENT) { //
// get logged items which occured after last cleared_at
$m->notify($m->id,$m->owner_id);
// notify the participants
foreach ($m->participants as $p) {
// don’t update removed and declined participants
if ($p->status!=Participant::STATUS_DEFAULT) {
continue;
}
//echo ‘Notify P-id: ‘.$p->participant_id.'<br />’;
$m->notify($m->id,$p->participant_id);
}
|
Любая фильтрация выполняется глубже в текстуализации журнала событий.
Расширенные уведомления: «Все доступно!»
Я также создал новое уведомление для оповещения организаторов, когда все согласны хотя бы с одним конкретным местом и временем, MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE
:
1
2
3
4
5
|
// check if meeting has place and time for everyone now
if (count($m->participants)>1 && !MeetingLog::hasEventOccurred($m->id,MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE) && Meeting::isEveryoneAvailable($m->id)) {
Meeting::notifyOrganizers($m->id,MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE);
MeetingLog::add($m->id,MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE,0);
}
|
Это уведомляет организаторов, когда собрание готово завершить / подтвердить.
Вот код, который просматривает все места встреч и время встреч, чтобы узнать, согласны ли все хотя бы на одно место:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public static function isEveryoneAvailable($meeting_id) {
// check that one place works for everyone attending
$m = Meeting::findOne($meeting_id);
$cntAll = $m->countAttendingParticipants(true);
// count organizer + attending participants
$mpExists=false;
$mtExists=true;
$mps = \frontend\models\MeetingPlace::find()->where([‘meeting_id’=>$meeting_id])->all();
foreach ($mps as $mp) {
$cnt=0;
foreach ($mp->meetingPlaceChoices as $mpc) {
if ($m->getParticipantStatus($mpc->user_id)!=Participant::STATUS_DEFAULT) {
// skip withdrawn, declined, removed participants
continue;
}
if ($mpc->status == \frontend\models\MeetingPlaceChoice::STATUS_YES) {
$cnt+=1;
}
}
if ($cnt >=$cntAll) {
$mpExists = true;
}
}
$mts = \frontend\models\MeetingTime::find()->where([‘meeting_id’=>$meeting_id])->all();
foreach ($mts as $mt) {
$cnt=0;
foreach ($mt->meetingTimeChoices as $mtc) {
if ($m->getParticipantStatus($mtc->user_id)!=Participant::STATUS_DEFAULT) {
// skip withdrawn, declined, removed participants
continue;
}
if ($mtc->status == \frontend\models\MeetingTimeChoice::STATUS_YES) {
$cnt+=1;
}
}
if ($cnt >=$cntAll) {
$mtExists = true;
}
}
// at least one time and one place works for everyone attending
if ($mpExists && $mtExists) {
return true;
} else {
return false;
}
}
|
Точно так же я построил функцию для уведомления организатора о том, что ни одно из значений времени и места не является приемлемым для кого-либо, Meeting::isSomeoneAvailable()
:
1
2
3
4
5
6
7
8
|
if ($model->status <= Meeting::STATUS_SENT) {
if ($model->isOrganizer()
&& ($model->status == Meeting::STATUS_SENT)
&& !$model->isSomeoneAvailable()) {
Yii::$app->getSession()->setFlash(‘danger’,
Yii::t(‘frontend’,
‘None of the participants are available for the meeting\’s current options.’));
}
|
Это указывает на то, что они должны предложить дополнительные даты и / или места.
Обновление напоминаний о встречах
Все, что касается напоминаний о собраниях, хорошо работало для нескольких участников, но мне нужно было отключить напоминания, если участник отклонился от участия в совещании или был удален:
1
2
3
4
5
6
|
$cnt =1;
foreach ($mtg->participants as $p) {
if ($p->status ==Participant::STATUS_DEFAULT) {
$attendees[$cnt]=$p->participant_id;
$cnt+=1;
}
|
STATUS_DEFAULT
указывает посетителя, который также должен быть добавлен в массив пользователей для отправки напоминаний по электронной почте.
Просмотр файлов календаря
Я также проверил работу по созданию файлов календаря для приглашений, чтобы убедиться, что все участники включены. В Meeting::prepareDownloadIcs()
я собрал массив посетителей с владельцем и участниками, которые активно посещают:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
$attendees = array();
foreach ($m->participants as $p) {
if ($p->status ==Participant::STATUS_DEFAULT) {
$auth_key=\common\models\User::find()->where([‘id’=>$p->participant_id])->one()->auth_key;
$attendees[$cnt]=[‘user_id’=>$p->participant_id,’auth_key’=>$auth_key,
’email’=>$p->participant->email,
‘username’=>$p->participant->username];
$cnt+=1;
// reciprocate friendship to organizer
\frontend\models\Friend::add($p->participant_id,$p->invited_by);
// to do — reciprocate friendship in multi participant meetings
}
}
$auth_key=\common\models\User::find()->where([‘id’=>$m->owner_id])->one()->auth_key;
$attendees[$cnt]=[‘user_id’=>$m->owner_id,
‘auth_key’=>$auth_key,
’email’=>$m->owner->email,
‘username’=>$m->owner->username];
foreach ($attendees as $cnt=>$a) {
if ($a[‘user_id’]==$actor_id) {
$icsPath = Meeting::buildCalendar($m->id,$chosenPlace,$chosenTime,$a,$attendees);
|
За это время я также узнал, как указать, что файл календаря отмененной встречи должен инициировать удаление события из чьего-либо календаря. Стандарт ИКС является мощным, хотя не легко выучить.
Обновление изменений запросов для групп
Как я писал недавно, функция « Запрашивать изменения собрания» потребовала много работы и нового UX.
Для нескольких встреч участников социальная инженерия должна была измениться снова. Например, организаторы могут принимать или отклонять запросы и изменять расписание встреч. Тем не менее, участники могут выражать только «нравится», «не нравится» или «не заботиться» о запросах на изменение. И организаторы должны видеть ответы всех участников, чтобы помочь им в принятии решений.
Вот что видит участник после отправки запроса на изменение:
Новые запросы на изменение должны быть отправлены всем участникам. Это прозрачно обрабатывается уведомлениями журнала активности. Когда запрос сделан, это событие создается в RequestController::actionCreate()
submit:
1
|
MeetingLog::add($model->meeting_id,MeetingLog::ACTION_REQUEST_CREATE,Yii::$app->user->getId(),$model->id);
|
Вот как выглядит запрошенное уведомление об изменении для других участников:
Всех просят ответить. Нажатие « Ответить на запрос» переходит прямо к запросу. Или вы можете найти его в списке запросов по ссылке на оповещение о вспышке на встрече, показанной выше.
Новый UX для участников, отвечающих на запросы
Вот форма, которую участники видят, когда они отвечают на запрос:
Если уже есть ответы других участников, они увидят их:
Вот верхняя часть этой формы в /frontend/views/request-response/_form.php:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<p><em>
<?= $subject ?>
</em>
</p>
<?= GridView::widget([
‘dataProvider’ => $responseProvider,
‘columns’ => [
[
‘label’=>’Responses from Other Participants’,
‘attribute’ => ‘responder_id’,
‘format’ => ‘raw’,
‘value’ => function ($model) {
$note=»;
if (!empty($model->note)) {
$note = ‘ said, «‘.$model->note.’»‘;
}
return ‘<div>’.MiscHelpers::getDisplayName($model->responder_id).’
},
],
],
]);
<div class=»request-response-form»>
<?php $form = ActiveForm::begin();
<?= BaseHtml::activeHiddenInput($model, ‘responder_id’);
<?= BaseHtml::activeHiddenInput($model, ‘request_id’);
<?= $form->field($model, ‘note’)->label(Yii::t(‘frontend’,’Include a note’))->textarea([‘rows’ => 6])->hint(Yii::t(‘frontend’,’optional’)) ?>
|
В Gridview перечислены существующие ответы, например, «Нравится», «Не нравится», «Нейтрально» и любые личные заметки.
Затем, вот код для нижней половины формы, который будет отображать « нравится», «не нравится», «все равно» для участников, но принимать и вносить изменения или отклонять запрос для владельцев.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
<?php
if (!$isOwner && $isOrganizer) {
?>
<p><em><?= Yii::t(‘frontend’,’Since you are an organizer, you can accept the request and make the changes or reject it.’);?></em></p>
<?php
}
?>
<?php
if ($isOrganizer) {
?>
<div class=»form-group»>
<?= Html::submitButton(Yii::t(‘frontend’, ‘Accept and Make Changes’), [‘class’ => ‘btn btn-success’,’name’=>’accept’,]) ?>
<?= Html::submitButton(Yii::t(‘frontend’, ‘Decline Request’),[‘class’ => ‘btn btn-danger’,’name’=>’reject’,
‘data’ => [
‘confirm’ => Yii::t(‘frontend’, ‘Are you sure you want to decline this request?’),
‘method’ => ‘post’,
],]) ?>
</div>
<?php
}
?>
<?php
if (!$isOwner && $isOrganizer) {
?>
<p><em><?= Yii::t(‘frontend’,’Or, you can just express your opinion and defer to other organizers.’);?></em></p>
<?php
}
?>
<?php
if (!$isOwner) {
?>
<?php
if (!$isOrganizer) {
?>
<p><em><?= Yii::t(‘frontend’,’Please share your opinion of this request for the organizers to consider.’);?></em></p>
<?php
}
?>
<div class=»form-group»>
<?= Html::submitButton(Yii::t(‘frontend’, ‘Like’), [‘class’ => ‘btn btn-success’,’name’=>’like’,]) ?>
<?= Html::submitButton(Yii::t(‘frontend’, ‘Don\’t Care’), [‘class’ => ‘btn btn-info’,’name’=>’neutral’,]) ?>
<?= Html::submitButton(Yii::t(‘frontend’, ‘Dislike’),[‘class’ => ‘btn btn-danger’,’name’=>’dislike’,]) ?>
</div>
<?php
}
?>
<?php ActiveForm::end();
|
Участникам, которые являются помазанными организаторами, показаны оба набора кнопок, и они могут либо высказать свое мнение, либо внести или отклонить изменение.
Вот электронное уведомление о том, что изменение было принято:
Конечно, обновленное приглашение и файлы календаря будут разосланы всем, если будут внесены изменения.
Всегда больше улучшений, чтобы сделать
Надеюсь, вам понравились эти два эпизода (сегодняшний и « Построение стартапа: встречи с несколькими участниками» ). В режиме запуска с огромной новой функцией всегда есть целенаправленное, оптимизированное усилие по запуску, которое оставляет множество незакрепленных концов невыполненными и неполированные дефекты.
Вот несколько примеров этого:
- Присоединение к собраниям, от которых вы отказались или от которых отказались.
- Улучшено представление списка участников в приглашениях.
- Варианты сохранения списка участников и / или их индивидуальных статусов закрытыми от других участников.
- Улучшена обработка и представление контактной информации группы и сведений о виртуальной конференции, например, линия конференции и код участия.
- Безопасный URL для обмена приглашениями на собрания. Это позволило бы организаторам поделиться URL-адресом на Facebook или по электронной почте, чтобы пригласить новых участников.
Несмотря на эти недостатки, я очень усердно работал, чтобы достичь такого уровня функций и удобства использования для Meeting Planner . Я очень взволнован его прогрессом и слышу хорошие отзывы об этом от друзей и коллег.
Сегодня я перехожу к этим двум урокам и провожу несколько дней в лесу без выходных. Отдых важен. Общение с природой помогает напомнить вам о том, что важно в жизни — стартапы не всегда, на самом деле. Они могут быть творческими занятиями, важными для нашего дохода и карьеры, они могут в некоторых случаях помочь людям жить более эффективно и продуктивно, но они часто далеки от земли, от дружеских отношений и от помощи другим менее удачливым. Это все, о чем я думаю каждый день и буду делать снова, пока меня нет.
Я постоянно спрашиваю себя, делаю ли я все, что хочу делать со своим временем, особенно в свете операции на мозге .
Я также приму к сведению тот факт, что я горжусь Meeting Planner, особенно работой на сегодняшний день и ее растущей полезностью. В целом, бета-версия сервиса близка к завершению.
Многочисленные собрания участников были самым сложным и сложным оставшимся рабочим вопросом. Заглядывая в будущее, функции и задачи являются более умеренными, небольшими и более легко управляемыми. Я взволнован о его будущих перспективах!
Если вы еще этого не сделали, запланируйте свою первую встречу с Meeting Planner прямо сейчас!
Учебное пособие по краудфандингу также находится в разработке, поэтому, пожалуйста, следуйте нашей странице WeFunder Meeting Planner .
Вы также можете обратиться ко мне @reifman . Я всегда открыт для новых идей и тематических предложений для будущих уроков.
Следите за всеми этими и другими учебниками, ознакомившись с серией « Построение стартапа на PHP» .