1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 12:52:42 +01:00

Countdown

Summary:
Addon that allows you to create a live countdown page to
some event.

Here is the ticket that this code is based on
https://secure.phabricator.com/T36

Test Plan:
Tested by manually setting dates in the timer.js file and
checking if they made sense.
I'm not sure if it works across different timezones though.

Reviewers: epriestley

CC:

Differential Revision: 436
This commit is contained in:
Hafsteinn Baldvinsson 2011-06-12 23:06:17 +00:00
parent 1c5b31d397
commit a11b5e8bbc
20 changed files with 738 additions and 10 deletions

View file

@ -0,0 +1,16 @@
CREATE DATABASE phabricator_countdown;
CREATE TABLE phabricator_countdown.countdown_timer (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
authorPHID VARCHAR(64) BINARY NOT NULL,
datepoint INT UNSIGNED NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL
);
INSERT INTO phabricator_directory.directory_item
(name, description, href, categoryID, sequence, dateCreated, dateModified)
VALUES
("Counterdown", "T-minus X to Y", "/countdown/", 5, 350,
UNIX_TIMESTAMP(), UNIX_TIMESTAMP());

View file

@ -161,6 +161,16 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/css/application/differential/core.css',
),
0 =>
array(
'uri' => '/res/39de799e/rsrc/js/javelin/docs/Base.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-install',
),
'disk' => '/rsrc/js/javelin/docs/Base.js',
),
'differential-inline-comment-editor' =>
array(
'uri' => '/res/5e4f0aa4/rsrc/js/application/differential/DifferentialInlineCommentEditor.js',
@ -293,16 +303,6 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/javelin/lib/behavior.js',
),
0 =>
array(
'uri' => '/res/39de799e/rsrc/js/javelin/docs/Base.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-install',
),
'disk' => '/rsrc/js/javelin/docs/Base.js',
),
'javelin-behavior-aphront-basic-tokenizer' =>
array(
'uri' => '/res/bce3961b/rsrc/js/application/core/behavior-tokenizer.js',
@ -342,6 +342,18 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/behavior-form.js',
),
'javelin-behavior-countdown-timer' =>
array(
'uri' => '/res/48477cc8/rsrc/js/application/countdown/timer.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
),
'disk' => '/rsrc/js/application/countdown/timer.js',
),
'javelin-behavior-dark-console' =>
array(
'uri' => '/res/c80156c4/rsrc/js/application/core/behavior-dark-console.js',
@ -941,6 +953,15 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/css/core/core.css',
),
'phabricator-countdown-css' =>
array(
'uri' => '/res/7fd1e21f/rsrc/css/application/countdown/timer.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/countdown/timer.css',
),
'phabricator-directory-css' =>
array(
'uri' => '/res/a3d307c5/rsrc/css/application/directory/phabricator-directory.css',

View file

@ -284,6 +284,12 @@ phutil_register_library_map(array(
'PhabricatorConduitLogController' => 'applications/conduit/controller/log',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog',
'PhabricatorController' => 'applications/base/controller/base',
'PhabricatorCountdownController' => 'applications/countdown/controller/base',
'PhabricatorCountdownDAO' => 'applications/countdown/storage/base',
'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/delete',
'PhabricatorCountdownEditController' => 'applications/countdown/controller/edit',
'PhabricatorCountdownListController' => 'applications/countdown/controller/list',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/view',
'PhabricatorDaemon' => 'infrastructure/daemon/base',
'PhabricatorDaemonCombinedLogController' => 'applications/daemon/controller/combined',
'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/console',
@ -487,6 +493,7 @@ phutil_register_library_map(array(
'PhabricatorTimelineEvent' => 'infrastructure/daemon/timeline/storage/event',
'PhabricatorTimelineEventData' => 'infrastructure/daemon/timeline/storage/eventdata',
'PhabricatorTimelineIterator' => 'infrastructure/daemon/timeline/cursor/iterator',
'PhabricatorTimer' => 'applications/countdown/storage/timer',
'PhabricatorTransformedFile' => 'applications/files/storage/transformed',
'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common',
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
@ -747,6 +754,12 @@ phutil_register_library_map(array(
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO',
'PhabricatorController' => 'AphrontController',
'PhabricatorCountdownController' => 'PhabricatorController',
'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO',
'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController',
'PhabricatorCountdownEditController' => 'PhabricatorCountdownController',
'PhabricatorCountdownListController' => 'PhabricatorCountdownController',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorDaemon' => 'PhutilDaemon',
'PhabricatorDaemonCombinedLogController' => 'PhabricatorDaemonController',
'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
@ -925,6 +938,7 @@ phutil_register_library_map(array(
'PhabricatorTimelineDAO' => 'PhabricatorLiskDAO',
'PhabricatorTimelineEvent' => 'PhabricatorTimelineDAO',
'PhabricatorTimelineEventData' => 'PhabricatorTimelineDAO',
'PhabricatorTimer' => 'PhabricatorCountdownDAO',
'PhabricatorTransformedFile' => 'PhabricatorFileDAO',
'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',

View file

@ -315,6 +315,17 @@ class AphrontDefaultApplicationConfiguration
'/help/' => array(
'keyboardshortcut/$' => 'PhabricatorHelpKeyboardShortcutController',
),
'/countdown/' => array(
'$'
=> 'PhabricatorCountdownListController',
'(?P<id>\d+)/$'
=> 'PhabricatorCountdownViewController',
'edit/(?:(?P<id>\d+)/)?$'
=> 'PhabricatorCountdownEditController',
'delete/(?P<id>\d+)/$'
=> 'PhabricatorCountdownDeleteController'
),
);
}

View file

@ -0,0 +1,44 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorCountdownController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Countdown');
$page->setBaseURI('/countdown/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\xB2");
$page->setTabs(
array(
'create' => array(
'href' => '/countdown/',
'name' => 'List',
),
),
idx($data, 'tab'));
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/webpage');
phutil_require_module('phabricator', 'applications/base/controller/base');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorCountdownController.php');

View file

@ -0,0 +1,58 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorCountdownDeleteController
extends PhabricatorCountdownController {
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$timer = id(new PhabricatorTimer())->load($this->id);
if (!$timer) {
return new Aphront404Response();
}
if (($timer->getAuthorPHID() !== $user->getPHID())
&& $user->getIsAdmin() === false) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$timer->delete();
return id(new AphrontRedirectResponse())
->setURI('/countdown/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle('Really delete this countdown?');
$dialog->appendChild("Are you sure you want to delete this countdown?");
$dialog->addSubmitButton('Delete');
$dialog->addCancelButton('/countdown/');
$dialog->setSubmitURI($request->getPath());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/dialog');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/countdown/controller/base');
phutil_require_module('phabricator', 'applications/countdown/storage/timer');
phutil_require_module('phabricator', 'view/dialog');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorCountdownDeleteController.php');

View file

@ -0,0 +1,121 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorCountdownEditController
extends PhabricatorCountdownController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$action_label = 'Create Timer';
if ($this->id) {
$timer = id(new PhabricatorTimer())->load($this->id);
// If no timer is found
if (!$timer) {
return new Aphront404Response();
}
if (($timer->getAuthorPHID() != $user->getPHID())
&& $user->getIsAdmin() == false) {
return new Aphront404Response();
}
$action_label = 'Update Timer';
} else {
$timer = new PhabricatorTimer();
}
$error_view = null;
$e_text = null;
if ($request->isFormPost()) {
$errors = array();
$title = $request->getStr('title');
$datepoint = $request->getStr('datepoint');
$timestamp = strtotime($datepoint);
$e_text = null;
if (!strlen($title)) {
$e_text = 'Required';
$errors[] = 'You must give it a name';
}
if ($timestamp === false) {
$errors[] = 'You entered an incorrect date. You can enter date like'.
' \'2011-06-26 13:33:37\' to create an event at'.
' 13:33:37 on the 26th of June 2011';
}
$timer->setTitle($title);
$timer->setDatePoint($timestamp);
if (!count($errors)) {
$timer->setAuthorPHID($user->getPHID());
$timer->save();
return id(new AphrontRedirectResponse())
->setURI('/countdown/'.$timer->getID());
}
else {
$error_view = id(new AphrontErrorView())
->setErrors($errors)
->setTitle('It\'s not The Final Countdown (du nu nuuu nun)' .
' until you fix these problem');
}
}
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getRequestURI()->getPath())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setValue($timer->getTitle())
->setName('title'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('End date')
->setValue(strftime("%F %H:%M:%S", $timer->getDatePoint()))
->setName('datepoint')
->setCaption('Post any date that is parsable by strtotime'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/countdown/')
->setValue($action_label));
$panel = id(new AphrontPanelView())
->setWidth(AphrontPanelView::WIDTH_FORM)
->setHeader($action_label)
->appendChild($form);
return $this->buildStandardPageResponse(array(
$error_view,
$panel
),
array(
'title' => 'Countdown management',
'tab' => 'management',
));
}
}

View file

@ -0,0 +1,22 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/countdown/controller/base');
phutil_require_module('phabricator', 'applications/countdown/storage/timer');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/text');
phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorCountdownEditController.php');

View file

@ -0,0 +1,109 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorCountdownListController
extends PhabricatorCountdownController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setURI($request->getRequestURI(), 'page');
$timers = id(new PhabricatorTimer())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$timers = $pager->sliceResults($timers);
$rows = array();
foreach ($timers as $timer) {
$control_buttons = array();
$control_buttons[] = phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => '/countdown/'.$timer->getID().'/',
),
'View');
if ($user->getIsAdmin() ||
($user->getPHID() == $timer->getAuthorPHID())) {
$control_buttons[] = phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => '/countdown/edit/'.$timer->getID().'/'
),
'Edit');
$control_buttons[] = javelin_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => '/countdown/delete/'.$timer->getID().'/',
'sigil' => 'workflow'
),
'Delete');
}
$rows[] = array(
phutil_escape_html($timer->getID()),
phutil_escape_html($timer->getTitle()),
phabricator_format_timestamp($timer->getDatepoint()),
implode('', $control_buttons)
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'ID',
'Title',
'End Date',
'Action'
));
$table->setColumnClasses(
array(
null,
null,
null,
'action'
));
$panel = id(new AphrontPanelView())
->appendChild($table)
->setHeader('Timers')
->setCreateButton('Create Timer', '/countdown/edit/')
->appendChild($pager);
return $this->buildStandardPageResponse($panel,
array(
'title' => 'Countdown',
'tab' => 'countdown',
));
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/countdown/controller/base');
phutil_require_module('phabricator', 'applications/countdown/storage/timer');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/control/pager');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/utils');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorCountdownListController.php');

