Browse Source

Merge remote-tracking branch 'julienbourdeau/introduce-algolia'

Jordi Boggiano 7 years ago
parent
commit
5bea35837f

+ 4 - 0
app/config/config.yml

@@ -35,6 +35,10 @@ twig:
     globals:
         google_analytics: '%google_analytics%'
         packagist_host: '%packagist_host%'
+        algolia:
+          app_id: '%algolia.app_id%'
+          search_key: '%algolia.search_key%'
+          index_name: '%algolia.index_name%'
 
 # Doctrine Configuration
 doctrine:

+ 6 - 0
app/config/parameters.yml.dist

@@ -50,3 +50,9 @@ parameters:
     trusted_proxies: ~
     # e.g. ['.*\.?packagist\.org$'] to allow packagist.org and all subdomains as valid hosts
     trusted_hosts: ~
+
+    # -- Algolia credentials --
+    algolia.app_id: CHANGE_ME
+    algolia.admin_key: CHANGE_ME
+    algolia.search_key: CHANGE_ME
+    algolia.index_name: 'packagist'

+ 2 - 1
composer.json

@@ -55,7 +55,8 @@
         "knplabs/knp-menu-bundle": "^2.1",
         "ezyang/htmlpurifier": "^4.6",
         "nelmio/cors-bundle": "^1.4",
-        "cebe/markdown": "^1.1"
+        "cebe/markdown": "^1.1",
+        "algolia/algoliasearch-client-php": "^1.18"
     },
     "_comment": ["fos user bundle 2.0.0 tag needed"],
     "require-dev": {

+ 51 - 0
src/Packagist/WebBundle/Command/ConfigureAlgoliaCommand.php

@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Command;
+
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class ConfigureAlgoliaCommand extends ContainerAwareCommand
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this
+            ->setName('algolia:configure')
+            ->setDescription('Configure Algolia index')
+        ;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $index_name = $this->getContainer()->getParameter('algolia.index_name');
+        $settings = Yaml::parse(
+            file_get_contents(__DIR__.'/../Resources/config/algolia_settings.yml')
+        );
+
+        $algolia = $this->getContainer()->get('packagist.algolia.client');
+        $index = $algolia->initIndex($index_name);
+
+        $index->setSettings($settings);
+    }
+}

+ 283 - 0
src/Packagist/WebBundle/Command/IndexAlgoliaPackagesCommand.php

