├── .gitignore ├── ui ├── img │ ├── bg.jpg │ ├── icon-back.png │ ├── icon-open.png │ ├── logo-big.png │ ├── logo-small.png │ ├── icon-closed.png │ ├── icon-logout.png │ ├── icon-comments.png │ ├── icon-open-green.png │ └── icon-closed-green.png └── sass │ ├── main.scss │ └── pages │ ├── _page_login.scss │ ├── _page_error.scss │ ├── _page_issue.scss │ └── _page_issues.scss ├── assets ├── entry.jpg ├── list.jpg ├── login.jpg └── design.psd ├── src ├── middleware.php ├── Exception │ ├── ApiFail.php │ └── AuthorizationFail.php ├── Helpers │ ├── PagerInfo.php │ ├── NotFoundHandler.php │ ├── ErrorHandler.php │ ├── ResponseCache.php │ └── GitHubApi.php ├── settings.php ├── dependencies.php └── routes.php ├── .gitattributes ├── ide-twig.json ├── templates ├── base.html.twig ├── index.html.twig ├── error.html.twig ├── 404.html.twig ├── issues │ ├── pagination.html.twig │ └── list.html.twig └── issue.html.twig ├── public ├── .htaccess └── index.php ├── composer.json ├── gulpfile.js ├── package.json ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | .sync 4 | public/dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /ui/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/bg.jpg -------------------------------------------------------------------------------- /assets/entry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/assets/entry.jpg -------------------------------------------------------------------------------- /assets/list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/assets/list.jpg -------------------------------------------------------------------------------- /assets/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/assets/login.jpg -------------------------------------------------------------------------------- /src/middleware.php: -------------------------------------------------------------------------------- 1 | add(new \Slim\Csrf\Guard); 4 | -------------------------------------------------------------------------------- /ui/img/icon-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-back.png -------------------------------------------------------------------------------- /ui/img/icon-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-open.png -------------------------------------------------------------------------------- /ui/img/logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/logo-big.png -------------------------------------------------------------------------------- /ui/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/logo-small.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zip filter=lfs diff=lfs merge=lfs -text 2 | *.psd filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /ui/img/icon-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-closed.png -------------------------------------------------------------------------------- /ui/img/icon-logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-logout.png -------------------------------------------------------------------------------- /ui/img/icon-comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-comments.png -------------------------------------------------------------------------------- /ui/img/icon-open-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-open-green.png -------------------------------------------------------------------------------- /ui/img/icon-closed-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tracker/fullstack-party/master/ui/img/icon-closed-green.png -------------------------------------------------------------------------------- /assets/design.psd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:363e229c3c88e2b6250efe23ed4b5e774bc9f887b3cedb99833322c93291b13f 3 | size 58978628 4 | -------------------------------------------------------------------------------- /ide-twig.json: -------------------------------------------------------------------------------- 1 | // Twig configuration for Symfony plugin in PHP Storm. 2 | { 3 | "namespaces": [ 4 | // Global 5 | { 6 | "path": "templates" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/Exception/ApiFail.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}Title{% endblock %} 4 | {% block stylesheets %} 5 | 6 | {% endblock %} 7 | 8 | 9 | {% block content %}{% endblock %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | # RewriteBase / 8 | 9 | RewriteCond %{REQUEST_FILENAME} !-f 10 | RewriteRule ^ index.php [QSA,L] 11 | -------------------------------------------------------------------------------- /templates/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %} Welcome {% endblock %} 4 | 5 | {% block bodyClasses %} page-login {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | Login With GitHub 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "^3.8", 4 | "twig/twig": "^2.3", 5 | "guzzlehttp/guzzle": "^6.2", 6 | "symfony/var-dumper": "^3.2", 7 | "twig/extensions": "^1.5" 8 | }, 9 | "autoload": { 10 | "psr-4": { 11 | "Helpers\\": "src/Helpers/", 12 | "Exception\\": "src/Exception" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/sass/main.scss: -------------------------------------------------------------------------------- 1 | $brand-success: #9fd533; 2 | @import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap"; 3 | 4 | @import "pages/page_login"; 5 | @import "pages/page_issues"; 6 | @import "pages/page_issue"; 7 | @import "pages/page_error"; 8 | 9 | html { 10 | height: 100%; 11 | } 12 | 13 | a { 14 | color: #9fd533; 15 | 16 | &:hover, &:focus { 17 | color: #9fd533; 18 | text-decoration: underline; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Helpers/PagerInfo.php: -------------------------------------------------------------------------------- 1 | pages = $pagesCount; 28 | $this->issues = $issuesCount; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sass = require('gulp-sass'); 3 | var concat = require('gulp-concat'); 4 | 5 | gulp.task('img', function() { 6 | gulp.src('ui/img/**/*') 7 | .pipe(gulp.dest('public/dist/img')); 8 | }); 9 | 10 | gulp.task('sass', function() { 11 | gulp.src('ui/sass/**/*.scss') 12 | .pipe(sass()) 13 | // .pipe(concat('main.css')) 14 | .pipe(gulp.dest('public/dist/css')); 15 | }); 16 | 17 | gulp.task('default', ['sass', 'img']) 18 | 19 | gulp.task('watch', function() { 20 | gulp.watch('ui/sass/**/*.scss', ['sass']); 21 | gulp.watch('ui/img/**/*', ['img']); 22 | }); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tesonet", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "dependencies": { 10 | "bootstrap-sass": "^3.3.7" 11 | }, 12 | "devDependencies": { 13 | "gulp": "^3.9.1", 14 | "gulp-concat": "^2.6.1", 15 | "gulp-sass": "^3.1.0" 16 | }, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:TracKer/fullstack-party.git" 23 | }, 24 | "author": "", 25 | "license": "ISC" 26 | } 27 | -------------------------------------------------------------------------------- /ui/sass/pages/_page_login.scss: -------------------------------------------------------------------------------- 1 | body.page-login { 2 | height: 100%; 3 | background-image: url('../img/bg.jpg'); 4 | background-position: center center; 5 | background-attachment: fixed; 6 | background-size: cover; 7 | 8 | .block-login { 9 | position: fixed; 10 | left: 50%; 11 | top: 50%; 12 | transform: translate(-50%, -50%); 13 | 14 | .logo { 15 | height: 64px; 16 | background-image: url('../img/logo-big.png'); 17 | background-position-x: right; 18 | width: 246px; 19 | margin: 0 auto 60px auto; 20 | } 21 | 22 | a { 23 | width: 370px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/settings.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'displayErrorDetails' => true, // set to false in production 5 | 'addContentLengthHeader' => false, // Allow the web server to send the content-length header 6 | 7 | // Twig settings. 8 | 'twig' => [ 9 | 'template_paths' => [ 10 | SITE_ROOT . '/templates/' 11 | ], 12 | ], 13 | 14 | // Guzzle settings. 15 | 'guzzle' => [ 16 | 'base_uri' => 'https://api.github.com/', 17 | ], 18 | 19 | // GitHub settings. 20 | 'github' => [ 21 | 'clientId' => '587b512a6831b5acae95', 22 | 'clientSecret' => '170da7bfe3a52a13d249c956fc0f9e921ea4d147', 23 | 'redirectUri' => 'http://tesonet-test.local/auth', 24 | 'issues_per_page' => 30, 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | run(); 27 | -------------------------------------------------------------------------------- /templates/error.html.twig: -------------------------------------------------------------------------------- 1 | {# @var error_text string #} 2 | {# @var logged_in bool #} 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block title %} {{ issue.title }} {% endblock %} 6 | 7 | {% block bodyClasses %} page-error {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 | 14 | {% if logged_in %} 15 | Logout 16 | {% endif %} 17 |
18 |
19 |
20 |
21 | 22 |
23 |

Error!