View file

@ -0,0 +1,75 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorCountdownViewController
extends PhabricatorCountdownController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$timer = id(new PhabricatorTimer())->load($this->id);
if (!$timer) {
return new Aphront404Response();
}
require_celerity_resource('phabricator-countdown-css');
$content =
'<div class="phabricator-timer">
<h1 class="phabricator-timer-header">'.
phutil_escape_html($timer->getTitle()).'
</h1>
<div class="phabricator-timer-pane">
<table class="phabricator-timer-table">
<tr>
<th>Days</th>
<th>Hours</th>
<th>Minutes</th>
<th>Seconds</th>
</tr>
<tr>
<td id="phabricator-timer-days"></td>
<td id="phabricator-timer-hours"></td>
<td id="phabricator-timer-minutes"></td>
<td id="phabricator-timer-seconds"></td>
</table>
</div>
</div>';
Javelin::initBehavior('countdown-timer', array(
'timestamp' => $timer->getDatepoint()
));
$panel = $content;
return $this->buildStandardPageResponse($panel,
array(
'title' => 'T-minus..',
'tab' => 'view',
));
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'applications/countdown/controller/base');
phutil_require_module('phabricator', 'applications/countdown/storage/timer');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorCountdownViewController.php');

View file

@ -0,0 +1,25 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorCountdownDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'countdown';
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/base/storage/lisk');
phutil_require_source('PhabricatorCountdownDAO.php');

