Commit a476d15c authored by Avris's avatar Avris

init

parents
Pipeline #964885 skipped
app/Config/parameters.yml
run/*
!run/.gitkeep
web/assetic/*
vendor/*
bin/*
tests/_output/*
.idea/*
**/.DS_Store
## Can I go home now? ##
Missing your cosy home? Come in to find out, if you can stop working already and head back home!
[Micrus Framework](http://micrus.avris.it) used.
\ No newline at end of file
pad = (n) -> if n < 10 then '0'+n else n
class GoHomeFavico
@lastBadge = false
@options = {}
constructor: (options) ->
@favico = new Favico(options)
update: (hoursLeft, minutesLeft) ->
[badge, options] = if hoursLeft then ["#{pad hoursLeft}:", {bgColor: '#d00'}] else [":#{if minutesLeft then minutesLeft else ')'}", {bgColor: '#0a4'}]
if badge != @lastBadge
@favico.badge badge, options
@lastBadge = badge
$fill = $('.fill')
resize = ->
$fill.css 'height', ''
$fill.each -> $(this).css 'height', Math.max($(this).height(), window.innerHeight, $(document).height())
resize()
$(window).resize(resize)
$('#settings-btn').click ->
$('.settings').toggleClass 'hidden-opac'
resize()
if @decisionUrl?
$timer = $('.timer')
@favicon = new GoHomeFavico({animation: 'fade'})
originalTitle = document.title
doCountdown = =>
if @secondsLeft <= 0
@secondsLeft = 0
clearInterval countdown
window.location.reload()
hoursLeft = Math.floor(@secondsLeft / 60 / 60)
minutesLeft = Math.floor(@secondsLeft / 60) % 60
secondsLeft = @secondsLeft % 60
timer = "#{pad hoursLeft}:#{pad minutesLeft}:#{pad secondsLeft}"
$timer.html timer
document.title = "[#{timer}] #{originalTitle}"
favicon.update hoursLeft, minutesLeft if window.useFavico
@secondsLeft--
countdown = setInterval doCountdown, 1000
doCountdown()
refresh = setInterval =>
$.post @decisionUrl, (data) => @secondsLeft = parseInt(data)
, 1000 * 60
$tz = $('#Settings_timezone')
if $tz.val() == 'UTC'
$tz.val(jstz.determine().name())
$mode = $('[name=Settings\\[mode\\]]')
updateMode = ->
mode = $mode.filter(':checked').val()
widgets = $('.form-control[data-showonly]')
widgets.removeAttr('required').parents('.form-group').addClass('hidden')
widgets.filter("[data-showonly=#{mode}]").attr('required', 'required').parents('.form-group').removeClass('hidden')
resize()
$mode.change updateMode
updateMode()
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
@import "mixins"
.bg, .box
+bgfull
background-attachment: fixed
$bgs: 'start' 'happy' 'sad'
@each $bg in $bgs
.bg-#{$bg}
background-image: url('gfx/bg-#{$bg}.jpg')
.box
+lightenBackground(0.4, 'gfx/bg-#{$bg}-blur.jpg')
main
width: 90%
max-width: 700px
margin: 0 auto
padding-top: 48px
text-align: center
.box
text-align: left
margin-bottom: 10px
border: 1px solid rgba(255, 255, 255, 0.3)
min-height: 20px
padding: 19px
border-radius: 4px
+box-shadow(5px 15px 30px rgba(0, 0, 0, 0.8))
text-shadow: 0 0 8px rgba(255, 255, 255, 0.8)
.settings
margin-top: 48px
margin-bottom: 36px
h3
margin: 10px 0 20px 0
+transition(opacity 0.4s ease-in-out)
.form-label
text-align: right
padding: 8px 15px
margin: 0
.checkbox, .radio
margin: 0
footer
margin-bottom: 0 !important
font-size: 0.8em
a
color: #000
.submit-container
padding-top: 12px
.answer
font-size: 1.3em
.flash
text-align: center
font-size: 1.3em
.hidden-opac
opacity: 0
=vertical-align
position: relative
top: 50%
-webkit-transform: translateY(-50%)
-ms-transform: translateY(-50%)
transform: translateY(-50%)
=transition ($options)
-webkit-transition: $options
-moz-transition: $options
-o-transition: $options
transition: $options
=translate($x, $y)
-webkit-transform: translate($x, $y)
-moz-transform: translate($x, $y)
-o-transform: translate($x, $y)
-ms-transform: translate($x, $y)
transform: translate($x, $y)
=cover
-webkit-background-size: cover
-moz-background-size: cover
-o-background-size: cover
background-size: cover
=bgfull
background-color: #fff
background-position: 50% 50%
background-repeat: no-repeat
+cover
=box
box-sizing: border-box
-moz-box-sizing: border-box
-webkit-box-sizing: border-box
=gradientVertical($col1, $col2, $col3, $percent)
background-image: -webkit-linear-gradient($col1, $col2 $percent, $col3)
background-image: -o-linear-gradient($col1, $col2 $percent, $col3)
background-image: -webkit-gradient(linear, left top, left bottom, from($col1), color-stop($percent, $col2), to($col3))
background-image: linear-gradient($col1, $col2 $percent, $col3)
background-repeat: no-repeat
=transform($transformation)
-ms-transform: $transformation
-webkit-transform: $transformation
transform: $transformation
=box-shadow($options)
-webkit-box-shadow: $options
box-shadow: $options
=lightenBackground($opacity, $url)
background-image: -webkit-linear-gradient(0deg, rgba(255, 255, 255, $opacity), rgba(255, 255, 255, $opacity)), url($url)
background-image: -o-linear-gradient(0deg, rgba(255, 255, 255, $opacity), rgba(255, 255, 255, $opacity)), url($url)
background-image: linear-gradient(0deg, rgba(255, 255, 255, $opacity), rgba(255, 255, 255, $opacity)), url($url)
filters:
sass: Sass\SassFilter
coffee: CoffeeScriptFilter
uglifyCss: UglifyCssFilter
uglifyJs: UglifyJsFilter
assets:
css:
inputs:
- lib/bootstrap.min.css
- lib/font-awesome.min.css
- [ sass/*, sass ]
filters: ?uglifyCss
js:
inputs:
- lib/jquery-2.1.3.min.js
- lib/bootstrap.min.js
- lib/favico.js
- lib/jstz.min.js
- [ coffee/* , coffee ]
filters: ?uglifyJs
statics:
- font
- gfx
- sfx
imports: [parameters, modules, assetic]
locale: en_UK
services: []
security: []
routing:
home:
pattern: /{whatever}
controller: Home/home
defaults: { whatever: '' }
unrestricted: true
- Avris\Micrus\Twig\TwigModule
- Avris\Micrus\Assetic\AsseticModule
<?php
namespace App\Controller;
use App\Form\SettingsForm;
use App\Model\Decision;
use App\Model\Settings;
use Avris\Micrus\Controller\Controller;
use Avris\Micrus\Controller\Http\Response;
use Guzzle\Http\Exception\ClientErrorResponseException;
class HomeController extends Controller
{
public function homeAction()
{
if ($this->getQuery('clear')) {
$this->setSettings(new Settings());
return $this->redirectToRoute('home');
}
$form = new SettingsForm($settings = $this->getSettings());
$form->bind($this->getData());
if ($form->isValid()) {
$settings->confirmed = true;
$this->setSettings($settings);
}
try {
$decision = $settings->confirmed ? new Decision($settings) : null;
} catch (ClientErrorResponseException $e) {
$this->addFlash('error', 'Specified Toggl API Key is not valid...');
$settings->confirmed = false;
$this->setSettings($settings);
return $this->redirectToRoute('home');
}
if ($this->getRequest()->isAjax()) {
return new Response($decision->secondsLeft);
}
return $this->render([
'_view' => 'index.html.twig',
'form' => $form,
'settings' => $settings,
'decision' => $decision,
]);
}
protected function getSettings()
{
return $this->getSession('settings') ?: unserialize($this->getCookies('settings')) ?: new Settings();
}
protected function setSettings(Settings $settings)
{
$this->getSession()->set('settings', $settings);
$this->getCookies()->set('settings', serialize($settings), time() + 60*60*24*30);
return $settings;
}
}
<?php
namespace App\Form;
use App\Model\Settings;
use Avris\Micrus\Model\Form;
use Avris\Micrus\Model\Assert as Assert;
use Avris\Micrus\Model\Widget\ChoiceHelper;
use Avris\Micrus\View\FormStyle\BootstrapHalf;
class SettingsForm extends Form
{
public function configure()
{
$this
->setStyle(new BootstrapHalf())
->add('mode', 'ButtonChoice', [
'label' => '',
'choices' => Settings::$modes,
'multiple' => false,
], new Assert\NotBlank())
->add('start', 'Time', [
'label' => 'What time did you start working today?',
'attr' => ['data-showonly' => Settings::MODE_CLOCK],
], new Assert\NotBlank())
->add('hours', 'NumberAddon', [
'label' => 'How long should you work today?',
'after' => 'hours',
], [
new Assert\NotBlank(),
new Assert\Min(0),
new Assert\Max(16),
])
->add('togglKey', 'Text', [
'label' => 'Toggl API token',
'attr' => ['data-showonly' => Settings::MODE_TOGGL],
])
->add('break', 'Time', [
'label' => 'How long a break have you taken already?',
'attr' => ['data-showonly' => Settings::MODE_CLOCK],
], new Assert\NotBlank())
->add('timezone', 'Choice', [
'label' => 'Timezone',
'choices' => ChoiceHelper::$timezone,
'attr' => ['data-showonly' => Settings::MODE_CLOCK],
])
->add('sound', 'Checkbox', [
'label' => '',
'sublabel' => '<span class="fa fa-volume-down fa-fw"></span> Play sounds',
])
->add('favicon', 'Checkbox', [
'label' => '',
'sublabel' => '<span class="fa fa-clock-o fa-fw"></span> Show countdown in the favicon',
])
;
}
}
<?php
namespace App\Model;
use AJT\Toggl\TogglClient;
use ICanBoogie\DateTime;
class Decision
{
public $secondsLeft;
public function __construct(Settings $settings)
{
$secondsDone = call_user_func([$this, $settings->mode . 'CountSeconds'], $settings);
$this->secondsLeft = ($settings->hours * 60 * 60) - ($secondsDone > 0 ? $secondsDone : 0);
}
public function canGoHome()
{
return $this->secondsLeft <= 0;
}
public function getTimeLeft()
{
return $this->canGoHome()
? sprintf(
'%s:%s:%s',
str_pad(floor($this->secondsLeft / 60 / 60), 2, '0', STR_PAD_LEFT),
str_pad(floor($this->secondsLeft / 60) % 60, 2, '0', STR_PAD_LEFT),
str_pad($this->secondsLeft % 60, 2, '0', STR_PAD_LEFT)
) : null;
}
protected function clockCountSeconds(Settings $settings)
{
date_default_timezone_set($settings->timezone);
return $this->diff(DateTime::now(), DateTime::from('today ' . $settings->start))
- $settings->getBreakInSeconds();
}
protected function togglCountSeconds(Settings $settings)
{
$togglClient = TogglClient::factory(['api_key' => $settings->togglKey]);
$user = $togglClient->getCurrentUser();
date_default_timezone_set($user['timezone']);
$todayEntries = $togglClient->getTimeEntries([
'start_date' => DateTime::from('00:00:00')->format('c'),
'end_date' => DateTime::from('23:59:59')->format('c'),
]);
$seconds = 0;
foreach ($todayEntries as $entry) {
$seconds += isset($entry['stop'])
? $entry['duration']
: $this->diff(DateTime::now(), DateTime::from($entry['start'])->local);
}
return $seconds;
}
protected function diff(DateTime $date1, DateTime $date2)
{
$diff = $date1->diff($date2);
return $diff->invert ? ($diff->s + $diff->i * 60 + $diff->h * 60 * 60) : 0;
}
}
<?php
namespace App\Model;
class Settings
{
const MODE_CLOCK = 'clock';
const MODE_TOGGL = 'toggl';
public static $modes = [
self::MODE_CLOCK => 'Standard',
self::MODE_TOGGL => 'With Toggl',
];
public $mode = self::MODE_CLOCK;
public $confirmed = false;
public $start = '09:00';
public $hours = 8;
public $togglKey;
public $sound = false;
public $favicon = true;
private $break = '00:00';
private $breakSetAt;
public $timezone = 'UTC';
public function getBreak()
{
if ($this->mode === self::MODE_CLOCK) {
return $this->breakSetAt == date('Y-m-d') ? $this->break : '00:00';
}
return '00:00';
}
public function getBreakInSeconds()
{
return preg_match('#(\d\d):(\d\d)#', $this->getBreak(), $matches)
? $matches[1] * 60 * 60 + $matches[2] * 60
: 0;
}
public function setBreak($break)
{
$this->break = $break;
$this->breakSetAt = date('Y-m-d');
return $this;
}
}
<!DOCTYPE html>
<html lang="{{ app.locale|slice(0,2) }}">
<head>
<meta charset="utf-8">
<title>{{ decision and decision.canGoHome ? 'Yes, you can!' : 'Can I go home now?'}}</title>
<meta name="description" content="Missing your cosy home? Come in to find out, if you can stop working already and head back home!">
<meta name="keywords" content="go,home,work,countdown,job,sad,happy">
<link rel="icon" href="{{ asset('assetic/gfx/favicon'~(decision and decision.canGoHome ? '-happy' : '')~'.ico') }}" />
<link rel="stylesheet" href="{{ asset(assets.css) }}">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:image" content="http://canigohome.info/assetic/gfx/bg-start.jpg" />
</head>
<body>
<div class="fill bg {{ decision ? (decision.canGoHome ? 'bg-happy' : 'bg-sad') : 'bg-start' }}">
<main>
{% for flash in app.flashBag %}
<div class="box flash">
<span class="fa fa-exclamation-triangle"></span> {{ flash.message }}
</div>
{% endfor %}
{% if decision %}
<div class="answer box">
<button id="settings-btn" class="btn btn-default btn-sm pull-right">
<span class="fa fa-cogs"></span>
</button>
{% if decision.canGoHome %}
<strong>Yup! You can go home now! :-)</strong>
{% else %}
Nope -_-' Sit on your f**in ass for
<strong class="timer">{{ decision.timeLeft }}</strong>
more, you cunning deadbeat.
<script>
window.decisionUrl = "{{ route('home') }}";
window.secondsLeft = {{ decision.secondsLeft }};
window.useFavico = {{ settings.favicon }};
</script>
{% endif %}
{% if settings.sound %}
<audio autoplay>
<source src="{{ asset('assetic/sfx/'~(decision.canGoHome ? 'happy' : 'sad')~'.mp3') }}" type="audio/mpeg">
</audio>
{% endif %}
</div>
{% endif %}
<form method="post" class="settings box form {% if decision %} hidden-opac{% endif %}">
{% if not decision %}
<h3 class="text-center">Can I go home now?</h3>
{% endif %}
{{ form|raw }}
<footer class="form-group">
<div class="row">
<div class="col-sm-6">
<span class="fa fa-photo"></span>
Authors of photos:
<a href="https://www.flickr.com/photos/benmciver/6180902486" target="_blank">BMclvr</a>,
<a href="https://www.flickr.com/photos/bs/3606756730" target="_blank">Britt Selvitelle</a>,
<a href="https://www.flickr.com/photos/cogdog/15171381719" target="_blank">Alan Levine</a>.
<br/>
<span class="fa fa-git-square"></span>
<a href="https://gitlab.com/Avris/GoHome" target="_blank">Source code</a>.
<br/>
<span class="fa fa-exclamation-circle"></span>
This app stores your settings for a month using <strong>cookies</strong>.
<br/>
You can disable them in your browser using
<a href="http://www.allaboutcookies.org/manage-cookies/" target="_blank">this instruction</a>.
</div>
<div class="col-sm-6 text-center submit-container">
<button type="submit" class="btn btn-default">
<strong>So? Can I go home?</strong>
</button>
</div>
</div>
</footer>
</form>
</main>
</div>
<script src="{{ asset(assets.js) }}"></script>
</body>
</html>
{
"name": "avris/gohome",
"type": "project",
"description": "Missing your cosy home? Come in to find out, if you can stop working already and head back home!",
"keywords": ["framework","full-stack","mvc","micrus"],
"license": "CC-BY",
"homepage": "http://canigohome.info/",
"authors": [
{
"name": "avris",
"email": "andrzej@avris.it",
"homepage": "http://avris.it"
},
{
"name": "japcok"
}
],
"require": {
"avris/micrus": "^2.0",
"avris/micrus-twig": "^2.0",
"avris/micrus-assetic": "^2.0",
"icanboogie/datetime": "^1.1",
"ajt/guzzle-toggl": "0.10.*"
},
"autoload": {
"psr-4": { "App\\": ["app"] }
},
"config": {
"bin-dir": "bin"
},
"scripts": {
"post-install-cmd": [
"php bin/micrus config:parameters",
"php bin/micrus cache:clear -a"
],
"post-update-cmd": [
"php bin/micrus config:parameters",
"php bin/micrus cache:clear -a"
]
}
}
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
stderr="true"
bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="unit">
<directory>tests/unit</directory>
</testsuite>
<testsuite name="functional">
<directory>tests/functional</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">app</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/_output/coverage"/>
<log type="coverage-clover" target="tests/_output/coverage.xml"/>
<log type="coverage-php" target="tests/_output/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showOnlySummary="true"/>
</logging>
</phpunit>
DirectoryIndex app.php
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .? - [L]
RewriteRule .? %{ENV:BASE}/app.php [L]
</IfModule>
<IfModule !mod_rewrite.c>
<IfModule mod_alias.c>
RedirectMatch 302 ^/$ /app.php/
</IfModule>
</IfModule>