24 |
25 | {{ error_text }} 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /src/Helpers/NotFoundHandler.php: -------------------------------------------------------------------------------- 1 | container = $container; 19 | } 20 | 21 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response) { 22 | /** @var \Twig_Environment $twig */ 23 | $twig = $this->container['twig']; 24 | $renderedContent = $twig->render('404.html.twig', [ 25 | 'logged_in' => isset($_SESSION['token']), 26 | ]); 27 | 28 | return $this->container['response'] 29 | ->withStatus(404) 30 | ->withHeader('Content-Type', 'text/html') 31 | ->write($renderedContent); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/404.html.twig: -------------------------------------------------------------------------------- 1 | {# @var error_text string #} 2 | {# @var logged_in bool #} 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block title %} {{ issue.title }} {% endblock %} 6 | 7 | {% block bodyClasses %} page-error {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 | 14 | {% if logged_in %} 15 | Logout 16 | {% endif %} 17 |
18 |
19 |
20 |
21 | 22 |
23 |

Page Not Found

24 |
25 |

The page you are looking for could not be found. Check the address bar to ensure your URL is spelled correctly. If all else fails, you can visit our home page at the link below.

26 |

Visit the Home Page

27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Great task for Great Fullstack Developer 2 | 3 | If you found this task it means we are looking for you! 4 | 5 | > Note: To clone this repository you will need [GIT-LFS](https://git-lfs.github.com/) 6 | 7 | ## Few simple steps 8 | 9 | 1. Fork this repo 10 | 2. Do your best 11 | 3. Prepare pull request and let us know that you are done 12 | 13 | ## Few simple requirements 14 | 15 | - Design should be recreated as closely as possible. 16 | - Design must be responsive. Because we live in our smartphones and we will check with them for sure. 17 | - Use GitHub V3 REST API to receive data. [Docs here](https://developer.github.com/v3/) 18 | - Use popular PHP framework (SlimPHP, Lumen, Symfony, Laravel, Zend or any other) 19 | - Use AngularJS or ReactJS. 20 | - Use CSS preprocessor (SCSS preferred). 21 | - Browser support must be great. All modern browsers plus IE9 and above. 22 | - Use a Javascript task-runner. Gulp, Webpack or Grunt - it doesn't matter. 23 | - Do not commit the build, because we are building things on deployment. 24 | 25 | ## Few tips 26 | 27 | - Structure! WE LOVE STRUCTURE! 28 | - Maybe You have an idea how it should interact with users? Do it! Its on you! 29 | - Have fun! 30 | -------------------------------------------------------------------------------- /ui/sass/pages/_page_error.scss: -------------------------------------------------------------------------------- 1 | body.page-error { 2 | background-color: #F5F5F5; 3 | 4 | .header-row { 5 | background-color: #fff; 6 | border-bottom-style: solid; 7 | border-bottom-width: 1px; 8 | border-bottom-color: #E6E6E6; 9 | } 10 | 11 | .header { 12 | height: 110px; 13 | 14 | .logo { 15 | float: left; 16 | margin-top: 35px; 17 | margin-left: 50px; 18 | background-image: url('../img/logo-small.png'); 19 | background-repeat: no-repeat; 20 | background-position: center; 21 | width: 120px; 22 | height: 40px; 23 | } 24 | 25 | a { 26 | float: right; 27 | display: block; 28 | font-size: 16px; 29 | line-height: 18px; 30 | color: #313155; 31 | background-image: url('../img/icon-logout.png'); 32 | background-position: left center; 33 | background-repeat: no-repeat; 34 | padding-left: 30px; 35 | margin-top: 45px; 36 | margin-right: 56px; 37 | } 38 | } 39 | 40 | .content { 41 | .top-block { 42 | margin-top: 35px; 43 | 44 | h1 { 45 | margin: 10px; 46 | } 47 | 48 | .panel-body { 49 | padding: 30px; 50 | 51 | a { 52 | color: #a94442; 53 | 54 | &:hover, &:focus { 55 | color: #a94442; 56 | text-decoration: underline; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/dependencies.php: -------------------------------------------------------------------------------- 1 | getContainer(); 4 | 5 | // Twig service. 6 | $container['twig'] = function ($c) { 7 | $paths = $c->get('settings')['twig']['template_paths']; 8 | $loader = new \Twig_Loader_Filesystem($paths); 9 | 10 | $twig = new Twig_Environment($loader, array( 11 | 'cache' => new Twig_Cache_Filesystem('/tmp/twig'), 12 | 'autoescape' => FALSE, 13 | 'auto_reload' => TRUE, 14 | 'debug' => TRUE, 15 | )); 16 | 17 | // $twig->addExtension(new Twig_Extension_Debug()); 18 | $twig->addExtension(new Twig_Extensions_Extension_Date()); 19 | 20 | return $twig; 21 | }; 22 | 23 | // Guzzle Client service. 24 | $container['guzzle'] = function ($c) { 25 | return new GuzzleHttp\Client($c->get('settings')['guzzle']); 26 | }; 27 | 28 | // GitHub API service. 29 | $container['githubApi'] = function ($c) { 30 | return new \Helpers\GitHubApi( 31 | ['issues_per_page' => $c->get('settings')['github']['issues_per_page']], 32 | $c->get('guzzle'), 33 | new \Helpers\ResponseCache() 34 | ); 35 | }; 36 | 37 | // Error handlers. 38 | $container['errorHandler'] = function ($container) { 39 | return new \Helpers\ErrorHandler($container); 40 | }; 41 | 42 | $container['phpErrorHandler'] = function ($container) { 43 | return new \Helpers\ErrorHandler($container); 44 | }; 45 | 46 | // 404 handler. 47 | $container['notFoundHandler'] = function ($container) { 48 | return new \Helpers\NotFoundHandler($container); 49 | }; 50 | -------------------------------------------------------------------------------- /src/Helpers/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | container = $container; 17 | } 18 | 19 | public function __invoke($request, $response, $error) { 20 | if ($error instanceof \Exception) { 21 | $exception = $error; 22 | if ($exception instanceof RequestException) { 23 | if (!$exception->hasResponse()) { 24 | $errorText = nl2br($exception->getMessage()); 25 | } else { 26 | $data = json_decode($exception->getResponse()->getBody()->getContents(), true); 27 | if (!isset($data['message'])) { 28 | $errorText = nl2br($exception->getMessage()); 29 | } else { 30 | $errorText = nl2br($data['message']); 31 | if (isset($data['documentation_url'])) { 32 | $uri = $data['documentation_url']; 33 | $errorText .= "
Please see more info here: {$uri}."; 34 | } 35 | } 36 | } 37 | } else { 38 | $errorText = nl2br($exception->getMessage()); 39 | } 40 | } else { 41 | $errorText = nl2br($error); 42 | } 43 | 44 | /** @var \Twig_Environment $twig */ 45 | $twig = $this->container['twig']; 46 | $renderedContent = $twig->render('error.html.twig', [ 47 | 'error_text' => $errorText, 48 | 'logged_in' => isset($_SESSION['token']), 49 | ]); 50 | 51 | return $this->container['response'] 52 | ->withStatus(500) 53 | ->withHeader('Content-Type', 'text/html') 54 | ->write($renderedContent); 55 | } 56 | } -------------------------------------------------------------------------------- /templates/issues/pagination.html.twig: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% if lastPage > 1 %} 3 | 4 | {# the number of first and last pages to be displayed #} 5 | {% set extremePagesLimit = 3 %} 6 | 7 | {# the number of pages that are displayed around the active page #} 8 | {% set nearbyPagesLimit = 2 %} 9 | 10 | {% set showAlwaysFirstAndLast = true %} 11 | 12 | 51 | {% endif %} 52 | {% endspaceless %} 53 | -------------------------------------------------------------------------------- /src/Helpers/ResponseCache.php: -------------------------------------------------------------------------------- 1 | cache = &$_SESSION['response_cache']; 22 | $this->cleanExpired(); 23 | } 24 | 25 | /** 26 | * Tries to get Response object from cache. 27 | * 28 | * @param string $key 29 | * Cache key. 30 | * @return Response|null 31 | * Returns Response object if cached, or null otherwise. 32 | */ 33 | public function get(string $key) { 34 | if (!isset($this->cache[$key])) { 35 | return null; 36 | } 37 | 38 | $data = $this->cache[$key]; 39 | $response = new Response( 40 | $data['data']['status'], 41 | $data['data']['header'], 42 | $data['data']['body'], 43 | $data['data']['version'], 44 | $data['data']['reason'] 45 | ); 46 | 47 | return $response; 48 | } 49 | 50 | /** 51 | * Saves Response object to cache. 52 | * 53 | * @param string $key 54 | * Cache key. 55 | * @param Response $response 56 | * Response object. 57 | */ 58 | public function set(string $key, Response &$response) { 59 | $data = [ 60 | 'expire' => time() + 60*5, 61 | 'data' => [ 62 | 'status' => $response->getStatusCode(), 63 | 'header' => $response->getHeaders(), 64 | 'body' => $response->getBody()->getContents(), 65 | 'version' => $response->getProtocolVersion(), 66 | 'reason' => $response->getReasonPhrase(), 67 | ], 68 | ]; 69 | 70 | $this->cache[$key] = $data; 71 | $response = $this->get($key); 72 | } 73 | 74 | /** 75 | * Removes expired objects from cache. 76 | */ 77 | public function cleanExpired() { 78 | $now = time(); 79 | foreach ($this->cache as $key => $data) { 80 | if ($now > $data['expire']) { 81 | unset($this->cache[$key]); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /templates/issue.html.twig: -------------------------------------------------------------------------------- 1 | {# @var issue array #} 2 | {# @var referer string #} 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block title %} {{ issue.title }} {% endblock %} 6 | 7 | {% block bodyClasses %} page-issue {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 | 14 | Logout 15 |
16 |
17 |
18 |
19 | 20 | {% spaceless %} 21 | 24 | {% endspaceless %} 25 | 26 |
27 |
28 |

{{ issue.title }} #{{ issue.number }}

29 | {% if issue.state == 'open' %} 30 | OPEN 31 | {% else %} 32 | CLOSED 33 | {% endif %} 34 | 35 | 36 | {{ issue.user.login }} 37 | {% if issue.state == 'open' %} 38 | opened {{ issue.created_at|time_diff ? : 'just now' }} 39 | {% else %} 40 | closed {{ issue.closed_at|time_diff ? : 'just now' }} 41 | {% endif %} 42 | this issue · {{ issue.comments|length }} comments. 43 | 44 | 45 |
46 |
47 |
48 |
49 | 50 | {% for comment in issue.comments %} 51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 | {{ comment.user.login }} 59 | commented {{ comment.created_at|time_diff ? : 'just now' }} 60 |
61 |
62 | {{ comment.body|nl2br }} 63 |
64 |
65 |
66 |
67 | {% endfor %} 68 | 69 |
70 | {% endblock %} 71 | -------------------------------------------------------------------------------- /templates/issues/list.html.twig: -------------------------------------------------------------------------------- 1 | {# @var state string #} 2 | {# @var issues array #} 3 | {# @var currentPage int #} 4 | {# @var lastPage int #} 5 | {# @var openIssuesCount int #} 6 | {# @var closedIssuesCount int #} 7 | {% extends 'base.html.twig' %} 8 | 9 | {% block title %} {{ state == 'open' ? 'Open' : 'Closed' }} issues {% endblock %} 10 | 11 | {% block bodyClasses %} page-issues {% endblock %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 | 18 | Logout 19 |
20 |
21 |
22 |
23 | 24 | {% spaceless %} 25 | 29 | {% endspaceless %} 30 | 31 | {% for issue in issues %} 32 |
33 |
34 |
35 |
36 |
37 | {{ issue.title }} 38 | {% for label in issue.labels %} 39 | {{ label.name }} 43 | {% endfor %} 44 |
45 | #{{ issue.number }} 46 | 47 | {% if state == 'open' %} 48 | opened {{ issue.created_at|time_diff ? : 'just now' }} 49 | {% else %} 50 | closed {{ issue.closed_at|time_diff ? : 'just now' }} 51 | {% endif %} 52 | 53 | by {{ issue.user.login }} 54 |
55 |
56 | 59 |
60 |
61 |
62 |
63 | {% endfor %} 64 | 65 | {% include 'issues/pagination.html.twig' %} 66 |
67 |
68 |
69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /ui/sass/pages/_page_issue.scss: -------------------------------------------------------------------------------- 1 | body.page-issue { 2 | background-color: #F5F5F5; 3 | 4 | .header-row { 5 | background-color: #fff; 6 | border-bottom-style: solid; 7 | border-bottom-width: 1px; 8 | border-bottom-color: #E6E6E6; 9 | } 10 | 11 | .header { 12 | height: 110px; 13 | 14 | .logo { 15 | float: left; 16 | margin-top: 35px; 17 | margin-left: 50px; 18 | background-image: url('../img/logo-small.png'); 19 | background-repeat: no-repeat; 20 | background-position: center; 21 | width: 120px; 22 | height: 40px; 23 | } 24 | 25 | a { 26 | float: right; 27 | display: block; 28 | font-size: 16px; 29 | line-height: 18px; 30 | color: #313155; 31 | background-image: url('../img/icon-logout.png'); 32 | background-position: left center; 33 | background-repeat: no-repeat; 34 | padding-left: 30px; 35 | margin-top: 45px; 36 | margin-right: 56px; 37 | } 38 | } 39 | 40 | .content { 41 | .menu { 42 | text-align: left; 43 | margin: 30px 0; 44 | 45 | a { 46 | display: inline-block; 47 | font-size: 14px; 48 | line-height: 18px; 49 | color: #85b32b; 50 | 51 | &:hover, &:focus { 52 | color: #85b32b; 53 | text-decoration: underline; 54 | } 55 | 56 | &:before { 57 | content: ' '; 58 | display: block; 59 | float: left; 60 | width: 15px; 61 | height: 18px; 62 | margin-right: 8px; 63 | background-repeat: no-repeat; 64 | background-position: left center; 65 | background-image: url('../img/icon-back.png'); 66 | } 67 | } 68 | } 69 | 70 | .top-block .panel-body { 71 | padding: 30px; 72 | 73 | h1 { 74 | font-size: 30px; 75 | color: #333333; 76 | padding: 0; 77 | margin: 0 0 20px 0; 78 | font-weight: normal; 79 | 80 | .issue-id { 81 | color: #999999; 82 | } 83 | } 84 | 85 | .main-state { 86 | font-size: 21px; 87 | font-weight: normal; 88 | line-height: 40px; 89 | margin-right: 20px; 90 | } 91 | 92 | .info { 93 | font-size: 13px; 94 | color: #999999; 95 | line-height: 40px; 96 | } 97 | } 98 | } 99 | 100 | .comment { 101 | .avatar-block { 102 | text-align: right; 103 | padding-right: 20px; 104 | } 105 | 106 | .panel-heading { 107 | background-color: #fff; 108 | font-size: 13px; 109 | color: #999999; 110 | line-height: 40px; 111 | padding-left: 20px; 112 | padding-right: 20px; 113 | } 114 | 115 | .panel-body { 116 | padding: 30px; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ui/sass/pages/_page_issues.scss: -------------------------------------------------------------------------------- 1 | body.page-issues { 2 | background-color: #F5F5F5; 3 | 4 | .header-row { 5 | background-color: #fff; 6 | border-bottom-style: solid; 7 | border-bottom-width: 1px; 8 | border-bottom-color: #E6E6E6; 9 | } 10 | 11 | .header { 12 | height: 110px; 13 | 14 | .logo { 15 | float: left; 16 | margin-top: 35px; 17 | margin-left: 50px; 18 | background-image: url('../img/logo-small.png'); 19 | background-repeat: no-repeat; 20 | background-position: center; 21 | width: 120px; 22 | height: 40px; 23 | } 24 | 25 | a { 26 | float: right; 27 | display: block; 28 | font-size: 16px; 29 | line-height: 18px; 30 | color: #313155; 31 | background-image: url('../img/icon-logout.png'); 32 | background-position: left center; 33 | background-repeat: no-repeat; 34 | padding-left: 30px; 35 | margin-top: 45px; 36 | margin-right: 56px; 37 | } 38 | } 39 | 40 | .content { 41 | .menu { 42 | text-align: center; 43 | margin: 30px 0; 44 | 45 | a { 46 | display: inline-block; 47 | margin: 0 10px; 48 | font-size: 14px; 49 | line-height: 18px; 50 | color: #808080; 51 | 52 | &:hover, &.active { 53 | color: #000; 54 | text-decoration: none; 55 | } 56 | 57 | &:before { 58 | content: ' '; 59 | display: block; 60 | float: left; 61 | width: 16px; 62 | height: 16px; 63 | margin-right: 8px; 64 | opacity: 0.5; 65 | background-repeat: no-repeat; 66 | background-position: left center; 67 | } 68 | 69 | &:hover:before, &.active:before { 70 | opacity: 1; 71 | } 72 | 73 | &.open:before { 74 | background-image: url('../img/icon-open.png'); 75 | } 76 | &.closed:before { 77 | background-image: url('../img/icon-closed.png'); 78 | } 79 | } 80 | } 81 | 82 | .issue { 83 | .main-block { 84 | background-repeat: no-repeat; 85 | background-position: 30px 3px; 86 | padding-left: 60px; 87 | 88 | &.open { 89 | background-image: url('../img/icon-open-green.png'); 90 | } 91 | 92 | &.closed { 93 | background-image: url('../img/icon-closed-green.png'); 94 | } 95 | 96 | .info { 97 | font-size: 13px; 98 | color: #bfbfbf; 99 | margin-top: 10px; 100 | } 101 | } 102 | 103 | .comments-block { 104 | padding-top: 5px; 105 | } 106 | 107 | a.title { 108 | font-size: 15px; 109 | line-height: 22px; 110 | color: #333; 111 | } 112 | 113 | a.comments { 114 | font-size: 13px; 115 | color: #999; 116 | background-image: url('../img/icon-comments.png'); 117 | background-position: left top; 118 | background-repeat: no-repeat; 119 | line-height: 16px; 120 | padding-left: 23px; 121 | } 122 | 123 | .label { 124 | line-height: 22px; 125 | } 126 | } 127 | } 128 | 129 | .pagination { 130 | display: block; 131 | text-align: center; 132 | 133 | * { 134 | display: inline-block; 135 | height: 35px; 136 | line-height: 35px; 137 | text-align: center; 138 | margin: 0 4px; 139 | } 140 | 141 | span.disabled { 142 | color: #bbb; 143 | } 144 | 145 | a.num { 146 | width: 35px; 147 | color: #000; 148 | border-radius: 50%; 149 | background-color: #fff; 150 | box-shadow: 0px 3px 1px -2px rgba(209,209,209,1); 151 | 152 | &.active, &:hover { 153 | color: #fff; 154 | background-color: #9fd533; 155 | box-shadow: none; 156 | } 157 | 158 | &:hover, &:focus { 159 | text-decoration: none; 160 | } 161 | 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | get('/auth', function(ServerRequestInterface $request, ResponseInterface $response, $args) { 10 | if (isset($_SESSION['token'])) { 11 | return $response->withStatus(302)->withHeader('Location', '/issues/open'); 12 | } 13 | $query = $request->getQueryParams(); 14 | 15 | if (!isset($query['code'])) { 16 | // Redirect user to authorization page. 17 | $_SESSION['last_state'] = md5(time() . 31337); 18 | $requestQuery = [ 19 | 'state' => $_SESSION['last_state'], 20 | 'redirect_uri' => 'http://tesonet-test.local/auth', 21 | 'client_id' => $this->settings['github']['clientId'], 22 | ]; 23 | $uri = 'https://github.com/login/oauth/authorize?' . http_build_query($requestQuery); 24 | return $response->withStatus(302)->withHeader('Location', $uri); 25 | } 26 | elseif (!isset($query['state']) || ($query['state'] !== $_SESSION['last_state'])) { 27 | // If state is not the same as was sent to GitHub, the request must be aborted. 28 | unset($_SESSION['last_state']); 29 | throw new \Exception\AuthorizationFail('Invalid state received!'); 30 | } 31 | else { 32 | // Try to get an access token using the temporary code. 33 | $guzzle = new \GuzzleHttp\Client(['base_uri' => 'https://github.com/']); 34 | $response = $guzzle->post('login/oauth/access_token', [ 35 | 'headers' => [ 36 | 'Accept' => 'application/json', 37 | 'Content-Type' => 'application/json' 38 | ], 39 | 'body' => json_encode([ 40 | 'client_id' => $this->settings['github']['clientId'], 41 | 'client_secret' => $this->settings['github']['clientSecret'], 42 | 'code' => $query['code'], 43 | 'state' => $_SESSION['last_state'], 44 | ]) 45 | ]); 46 | $data = json_decode($response->getBody()->getContents(), true); 47 | 48 | // If data is empty or json parsing failed. 49 | if (!isset($data['access_token'])) { 50 | unset($_SESSION['last_state']); 51 | throw new \Exception\AuthorizationFail('Can not get access token from GitHub response!'); 52 | } 53 | 54 | unset($_SESSION['last_state']); 55 | $_SESSION['token'] = $data['access_token']; 56 | 57 | return $response->withStatus(302)->withHeader('Location', '/issues/open'); 58 | } 59 | }); 60 | 61 | // Front page. 62 | $app->get('/', function($request, $response, $args) { 63 | if (isset($_SESSION['token'])) { 64 | return $response->withStatus(302)->withHeader('Location', '/issues/open'); 65 | } 66 | 67 | /** @var Twig_Environment $twig */ 68 | $twig = $this->twig; 69 | return $twig->render('index.html.twig', $args); 70 | }); 71 | 72 | // Issues list. 73 | $app->get('/issues/{state}[/{page:[0-9]+}]', function(ServerRequestInterface $request, ResponseInterface $response, $args) { 74 | if (!isset($_SESSION['token'])) { 75 | return $response->withStatus(302)->withHeader('Location', '/'); 76 | } 77 | 78 | $state = $args['state']; 79 | $page = isset($args['page']) ? $args['page'] : 1; 80 | 81 | /** @var \Helpers\GitHubApi $githubApi */ 82 | $githubApi = $this->githubApi; 83 | $openPagerInfo = $githubApi->getPagerInfo('symfony/symfony', 'open', $_SESSION['token']); 84 | $closedPagerInfo = $githubApi->getPagerInfo('symfony/symfony', 'closed', $_SESSION['token']); 85 | $issues = $githubApi->getIssuesList('symfony/symfony', $state, $page, $_SESSION['token']); 86 | 87 | /** @var Twig_Environment $twig */ 88 | $twig = $this->twig; 89 | return $twig->render('issues/list.html.twig', [ 90 | 'state' => $state, 91 | 'issues' => $issues, 92 | 'currentPage' => $page, 93 | 'lastPage' => $state == 'open' ? $openPagerInfo->pages : $closedPagerInfo->pages, 94 | 'openIssuesCount' => $openPagerInfo->issues, 95 | 'closedIssuesCount' => $closedPagerInfo->issues, 96 | ]); 97 | }); 98 | 99 | // Issue. 100 | $app->get('/issue/{id}', function(ServerRequestInterface $request, ResponseInterface $response, $args) { 101 | if (!isset($_SESSION['token'])) { 102 | return $response->withStatus(302)->withHeader('Location', '/'); 103 | } 104 | 105 | /** @var \Helpers\GitHubApi $githubApi */ 106 | $githubApi = $this->githubApi; 107 | $issue = $githubApi->getIssue('symfony/symfony', $args['id'], $_SESSION['token']); 108 | 109 | if ($request->hasHeader('HTTP_REFERER')) { 110 | $referer = reset($request->getHeader('HTTP_REFERER')); 111 | } else { 112 | $referer = '/issues/' . $issue['state']; 113 | } 114 | 115 | /** @var Twig_Environment $twig */ 116 | $twig = $this->twig; 117 | return $twig->render('issue.html.twig', [ 118 | 'issue' => $issue, 119 | 'referer' => $referer, 120 | ]); 121 | }); 122 | 123 | $app->get('/logout', function(ServerRequestInterface $request, ResponseInterface $response, $args) { 124 | unset($_SESSION['token']); 125 | return $response->withStatus(302)->withHeader('Location', '/'); 126 | }); 127 | -------------------------------------------------------------------------------- /src/Helpers/GitHubApi.php: -------------------------------------------------------------------------------- 1 | client = $client; 25 | $this->cache = $cache; 26 | $this->issuesPerPage = isset($config['issues_per_page']) ? $config['issues_per_page'] : 30; 27 | } 28 | 29 | /** 30 | * Executes API request with caching. 31 | * 32 | * @param string $uri 33 | * Uri. 34 | * @param string[] $query 35 | * Query parameters. 36 | * @param string|null $token 37 | * Access token. 38 | * @return Response 39 | * Data. 40 | * @throws RequestException 41 | */ 42 | public function get(string $uri, array $query = [], string $token = null): Response { 43 | ksort($query); 44 | $queryString = http_build_query($query); 45 | $cacheKey = "{$uri}?$queryString"; 46 | 47 | $response = $this->cache->get($cacheKey); 48 | if ($response !== null) { 49 | return $response; 50 | } 51 | 52 | if (isset($token)) { 53 | $query['access_token'] = $token; 54 | } 55 | 56 | /** @var Response $response */ 57 | $response = $this->client->get($uri, ['query' => $query]); 58 | $this->cache->set($cacheKey, $response); 59 | return $response; 60 | } 61 | 62 | /** 63 | * Gets pager information. 64 | * 65 | * @param string $repo 66 | * Repository. 67 | * @param string $state 68 | * Issue state 'open' or 'closed'. 69 | * @param string|null $token 70 | * Access token. 71 | * @return PagerInfo 72 | * Object with information about issues count and pages count. 73 | * @throws RequestException 74 | * @throws ApiFail 75 | */ 76 | public function getPagerInfo(string $repo, string $state, string $token = null) { 77 | // Get first page. 78 | $response = $this->get("repos/{$repo}/issues", [ 79 | 'state' => $state, 80 | 'per_page' => $this->issuesPerPage, 81 | 'page' => 1, 82 | ], $token); 83 | $data = json_decode($response->getBody()->getContents(), true); 84 | 85 | // If data is empty of json parsing failed. 86 | if ((!isset($data)) || empty($data)) { 87 | return new PagerInfo(0, 0); 88 | } 89 | 90 | // *IPP = $this->issuesPerPage 91 | // If issues count on page less then IPP, then it's amount of all items, and there is only one page. 92 | // Or, if there is no Link header, the issues amount is the amount of issues on first page. 93 | if ((count($data) < $this->issuesPerPage) || (!$response->hasHeader('Link'))) { 94 | return new PagerInfo(1,count($data)); 95 | } 96 | 97 | preg_match('/<[^>]+[?&]page=(\d+)[^>]*>;\s*rel="last"/', reset($response->getHeader('Link')), $matches); 98 | if (!isset($matches[1])) { 99 | // Error. 100 | throw new ApiFail('Error occurred while parsing pagination data from response.'); 101 | } 102 | 103 | $pagesCount = $matches[1]; 104 | 105 | // Get last page. 106 | $response = $this->get("repos/{$repo}/issues", [ 107 | 'state' => $state, 108 | 'per_page' => $this->issuesPerPage, 109 | 'page' => $pagesCount, 110 | ], $token); 111 | $data = json_decode($response->getBody()->getContents(), true); 112 | 113 | if ((!isset($data)) || empty($data)) { 114 | // Error. 115 | throw new ApiFail('Error occurred while getting last page data.'); 116 | } 117 | 118 | $issuesCount = (($pagesCount - 1) * $this->issuesPerPage) + count($data); 119 | return new PagerInfo($pagesCount, $issuesCount); 120 | } 121 | 122 | /** 123 | * Gets issues list. 124 | * 125 | * @param string $repo 126 | * Repository. 127 | * @param string $state 128 | * Issue state 'open' or 'closed'. 129 | * @param int $page 130 | * Page number. 131 | * @param string|null $token 132 | * Access token. 133 | * @return array 134 | * List of issues. 135 | * @throws RequestException 136 | */ 137 | public function getIssuesList(string $repo, string $state, int $page = 1, string $token = null) { 138 | $response = $this->get("repos/{$repo}/issues", [ 139 | 'state' => $state, 140 | 'per_page' => $this->issuesPerPage, 141 | 'page' => $page, 142 | ], $token); 143 | $data = json_decode($response->getBody()->getContents(), true); 144 | 145 | // If data is empty of json parsing failed. 146 | if ((!isset($data)) || empty($data)) { 147 | return []; 148 | } 149 | 150 | foreach ($data as &$issue) { 151 | self::processIssue($issue); 152 | } 153 | 154 | return $data; 155 | } 156 | 157 | /** 158 | * Gets issue. 159 | * 160 | * @param string $repo 161 | * Repository. 162 | * @param string $id 163 | * Issue ID. 164 | * @param string|null $token 165 | * Access token. 166 | * @return array 167 | * Issue data. 168 | * @throws ApiFail 169 | */ 170 | public function getIssue(string $repo, string $id, string $token = null) { 171 | $response = $this->get("/repos/{$repo}/issues/{$id}", [], $token); 172 | $data = json_decode($response->getBody()->getContents(), true); 173 | 174 | if ((!isset($data)) || empty($data)) { 175 | // Error. 176 | throw new ApiFail('Error occurred while getting issue data.'); 177 | } 178 | 179 | self::processIssue($data); 180 | 181 | if ($data['comments'] > 0) { 182 | $response = $this->get("/repos/{$repo}/issues/{$id}/comments", [], $token); 183 | $comments = json_decode($response->getBody()->getContents(), true); 184 | 185 | if ((!isset($data)) || empty($data)) { 186 | $data['comments'] = []; 187 | } else { 188 | foreach ($comments as &$comment) { 189 | self::processComment($comment); 190 | } 191 | $data['comments'] = $comments; 192 | } 193 | } else { 194 | $data['comments'] = []; 195 | } 196 | 197 | return $data; 198 | } 199 | 200 | /** 201 | * Converts dates to time stamp, calculating text color on labels. 202 | * 203 | * @param array $issue 204 | * Issue data. 205 | */ 206 | private static function processIssue(&$issue) { 207 | $issue['created_at'] = strtotime($issue['created_at']); 208 | $issue['updated_at'] = strtotime($issue['updated_at']); 209 | if (isset($issue['closed_at'])) { 210 | $issue['closed_at'] = strtotime($issue['closed_at']); 211 | } 212 | 213 | foreach ($issue['labels'] as &$label) { 214 | $label['text_color'] = self::getContrastColor(ltrim($label['color'], '#')); 215 | } 216 | } 217 | 218 | /** 219 | * Converts dates to time stamp. 220 | * 221 | * @param array $comment 222 | * Comment data. 223 | */ 224 | private static function processComment(&$comment) { 225 | $comment['created_at'] = strtotime($comment['created_at']); 226 | $comment['updated_at'] = strtotime($comment['updated_at']); 227 | } 228 | 229 | /** 230 | * Calculates which text color is looking better on specific background color. 231 | * 232 | * @param $hexColor 233 | * Color. 234 | * @return string 235 | * Calculated color. 236 | */ 237 | private static function getContrastColor($hexColor) { 238 | if (strlen($hexColor) == 3) { 239 | $hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2]; 240 | } 241 | 242 | //////////// hexColor RGB 243 | $R1 = hexdec(substr($hexColor, 0, 2)); 244 | $G1 = hexdec(substr($hexColor, 2, 2)); 245 | $B1 = hexdec(substr($hexColor, 4, 2)); 246 | 247 | //////////// Black RGB 248 | $blackColor = "#000000"; 249 | $R2BlackColor = hexdec(substr($blackColor, 0, 2)); 250 | $G2BlackColor = hexdec(substr($blackColor, 2, 2)); 251 | $B2BlackColor = hexdec(substr($blackColor, 4, 2)); 252 | 253 | //////////// Calc contrast ratio 254 | $L1 = 0.2126 * pow($R1 / 255, 2.2) + 255 | 0.7152 * pow($G1 / 255, 2.2) + 256 | 0.0722 * pow($B1 / 255, 2.2); 257 | 258 | $L2 = 0.2126 * pow($R2BlackColor / 255, 2.2) + 259 | 0.7152 * pow($G2BlackColor / 255, 2.2) + 260 | 0.0722 * pow($B2BlackColor / 255, 2.2); 261 | 262 | $contrastRatio = 0; 263 | if ($L1 > $L2) { 264 | $contrastRatio = (int)(($L1 + 0.05) / ($L2 + 0.05)); 265 | } else { 266 | $contrastRatio = (int)(($L2 + 0.05) / ($L1 + 0.05)); 267 | } 268 | 269 | //////////// If contrast is more than 5, return black color 270 | if ($contrastRatio > 5) { 271 | return '000000'; 272 | } else { //////////// if not, return white color. 273 | return 'ffffff'; 274 | } 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "1d04a65795350915cce1b181b35e1dad", 8 | "packages": [ 9 | { 10 | "name": "container-interop/container-interop", 11 | "version": "1.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/container-interop/container-interop.git", 15 | "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", 20 | "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "psr/container": "^1.0" 25 | }, 26 | "type": "library", 27 | "autoload": { 28 | "psr-4": { 29 | "Interop\\Container\\": "src/Interop/Container/" 30 | } 31 | }, 32 | "notification-url": "https://packagist.org/downloads/", 33 | "license": [ 34 | "MIT" 35 | ], 36 | "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", 37 | "homepage": "https://github.com/container-interop/container-interop", 38 | "time": "2017-02-14T19:40:03+00:00" 39 | }, 40 | { 41 | "name": "guzzlehttp/guzzle", 42 | "version": "6.2.3", 43 | "source": { 44 | "type": "git", 45 | "url": "https://github.com/guzzle/guzzle.git", 46 | "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006" 47 | }, 48 | "dist": { 49 | "type": "zip", 50 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006", 51 | "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006", 52 | "shasum": "" 53 | }, 54 | "require": { 55 | "guzzlehttp/promises": "^1.0", 56 | "guzzlehttp/psr7": "^1.4", 57 | "php": ">=5.5" 58 | }, 59 | "require-dev": { 60 | "ext-curl": "*", 61 | "phpunit/phpunit": "^4.0", 62 | "psr/log": "^1.0" 63 | }, 64 | "type": "library", 65 | "extra": { 66 | "branch-alias": { 67 | "dev-master": "6.2-dev" 68 | } 69 | }, 70 | "autoload": { 71 | "files": [ 72 | "src/functions_include.php" 73 | ], 74 | "psr-4": { 75 | "GuzzleHttp\\": "src/" 76 | } 77 | }, 78 | "notification-url": "https://packagist.org/downloads/", 79 | "license": [ 80 | "MIT" 81 | ], 82 | "authors": [ 83 | { 84 | "name": "Michael Dowling", 85 | "email": "mtdowling@gmail.com", 86 | "homepage": "https://github.com/mtdowling" 87 | } 88 | ], 89 | "description": "Guzzle is a PHP HTTP client library", 90 | "homepage": "http://guzzlephp.org/", 91 | "keywords": [ 92 | "client", 93 | "curl", 94 | "framework", 95 | "http", 96 | "http client", 97 | "rest", 98 | "web service" 99 | ], 100 | "time": "2017-02-28T22:50:30+00:00" 101 | }, 102 | { 103 | "name": "guzzlehttp/promises", 104 | "version": "v1.3.1", 105 | "source": { 106 | "type": "git", 107 | "url": "https://github.com/guzzle/promises.git", 108 | "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" 109 | }, 110 | "dist": { 111 | "type": "zip", 112 | "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", 113 | "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", 114 | "shasum": "" 115 | }, 116 | "require": { 117 | "php": ">=5.5.0" 118 | }, 119 | "require-dev": { 120 | "phpunit/phpunit": "^4.0" 121 | }, 122 | "type": "library", 123 | "extra": { 124 | "branch-alias": { 125 | "dev-master": "1.4-dev" 126 | } 127 | }, 128 | "autoload": { 129 | "psr-4": { 130 | "GuzzleHttp\\Promise\\": "src/" 131 | }, 132 | "files": [ 133 | "src/functions_include.php" 134 | ] 135 | }, 136 | "notification-url": "https://packagist.org/downloads/", 137 | "license": [ 138 | "MIT" 139 | ], 140 | "authors": [ 141 | { 142 | "name": "Michael Dowling", 143 | "email": "mtdowling@gmail.com", 144 | "homepage": "https://github.com/mtdowling" 145 | } 146 | ], 147 | "description": "Guzzle promises library", 148 | "keywords": [ 149 | "promise" 150 | ], 151 | "time": "2016-12-20T10:07:11+00:00" 152 | }, 153 | { 154 | "name": "guzzlehttp/psr7", 155 | "version": "1.4.2", 156 | "source": { 157 | "type": "git", 158 | "url": "https://github.com/guzzle/psr7.git", 159 | "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" 160 | }, 161 | "dist": { 162 | "type": "zip", 163 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", 164 | "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", 165 | "shasum": "" 166 | }, 167 | "require": { 168 | "php": ">=5.4.0", 169 | "psr/http-message": "~1.0" 170 | }, 171 | "provide": { 172 | "psr/http-message-implementation": "1.0" 173 | }, 174 | "require-dev": { 175 | "phpunit/phpunit": "~4.0" 176 | }, 177 | "type": "library", 178 | "extra": { 179 | "branch-alias": { 180 | "dev-master": "1.4-dev" 181 | } 182 | }, 183 | "autoload": { 184 | "psr-4": { 185 | "GuzzleHttp\\Psr7\\": "src/" 186 | }, 187 | "files": [ 188 | "src/functions_include.php" 189 | ] 190 | }, 191 | "notification-url": "https://packagist.org/downloads/", 192 | "license": [ 193 | "MIT" 194 | ], 195 | "authors": [ 196 | { 197 | "name": "Michael Dowling", 198 | "email": "mtdowling@gmail.com", 199 | "homepage": "https://github.com/mtdowling" 200 | }, 201 | { 202 | "name": "Tobias Schultze", 203 | "homepage": "https://github.com/Tobion" 204 | } 205 | ], 206 | "description": "PSR-7 message implementation that also provides common utility methods", 207 | "keywords": [ 208 | "http", 209 | "message", 210 | "request", 211 | "response", 212 | "stream", 213 | "uri", 214 | "url" 215 | ], 216 | "time": "2017-03-20T17:10:46+00:00" 217 | }, 218 | { 219 | "name": "nikic/fast-route", 220 | "version": "v1.2.0", 221 | "source": { 222 | "type": "git", 223 | "url": "https://github.com/nikic/FastRoute.git", 224 | "reference": "b5f95749071c82a8e0f58586987627054400cdf6" 225 | }, 226 | "dist": { 227 | "type": "zip", 228 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/b5f95749071c82a8e0f58586987627054400cdf6", 229 | "reference": "b5f95749071c82a8e0f58586987627054400cdf6", 230 | "shasum": "" 231 | }, 232 | "require": { 233 | "php": ">=5.4.0" 234 | }, 235 | "type": "library", 236 | "autoload": { 237 | "psr-4": { 238 | "FastRoute\\": "src/" 239 | }, 240 | "files": [ 241 | "src/functions.php" 242 | ] 243 | }, 244 | "notification-url": "https://packagist.org/downloads/", 245 | "license": [ 246 | "BSD-3-Clause" 247 | ], 248 | "authors": [ 249 | { 250 | "name": "Nikita Popov", 251 | "email": "nikic@php.net" 252 | } 253 | ], 254 | "description": "Fast request router for PHP", 255 | "keywords": [ 256 | "router", 257 | "routing" 258 | ], 259 | "time": "2017-01-19T11:35:12+00:00" 260 | }, 261 | { 262 | "name": "pimple/pimple", 263 | "version": "v3.0.2", 264 | "source": { 265 | "type": "git", 266 | "url": "https://github.com/silexphp/Pimple.git", 267 | "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a" 268 | }, 269 | "dist": { 270 | "type": "zip", 271 | "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a", 272 | "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a", 273 | "shasum": "" 274 | }, 275 | "require": { 276 | "php": ">=5.3.0" 277 | }, 278 | "type": "library", 279 | "extra": { 280 | "branch-alias": { 281 | "dev-master": "3.0.x-dev" 282 | } 283 | }, 284 | "autoload": { 285 | "psr-0": { 286 | "Pimple": "src/" 287 | } 288 | }, 289 | "notification-url": "https://packagist.org/downloads/", 290 | "license": [ 291 | "MIT" 292 | ], 293 | "authors": [ 294 | { 295 | "name": "Fabien Potencier", 296 | "email": "fabien@symfony.com" 297 | } 298 | ], 299 | "description": "Pimple, a simple Dependency Injection Container", 300 | "homepage": "http://pimple.sensiolabs.org", 301 | "keywords": [ 302 | "container", 303 | "dependency injection" 304 | ], 305 | "time": "2015-09-11T15:10:35+00:00" 306 | }, 307 | { 308 | "name": "psr/container", 309 | "version": "1.0.0", 310 | "source": { 311 | "type": "git", 312 | "url": "https://github.com/php-fig/container.git", 313 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 314 | }, 315 | "dist": { 316 | "type": "zip", 317 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 318 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 319 | "shasum": "" 320 | }, 321 | "require": { 322 | "php": ">=5.3.0" 323 | }, 324 | "type": "library", 325 | "extra": { 326 | "branch-alias": { 327 | "dev-master": "1.0.x-dev" 328 | } 329 | }, 330 | "autoload": { 331 | "psr-4": { 332 | "Psr\\Container\\": "src/" 333 | } 334 | }, 335 | "notification-url": "https://packagist.org/downloads/", 336 | "license": [ 337 | "MIT" 338 | ], 339 | "authors": [ 340 | { 341 | "name": "PHP-FIG", 342 | "homepage": "http://www.php-fig.org/" 343 | } 344 | ], 345 | "description": "Common Container Interface (PHP FIG PSR-11)", 346 | "homepage": "https://github.com/php-fig/container", 347 | "keywords": [ 348 | "PSR-11", 349 | "container", 350 | "container-interface", 351 | "container-interop", 352 | "psr" 353 | ], 354 | "time": "2017-02-14T16:28:37+00:00" 355 | }, 356 | { 357 | "name": "psr/http-message", 358 | "version": "1.0.1", 359 | "source": { 360 | "type": "git", 361 | "url": "https://github.com/php-fig/http-message.git", 362 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 363 | }, 364 | "dist": { 365 | "type": "zip", 366 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 367 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 368 | "shasum": "" 369 | }, 370 | "require": { 371 | "php": ">=5.3.0" 372 | }, 373 | "type": "library", 374 | "extra": { 375 | "branch-alias": { 376 | "dev-master": "1.0.x-dev" 377 | } 378 | }, 379 | "autoload": { 380 | "psr-4": { 381 | "Psr\\Http\\Message\\": "src/" 382 | } 383 | }, 384 | "notification-url": "https://packagist.org/downloads/", 385 | "license": [ 386 | "MIT" 387 | ], 388 | "authors": [ 389 | { 390 | "name": "PHP-FIG", 391 | "homepage": "http://www.php-fig.org/" 392 | } 393 | ], 394 | "description": "Common interface for HTTP messages", 395 | "homepage": "https://github.com/php-fig/http-message", 396 | "keywords": [ 397 | "http", 398 | "http-message", 399 | "psr", 400 | "psr-7", 401 | "request", 402 | "response" 403 | ], 404 | "time": "2016-08-06T14:39:51+00:00" 405 | }, 406 | { 407 | "name": "slim/slim", 408 | "version": "3.8.1", 409 | "source": { 410 | "type": "git", 411 | "url": "https://github.com/slimphp/Slim.git", 412 | "reference": "5385302707530b2bccee1769613ad769859b826d" 413 | }, 414 | "dist": { 415 | "type": "zip", 416 | "url": "https://api.github.com/repos/slimphp/Slim/zipball/5385302707530b2bccee1769613ad769859b826d", 417 | "reference": "5385302707530b2bccee1769613ad769859b826d", 418 | "shasum": "" 419 | }, 420 | "require": { 421 | "container-interop/container-interop": "^1.2", 422 | "nikic/fast-route": "^1.0", 423 | "php": ">=5.5.0", 424 | "pimple/pimple": "^3.0", 425 | "psr/container": "^1.0", 426 | "psr/http-message": "^1.0" 427 | }, 428 | "provide": { 429 | "psr/http-message-implementation": "1.0" 430 | }, 431 | "require-dev": { 432 | "phpunit/phpunit": "^4.0", 433 | "squizlabs/php_codesniffer": "^2.5" 434 | }, 435 | "type": "library", 436 | "autoload": { 437 | "psr-4": { 438 | "Slim\\": "Slim" 439 | } 440 | }, 441 | "notification-url": "https://packagist.org/downloads/", 442 | "license": [ 443 | "MIT" 444 | ], 445 | "authors": [ 446 | { 447 | "name": "Rob Allen", 448 | "email": "rob@akrabat.com", 449 | "homepage": "http://akrabat.com" 450 | }, 451 | { 452 | "name": "Josh Lockhart", 453 | "email": "hello@joshlockhart.com", 454 | "homepage": "https://joshlockhart.com" 455 | }, 456 | { 457 | "name": "Gabriel Manricks", 458 | "email": "gmanricks@me.com", 459 | "homepage": "http://gabrielmanricks.com" 460 | }, 461 | { 462 | "name": "Andrew Smith", 463 | "email": "a.smith@silentworks.co.uk", 464 | "homepage": "http://silentworks.co.uk" 465 | } 466 | ], 467 | "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", 468 | "homepage": "https://slimframework.com", 469 | "keywords": [ 470 | "api", 471 | "framework", 472 | "micro", 473 | "router" 474 | ], 475 | "time": "2017-03-19T17:55:20+00:00" 476 | }, 477 | { 478 | "name": "symfony/polyfill-mbstring", 479 | "version": "v1.3.0", 480 | "source": { 481 | "type": "git", 482 | "url": "https://github.com/symfony/polyfill-mbstring.git", 483 | "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" 484 | }, 485 | "dist": { 486 | "type": "zip", 487 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", 488 | "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", 489 | "shasum": "" 490 | }, 491 | "require": { 492 | "php": ">=5.3.3" 493 | }, 494 | "suggest": { 495 | "ext-mbstring": "For best performance" 496 | }, 497 | "type": "library", 498 | "extra": { 499 | "branch-alias": { 500 | "dev-master": "1.3-dev" 501 | } 502 | }, 503 | "autoload": { 504 | "psr-4": { 505 | "Symfony\\Polyfill\\Mbstring\\": "" 506 | }, 507 | "files": [ 508 | "bootstrap.php" 509 | ] 510 | }, 511 | "notification-url": "https://packagist.org/downloads/", 512 | "license": [ 513 | "MIT" 514 | ], 515 | "authors": [ 516 | { 517 | "name": "Nicolas Grekas", 518 | "email": "p@tchwork.com" 519 | }, 520 | { 521 | "name": "Symfony Community", 522 | "homepage": "https://symfony.com/contributors" 523 | } 524 | ], 525 | "description": "Symfony polyfill for the Mbstring extension", 526 | "homepage": "https://symfony.com", 527 | "keywords": [ 528 | "compatibility", 529 | "mbstring", 530 | "polyfill", 531 | "portable", 532 | "shim" 533 | ], 534 | "time": "2016-11-14T01:06:16+00:00" 535 | }, 536 | { 537 | "name": "symfony/var-dumper", 538 | "version": "v3.2.8", 539 | "source": { 540 | "type": "git", 541 | "url": "https://github.com/symfony/var-dumper.git", 542 | "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8" 543 | }, 544 | "dist": { 545 | "type": "zip", 546 | "url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", 547 | "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", 548 | "shasum": "" 549 | }, 550 | "require": { 551 | "php": ">=5.5.9", 552 | "symfony/polyfill-mbstring": "~1.0" 553 | }, 554 | "conflict": { 555 | "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" 556 | }, 557 | "require-dev": { 558 | "ext-iconv": "*", 559 | "twig/twig": "~1.20|~2.0" 560 | }, 561 | "suggest": { 562 | "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", 563 | "ext-symfony_debug": "" 564 | }, 565 | "type": "library", 566 | "extra": { 567 | "branch-alias": { 568 | "dev-master": "3.2-dev" 569 | } 570 | }, 571 | "autoload": { 572 | "files": [ 573 | "Resources/functions/dump.php" 574 | ], 575 | "psr-4": { 576 | "Symfony\\Component\\VarDumper\\": "" 577 | }, 578 | "exclude-from-classmap": [ 579 | "/Tests/" 580 | ] 581 | }, 582 | "notification-url": "https://packagist.org/downloads/", 583 | "license": [ 584 | "MIT" 585 | ], 586 | "authors": [ 587 | { 588 | "name": "Nicolas Grekas", 589 | "email": "p@tchwork.com" 590 | }, 591 | { 592 | "name": "Symfony Community", 593 | "homepage": "https://symfony.com/contributors" 594 | } 595 | ], 596 | "description": "Symfony mechanism for exploring and dumping PHP variables", 597 | "homepage": "https://symfony.com", 598 | "keywords": [ 599 | "debug", 600 | "dump" 601 | ], 602 | "time": "2017-05-01T14:55:58+00:00" 603 | }, 604 | { 605 | "name": "twig/extensions", 606 | "version": "v1.5.0", 607 | "source": { 608 | "type": "git", 609 | "url": "https://github.com/twigphp/Twig-extensions.git", 610 | "reference": "d6d74d6f3213a9574c460c4390c25a1a67c17cc3" 611 | }, 612 | "dist": { 613 | "type": "zip", 614 | "url": "https://api.github.com/repos/twigphp/Twig-extensions/zipball/d6d74d6f3213a9574c460c4390c25a1a67c17cc3", 615 | "reference": "d6d74d6f3213a9574c460c4390c25a1a67c17cc3", 616 | "shasum": "" 617 | }, 618 | "require": { 619 | "twig/twig": "~1.27|~2.0" 620 | }, 621 | "require-dev": { 622 | "symfony/phpunit-bridge": "~3.3@dev", 623 | "symfony/translation": "~2.3|~3.0" 624 | }, 625 | "suggest": { 626 | "symfony/translation": "Allow the time_diff output to be translated" 627 | }, 628 | "type": "library", 629 | "extra": { 630 | "branch-alias": { 631 | "dev-master": "1.5-dev" 632 | } 633 | }, 634 | "autoload": { 635 | "psr-0": { 636 | "Twig_Extensions_": "lib/" 637 | }, 638 | "psr-4": { 639 | "Twig\\Extensions\\": "src/" 640 | } 641 | }, 642 | "notification-url": "https://packagist.org/downloads/", 643 | "license": [ 644 | "MIT" 645 | ], 646 | "authors": [ 647 | { 648 | "name": "Fabien Potencier", 649 | "email": "fabien@symfony.com" 650 | } 651 | ], 652 | "description": "Common additional features for Twig that do not directly belong in core", 653 | "homepage": "http://twig.sensiolabs.org/doc/extensions/index.html", 654 | "keywords": [ 655 | "i18n", 656 | "text" 657 | ], 658 | "time": "2017-05-24T06:24:07+00:00" 659 | }, 660 | { 661 | "name": "twig/twig", 662 | "version": "v2.3.2", 663 | "source": { 664 | "type": "git", 665 | "url": "https://github.com/twigphp/Twig.git", 666 | "reference": "85e8372c451510165c04bf781295f9d922fa524b" 667 | }, 668 | "dist": { 669 | "type": "zip", 670 | "url": "https://api.github.com/repos/twigphp/Twig/zipball/85e8372c451510165c04bf781295f9d922fa524b", 671 | "reference": "85e8372c451510165c04bf781295f9d922fa524b", 672 | "shasum": "" 673 | }, 674 | "require": { 675 | "php": "^7.0", 676 | "symfony/polyfill-mbstring": "~1.0" 677 | }, 678 | "require-dev": { 679 | "psr/container": "^1.0", 680 | "symfony/debug": "~2.7", 681 | "symfony/phpunit-bridge": "~3.3@dev" 682 | }, 683 | "type": "library", 684 | "extra": { 685 | "branch-alias": { 686 | "dev-master": "2.3-dev" 687 | } 688 | }, 689 | "autoload": { 690 | "psr-0": { 691 | "Twig_": "lib/" 692 | } 693 | }, 694 | "notification-url": "https://packagist.org/downloads/", 695 | "license": [ 696 | "BSD-3-Clause" 697 | ], 698 | "authors": [ 699 | { 700 | "name": "Fabien Potencier", 701 | "email": "fabien@symfony.com", 702 | "homepage": "http://fabien.potencier.org", 703 | "role": "Lead Developer" 704 | }, 705 | { 706 | "name": "Armin Ronacher", 707 | "email": "armin.ronacher@active-4.com", 708 | "role": "Project Founder" 709 | }, 710 | { 711 | "name": "Twig Team", 712 | "homepage": "http://twig.sensiolabs.org/contributors", 713 | "role": "Contributors" 714 | } 715 | ], 716 | "description": "Twig, the flexible, fast, and secure template language for PHP", 717 | "homepage": "http://twig.sensiolabs.org", 718 | "keywords": [ 719 | "templating" 720 | ], 721 | "time": "2017-04-21T00:13:02+00:00" 722 | } 723 | ], 724 | "packages-dev": [], 725 | "aliases": [], 726 | "minimum-stability": "stable", 727 | "stability-flags": [], 728 | "prefer-stable": false, 729 | "prefer-lowest": false, 730 | "platform": [], 731 | "platform-dev": [] 732 | } 733 | --------------------------------------------------------------------------------