View file

@ -0,0 +1,26 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorTimer extends PhabricatorCountdownDAO {
protected $id;
protected $title;
protected $authorPHID;
protected $datepoint;
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/countdown/storage/base');
phutil_require_source('PhabricatorTimer.php');

View file

@ -0,0 +1,40 @@
/**
* @provides phabricator-countdown-css
*/
.phabricator-timer {
width: 80%;
margin: 0 auto;
margin-top: 2em;
margin-bottom: 2em;
background: #efefef;
border-top: 1px solid #cccccc;
}
.phabricator-timer-header {
padding: 8px;
background: #dfdfdf;
}
.phabricator-timer-pane {
padding: 8px 2em;
}
.phabricator-timer-table {
width: 100%;
}
.phabricator-timer-table th {
font-weight: bold;
text-align: center;
color: #666666;
width: 10%;
padding: 4px;
}
.phabricator-timer-table td {
padding: 4px;
text-align: center;
font-size: 4em;
}

View file

@ -0,0 +1,48 @@
/**
* @provides javelin-behavior-countdown-timer
* @requires javelin-behavior
* javelin-dom
* javelin-util
*/
JX.behavior('countdown-timer', function(config) {
calculateTimeLeft();
function calculateTimeLeft() {
var days = 0;
var hours = 0;
var minutes = 0;
var seconds = 0;
var current_timestamp = Math.round(new Date() / 1000);
var delta = config.timestamp - current_timestamp;
if (delta <= 0) {
JX.DOM.setContent(JX.$('phabricator-timer-days'), days);
JX.DOM.setContent(JX.$('phabricator-timer-hours'), hours);
JX.DOM.setContent(JX.$('phabricator-timer-minutes'), minutes);
JX.DOM.setContent(JX.$('phabricator-timer-seconds'), seconds);
return;
}
days = Math.floor(delta/86400);
delta -= days * 86400;
hours = Math.floor(delta/3600);
delta -= hours * 3600;
minutes = Math.floor(delta / 60);
delta -= minutes * 60;
seconds = delta;
JX.DOM.setContent(JX.$('phabricator-timer-days'), days);
JX.DOM.setContent(JX.$('phabricator-timer-hours'), hours);
JX.DOM.setContent(JX.$('phabricator-timer-minutes'), minutes);
JX.DOM.setContent(JX.$('phabricator-timer-seconds'), seconds);
JX.defer(calculateTimeLeft, 1000);
}
});