@@ -0,0 +1,283 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Command;
+
+use Packagist\WebBundle\Entity\Package;
+use Packagist\WebBundle\Model\DownloadManager;
+use Packagist\WebBundle\Model\FavoriteManager;
+use Solarium_Document_ReadWrite;
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Filesystem\LockHandler;
+use Doctrine\DBAL\Connection;
+
+/**
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class IndexAlgoliaPackagesCommand extends ContainerAwareCommand
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this
+            ->setName('algolia:index')
+            ->setDefinition(array(
+                new InputOption('force', null, InputOption::VALUE_NONE, 'Force a re-indexing of all packages'),
+                new InputOption('all', null, InputOption::VALUE_NONE, 'Index all packages without clearing the index first'),
+                new InputArgument('package', InputArgument::OPTIONAL, 'Package name to index'),
+            ))
+            ->setDescription('Indexes packages in Algolia')
+        ;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $verbose = $input->getOption('verbose');
+        $force = $input->getOption('force');
+        $indexAll = $input->getOption('all');
+        $package = $input->getArgument('package');
+        $index_name = $this->getContainer()->getParameter('algolia.index_name');
+
+
+        $deployLock = $this->getContainer()->getParameter('kernel.cache_dir').'/deploy.globallock';
+        if (file_exists($deployLock)) {
+            if ($verbose) {
+                $output->writeln('Aborting, '.$deployLock.' file present');
+            }
+            return;
+        }
+
+        $doctrine = $this->getContainer()->get('doctrine');
+        $algolia = $this->getContainer()->get('packagist.algolia.client');
+        $index = $algolia->initIndex($index_name);
+
+        $redis = $this->getContainer()->get('snc_redis.default');
+        $downloadManager = $this->getContainer()->get('packagist.download_manager');
+        $favoriteManager = $this->getContainer()->get('packagist.favorite_manager');
+
+        $lock = new LockHandler('packagist_algolia_indexer');
+
+        // another dumper is still active
+        if (!$lock->lock()) {
+            if ($verbose) {
+                $output->writeln('Aborting, another indexer is still active');
+            }
+            return;
+        }
+
+        if ($package) {
+            $packages = array(array('id' => $doctrine->getRepository('PackagistWebBundle:Package')->findOneByName($package)->getId()));
+        } elseif ($force || $indexAll) {
+            $packages = $doctrine->getManager()->getConnection()->fetchAll('SELECT id FROM package ORDER BY id ASC');
+            $doctrine->getManager()->getConnection()->executeQuery('UPDATE package SET indexedAt = NULL');
+        } else {
+            $packages = $doctrine->getRepository('PackagistWebBundle:Package')->getStalePackagesForIndexing();
+        }
+
+        $ids = array();
+        foreach ($packages as $row) {
+            $ids[] = $row['id'];
+        }
+
+        // clear index before a full-update
+        if ($force && !$package) {
+            if ($verbose) {
+                $output->writeln('Deleting existing index');
+            }
+
+            $index->clearIndex();
+        }
+
+        $total = count($ids);
+        $current = 0;
+
+        // update package index
+        while ($ids) {
+            $indexTime = new \DateTime;
+            $idsSlice = array_splice($ids, 0, 50);
+            $packages = $doctrine->getRepository('PackagistWebBundle:Package')->findById($idsSlice);
+
+            $idsToUpdate = [];
+            $records = [];
+
+            foreach ($packages as $package) {
+                $current++;
+                if ($verbose) {
+                    $output->writeln('['.sprintf('%'.strlen($total).'d', $current).'/'.$total.'] Indexing '.$package->getName());
+                }
+
+                try {
+                    $tags_formatted = $this->getTags($doctrine, $package);
+
+                    $records[] = $this->packageToSearchableArray($package, $tags_formatted, $redis, $downloadManager, $favoriteManager);
+
+                    $idsToUpdate[] = $package->getId();
+                } catch (\Exception $e) {
+                    $output->writeln('<error>Exception: '.$e->getMessage().', skipping package '.$package->getName().'.</error>');
+
+                    continue;
+                }
+
+//                $providers = $doctrine->getManager()->getConnection()->fetchAll(
+//                    'SELECT lp.packageName
+//                        FROM package p
+//                        JOIN package_version pv ON p.id = pv.package_id
+//                        JOIN link_provide lp ON lp.version_id = pv.id
+//                        WHERE p.id = :id
+//                        AND pv.development = true
+//                        GROUP BY lp.packageName',
+//                    ['id' => $package->getId()]
+//                );
+//                foreach ($providers as $provided) {
+//                    $provided = $provided['packageName'];
+//                    try {
+//                        $document = $update->createDocument();
+//                        $document->setField('id', $provided);
+//                        $document->setField('name', $provided);
+//                        $document->setField('package_name', '');
+//                        $document->setField('description', '');
+//                        $document->setField('type', 'virtual-package');
+//                        $document->setField('trendiness', 100);
+//                        $document->setField('repository', '');
+//                        $document->setField('abandoned', 0);
+//                        $document->setField('replacementPackage', '');
+//                        $update->addDocument($document);
+//                    } catch (\Exception $e) {
+//                        $output->writeln('<error>'.get_class($e).': '.$e->getMessage().', skipping package '.$package->getName().':provide:'.$provided.'</error>');
+//                    }
+//                }
+            }
+
+            try {
+                $index->addObjects($records);
+            } catch (\Exception $e) {
+                $output->writeln('<error>'.get_class($e).': '.$e->getMessage().', occurred while processing packages: '.implode(',', $idsSlice).'</error>');
+                continue;
+            }
+
+            $doctrine->getManager()->clear();
+            unset($packages);
+
+            if ($verbose) {
+                $output->writeln('Updating package indexedAt column');
+            }
+
+            $this->updateIndexedAt($idsToUpdate, $doctrine, $indexTime->format('Y-m-d H:i:s'));
+        }
+
+        $lock->release();
+    }
+
+    private function packageToSearchableArray(
+        Package $package,
+        array $tags,
+        $redis,
+        DownloadManager $downloadManager,
+        FavoriteManager $favoriteManager
+    ) {
+        $faversCount = $favoriteManager->getFaverCount($package);
+        $downloads = $downloadManager->getTotalDownloads($package);
+        $download_log = $package['downloads']['monthly'] ? log($package['downloads']['monthly'], 10) : 0;
+        $start_log = $package->getGitHubStars() ? log($package->getGitHubStars(), 10) : 0;
+        $popularity = round($download_log + $start_log);
+
+        $record = [
+            'id' => $package->getId(),
+            'objectID' => $package->getName(),
+            'name' => $package->getName(),
+            'package_organisation' => $package->getVendor(),
+            'package_name' => $package->getPackageName(),
+            'description' => preg_replace('{[\x00-\x1f]+}u', '', $package->getDescription()),
+            'type' => $package->getType(),
+            'repository' => $package->getRepository(),
+            'language' => $package->getLanguage(),
+            'trendiness' => $redis->zscore('downloads:trending', $package->getId()),
+            'popularity' => $popularity,
+            'meta' => [
+                'downloads' => $downloads,
+                'download_formatted' => [
+                    'total' => number_format($downloads['total'], 0, ',', ' '),
+                    'monthly' => number_format($downloads['monthly'], 0, ',', ' '),
+                    'daily' => number_format($downloads['daily'], 0, ',', ' '),
+                ],
+                'favers' => $faversCount,
+                'favers_formatted' => number_format($faversCount, 0, ',', ' '),
+            ],
+        ];
+
+        if ($package->isAbandoned()) {
+            $record['abandoned'] = 1;
+            $record['replacementPackage'] = $package->getReplacementPackage() ?: '';
+        } else {
+            $record['abandoned'] = 0;
+            $record['replacementPackage'] = '';
+        }
+
+
+        $record['tags'] = $tags;
+
+        return $record;
+    }
+
+    private function getTags($doctrine, $package)
+    {
+        $tags = $doctrine->getManager()->getConnection()->fetchAll(
+            'SELECT t.name FROM package p
+                            JOIN package_version pv ON p.id = pv.package_id
+                            JOIN version_tag vt ON vt.version_id = pv.id
+                            JOIN tag t ON t.id = vt.tag_id
+                            WHERE p.id = :id
+                            GROUP BY t.id, t.name',
+            ['id' => $package->getId()]
+        );
+
+        foreach ($tags as $idx => $tag) {
+            $tags[$idx] = $tag['name'];
+        }
+
+        return array_map(function ($tag) {
+            return mb_strtolower(preg_replace('{[\x00-\x1f]+}u', '', $tag), 'UTF-8');
+        }, $tags);
+    }
+
+    private function updateIndexedAt($idsToUpdate, $doctrine, $time)
+    {
+        $retries = 5;
+        // retry loop in case of a lock timeout
+        while ($retries--) {
+            try {
+                $doctrine->getManager()->getConnection()->executeQuery(
+                    'UPDATE package SET indexedAt=:indexed WHERE id IN (:ids)',
+                    [
+                        'ids' => $idsToUpdate,
+                        'indexed' => $time,
+                    ],
+                    ['ids' => Connection::PARAM_INT_ARRAY]
+                );
+            } catch (\Exception $e) {
+                if (!$retries) {
+                    throw $e;
+                }
+                sleep(2);
+            }
+        }
+    }
+}

+ 23 - 0
src/Packagist/WebBundle/Resources/config/algolia_settings.yml

@@ -0,0 +1,23 @@
+searchableAttributes:
+    - "package_name"
+    - "package_organisation"
+    - "description"
+
+ranking:
+    - "typo"
+    - "geo"
+    - "words"
+    - "filters"
+    - "attribute"
+    - "proximity"
+    - "exact"
+    - "custom"
+
+customRanking:
+    - "desc(popularity)"
+
+attributesToHighlight:
+    - "name"
+    - "package_name"
+    - "package_organisation"
+    - "description"

+ 4 - 0
src/Packagist/WebBundle/Resources/config/services.yml

@@ -131,5 +131,9 @@ services:
         tags:
             - { name: knp_menu.menu, alias: profile_menu }
 
+    packagist.algolia.client:
+        class: AlgoliaSearch\Client
+        arguments: ['%algolia.app_id%', '%algolia.admin_key%']
+
 parameters:
     security.exception_listener.class: Packagist\WebBundle\Security\ExceptionListener

+ 21 - 6
src/Packagist/WebBundle/Resources/public/css/main.css

@@ -580,11 +580,11 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
     color: rgb(82, 168, 236);
 }
 
-ul.packages {
+.packages {
     margin-top: 20px;
 }
 
-ul.packages h1 {
+.packages h1 {
   font-family: Verdana;
   font-size: 22px;
   line-height: 1em;
@@ -597,20 +597,20 @@ ul.packages h1 {
   white-space: nowrap;
 }
 
-ul.packages .metadata {
+.packages .metadata {
   color: #555;
   font-size: 15px;
 }
-ul.packages .metadata .glyphicon {
+.packages .metadata .glyphicon {
   top: 3px;
   margin: 0 1px;
   font-size: 18px;
 }
-ul.packages .metadata-block {
+.packages .metadata-block {
   display: block;
   margin-bottom: 10px;
 }
-ul.packages .metadata-block:last-child {
+.packages .metadata-block:last-child {
   margin-bottom: 0;
 }
 
@@ -1233,6 +1233,21 @@ ul.packages .metadata-block:last-child {
     }
 }
 
+#powered-by {
+    float: right;
+    width: 160px;
+    text-align: right;
+    padding-top: 16px;
+}
+#powered-by a img {
+    width: 77px;
+}
+
+#pagination-container {
+    text-align: center;
+    margin-bottom: 32px;
+}
+
 .pagination {
     margin: 6px 0;
 }

File diff suppressed because it is too large
+ 7 - 0
src/Packagist/WebBundle/Resources/public/img/algolia-logo-light.svg


+ 83 - 158
src/Packagist/WebBundle/Resources/public/js/search.js

@@ -1,160 +1,85 @@
-/*jslint browser: true */
-/*global jQuery: true */
-(function ($) {
-    "use strict";
-
-    var list = $('.search-list'),
-        form = $('form#search-form'),
-        showResults,
-        doSearch,
-        searching = false,
-        searchQueued = false,
-        previousQuery = form.serialize(),
-        firstQuery = true;
-
-    showResults = function (page) {
-        var newList = $(page);
-
-        list.html(newList.html());
-        list.parent().removeClass('hidden');
-        list.find('ul.packages li:first').addClass('selected');
-        $('.order-by-group').attr('href', function (index, current) {
-            return current.replace(/q=.*?&/, 'q=' + encodeURIComponent($('input[type="search"]', form).val()) + '&')
-        });
-        $('.js-search-field-wrapper').removeClass('col-xs-12').addClass('col-xs-8');
-        $('#order-bys-wrapper').removeClass('hidden');
-
-        searching = false;
-
-        if (searchQueued) {
-            doSearch();
-            searchQueued = false;
-        }
-    };
-
-    doSearch = function () {
-        var currentQuery,
-            q,
-            pathname,
-            urlPrefix,
-            url,
-            title;
-
-        if (searching) {
-            searchQueued = true;
-            return;
-        }
-
-        if ($('#search_query_query').val().match(/^\s*$/) !== null) {
-            if (!firstQuery) {
-                list.parent().addClass('hidden');
-                $('#order-bys-wrapper').addClass('hidden');
-            }
-            return;
-        }
-
-        currentQuery = form.serialize();
-
-        if (previousQuery === currentQuery) {
-            return;
+var search = instantsearch({
+    appId: algoliaConfig.app_id,
+    apiKey: algoliaConfig.search_key,
+    indexName: algoliaConfig.index_name,
+    urlSync: true,
+    searchFunction: function(helper) {
+        var searchResults = $('#search-container');
+        if (helper.state.query === '') {
+            searchResults.hide();
         }
-
-        $('.order-by-group .active').removeClass('active');
-
-        if (window.history.pushState) {
-            q = encodeURIComponent($('input[type="search"]', form).val());
-
-            pathname = window.location.pathname;
-
-            if (pathname.indexOf('/app_dev.php') === 0) {
-                urlPrefix = '/app_dev.php';
-            } else if (pathname.indexOf('/app.php') === 0) {
-                urlPrefix = '/app.php';
-            } else {
-                urlPrefix = '';
-            }
-
-            url = urlPrefix + '/search/?q=' + q;
-            title = 'Search';
-
-            if (firstQuery) {
-                window.history.pushState(null, title, url);
-                firstQuery = false;
-            } else {
-                window.history.replaceState(null, title, url);
-            }
-        }
-
-        $.ajax({
-            url: form.attr('action'),
-            data: currentQuery,
-            success: showResults
-        });
-
-        searching = true;
-        previousQuery = currentQuery;
-    };
-
-    form.bind('keyup search', doSearch);
-
-    form.bind('keydown', function (event) {
-        var keymap,
-            currentSelected,
-            nextSelected;
-
-        keymap = {
-            enter: 13,
-            left: 37,
-            up: 38,
-            right: 39,
-            down: 40
-        };
-
-        if (keymap.up !== event.which && keymap.down !== event.which && keymap.enter !== event.which) {
-            return;
-        }
-
-        if ($('#search_query_query').val().match(/^\s*$/) !== null) {
-            document.activeElement.blur();
-            return;
-        }
-
-        event.preventDefault();
-
-        currentSelected = list.find('ul.packages li.selected');
-        nextSelected = (keymap.down === event.which) ? currentSelected.next('li') : currentSelected.prev('li');
-
-        if (currentSelected.length === 0 && keymap.down === event.which) {
-            currentSelected = list.find('ul.packages li:first').addClass('selected');
-        }
-
-        if (keymap.enter === event.which && currentSelected.data('url')) {
-            var url = currentSelected.data('url');
-            if (event.ctrlKey) {
-                window.open(url);
-                return;
-            }
-
-            window.location = url;
-            return;
-        }
-
-        if (nextSelected.length > 0) {
-            currentSelected.removeClass('selected');
-            nextSelected.addClass('selected');
-
-            nextSelected.get(0).scrollIntoView(false);
-        }
-    });
-
-    // handle pressing S to focus the search
-    $(document.body).bind('keyup', function (event) {
-        if (event.which === 83 && (!document.activeElement || -1 === ['INPUT','SELECT','TEXTAREA'].indexOf(document.activeElement.tagName))) {
-            $('#search_query_query').focus();
-        }
-    });
-
-    if ($(document).width() >= 992) {
-        $('#search_query_query').focus();
+        helper.search();
+        searchResults.show();
     }
-}(jQuery));
+});
+
+search.addWidget(
+    instantsearch.widgets.searchBox({
+        container: '#search_query_query',
+        magnifier: false,
+        reset: false,
+        wrapInput: false
+    })
+);
+
+search.addWidget(
+    instantsearch.widgets.hits({
+        container: '.search-list',
+        templates: {
+            empty: 'No packages found.',
+            item: `
+<div data-url="{{ packageUrl }}" class="col-xs-12 package-item">
+    <div class="row">
+        <div class="col-sm-9 col-lg-10">
+            <p class="pull-right language">{{ language }}</p>
+            <h4 class="font-bold">
+                <a href="{{ packageUrl }}">{{ name }}</a>
+                {{#virtual}}
+                    <small>(Virtual Package)</small>
+                {{/virtual}}
+                {{#showAutoUpdateWarning}}
+                    <small>(Not Auto-Updated)</small>
+                {{/showAutoUpdateWarning}}
+            </h4>
+
+            <p>{{ description }}</p>
+
+            {{#abandoned}}
+            <p class="abandoned">
+                <i class="glyphicon glyphicon-exclamation-sign"></i> Abandoned!
+                {{#replacementPackage}}
+                    See <a href="{{ replacementPackage.link }}">{{ replacementPackage.name }}</a>
+                {{/replacementPackage}}
+            </p>
+            {{/abandoned}}
+        </div>
+
+        <div class="col-sm-3 col-lg-2">
+            {{#meta}}
+                <p class="metadata">
+                    <span class="metadata-block"><i class="glyphicon glyphicon-arrow-down"></i> {{ meta.download_formatted.total }}</span>
+                    <span class="metadata-block"><i class="glyphicon glyphicon-star"></i> {{ meta.favers_formatted }}</span>
+                </p>
+            {{/meta}}
+        </div>
+    </div>
+</div>
+`
+        },
+        cssClasses: {
+            root: 'packages',
+            item: 'row'
+        }
+    })
+);
+
+search.addWidget(
+    instantsearch.widgets.pagination({
+        container: '.pagination',
+        maxPages: 200,
+        scrollTo: false,
+        showFirstLast: false,
+    })
+);
+
+search.start();

+ 11 - 0
src/Packagist/WebBundle/Resources/views/Web/search.html.twig

@@ -6,5 +6,16 @@
         <div class="search-list">
             {{ block('list') }}
         </div>
+
+        <div id="powered-by">
+            Search by
+            <a href="https://www.algolia.com/">
+                <img src="{{ asset('bundles/packagistweb/img/algolia-logo-light.svg') }}">
+            </a>
+        </div>
+
+        <div id="pagination-container">
+            <div class="pagination"></div>
+        </div>
     {% endblock %}
 {% endembed %}

+ 1 - 22
src/Packagist/WebBundle/Resources/views/Web/searchForm.html.twig

@@ -5,28 +5,7 @@
             {{ form_widget(searchForm.query, {'attr': {'autocomplete': 'off', 'placeholder': 'Search packages...', 'tabindex': 1}}) }}
         </div>
 
-        {% set hasActiveOrderBy = false %}
-        {% spaceless %}
-        <div id="order-bys-wrapper" class="col-xs-4 col-md-3 col-lg-2{% if searchForm.vars.value.query is empty %} hidden{% endif %}">
-            {% for sort, param in orderBys %}
-                <a title="{{ param.title }}" href="{{ param.href }}" class="order-by-group">
-                    <i class="glyphicon {{ param.class }}{% if param.arrowClass is not empty %} active{% endif %}"></i>
-                    {% if param.arrowClass is not empty %}
-                        {% set hasActiveOrderBy = true %}
-                    {% endif %}
-                </a>
-            {% endfor %}
-            {% if hasActiveOrderBy %}
-                <a title="Clear order bys" href="?q={{ searchForm.vars.value.query }}" class="clear active">
-                    <i class="glyphicon glyphicon-remove-circle"></i>
-                </a>
-            {% endif %}
-        </div>
-        {% endspaceless %}
-        <div
-            class="hidden">
-            {{ form_widget(searchForm.orderBys) }}
-        </div>
         {{ form_rest(searchForm) }}
+
     </div>
 {{ form_end(searchForm) }}

+ 12 - 1
src/Packagist/WebBundle/Resources/views/layout.html.twig

@@ -138,8 +138,14 @@
         <section class="wrapper">
             <section class="container content" role="main">
                 {% block search_results %}
-                    <div class="row hidden">
+                    <div id="search-container" class="row">
                         <div class="search-list col-md-12"></div>
+                        <div id="powered-by">
+                            Search by <a href="https://www.algolia.com/"><img src="{{ asset('bundles/packagistweb/img/algolia-logo-light.svg') }}"></a>
+                        </div>
+                        <div id="pagination-container">
+                            <div class="pagination"></div>
+                        </div>
                     </div>
                 {% endblock %}
 
@@ -183,8 +189,13 @@
             </nav>
         </footer>
 
+        <script>
+            var algoliaConfig = {{ algolia|json_encode|raw }};
+        </script>
+
         <script src="{{ asset('libs/jquery-2.1.4.min.js') }}"></script>
         <script src="{{ asset('libs/humane-3.2.2.min.js') }}"></script>
+        <script src="{{ asset('libs/algolia-instantsearch-2.0.2/instantsearch.min.js') }}"></script>
         <script src="{{ asset('bundles/packagistweb/js/layout.js') }}"></script>
         <script src="{{ asset('bundles/packagistweb/js/search.js')}}"></script>
         <script src="{{ asset('libs/bootstrap-3.3.5/js/bootstrap.min.js')}}"></script>

File diff suppressed because it is too large
+ 1 - 0
web/libs/algolia-instantsearch-2.0.2/instantsearch.min.js


Some files were not shown because too many files changed in this diff