1
0
mirror of https://github.com/devfake/flox.git synced 2024-11-08 19:32:29 +01:00

Init new version

This commit is contained in:
devfake 2016-10-10 10:57:39 +02:00
parent a7cba7dc2e
commit b754d84149
118 changed files with 26229 additions and 5 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
* text=auto
*.css linguist-vendored
*.scss linguist-vendored

12
.gitignore vendored
View File

@ -1,5 +1,9 @@
.idea
.env
/backend/vendor
/client/node_modules
/backend/vendor
/.idea
Homestead.json
Homestead.yaml
/backend/.env
*/.DS_Store
/public/assets/poster/*.jpg

View File

@ -1 +1,60 @@
New version is coming soon
![flox](http://80.240.132.120/flox-demo/public/assets/img/dark.jpg)
Flox
===============
Flox is a self hosted Movie watch list. It's build on top of PHP (Laravel), MySQL and Vue.js and uses [The Movie Database](https://www.themoviedb.org/) API.
The rating based on an 3-Point system for `good`, `medium` and `bad`.
[Try live demo](http://80.240.132.120/flox-demo/public/) and [login](http://80.240.132.120/flox-demo/public/login) with `demo / demo` to add new movies or change ratings.
### Requirements
* PHP >=5.6.4
* [Composer](https://getcomposer.org/)
* [The Movie Database](https://www.themoviedb.org/) Account for the [API-Key](https://www.themoviedb.org/faq/api)
### Install
* Download Flox and `cd` into `backend` and run
```bash
composer install
php artisan flox:init # Enter here your database credentials
php artisan flox:db # Running migrations and enter your admin credentials for the site
```
* Enter your TMDB API-Key in `backend/.env`
* Set your `CLIENT_URI` in `backend/.env`. If you use something like XAMPP or MAMP, the CLIENT_URI should be `/flox/public`. If you use something like Homestead, the CLIENT_URI should be `/`.
### Alternative Language
All movie titles are in english by default. You can check if TMDB has an alternative title by setting `ALTERNATIVE_LANGUAGE` in `backend/.env`.
The most commons are `DE`, `IT`, `FR`, `ES` and `RU`.
### Better Search
You can use [Laravel Scout](https://laravel.com/docs/master/scout) for better search results with typos. Something like `findg nemo`.
Add your driver and API-Keys in `backend/.env` and uncomment `use Searchable;` in `backend/app/Item.php` on line 11.
To sync your own movie list with your Laravel Scout driver, run `php artisan flox:sync` (If you using Laravel Scout later).
[Algolia](https://www.algolia.com/) is a great service and has a free hacker plan.
Note: Laravel Scout is on the demo site not active.
### Development
* Run `npm install` in your `/client` folder.
* Make sure you have installed `webpack` globally.
* Run `npm run dev` or `npm run build`.
### Misc
* Give `backend/storage` and `public/assets` recursive write access.
### Todo
* Settings
* Change username / password
* Export and import
* Sync scout driver
* Ajax request for settings
* Better responsive

18
backend/.env.example Normal file
View File

@ -0,0 +1,18 @@
TMDB_API_KEY=
ALTERNATIVE_LANGUAGE=
CLIENT_URI=/
LOADING_ITEMS=30
SCOUT_DRIVER=
ALGOLIA_APP_ID=
ALGOLIA_SECRET=
DB_CONNECTION=mysql
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
APP_ENV=local
APP_KEY=
APP_DEBUG=true

View File

@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\User;
use Illuminate\Console\Command;
class DB extends Command {
protected $signature = 'flox:db';
protected $description = 'Create database migrations and admin account';
public function __construct()
{
parent::__construct();
}
public function handle()
{
try {
$this->createMigrations();
} catch(\Exception $e) {
$this->error('Can not connect to the database. Error: ' . $e->getMessage());
$this->error('Make sure your database credentials in .env are correct');
return;
}
$this->createUser();
}
private function createMigrations()
{
$this->info('TRYING TO MIGRATE DATABASE');
$this->call('migrate', ['--force' => true]);
$this->info('MIGRATION COMPLETED');
}
private function createUser()
{
$username = $this->ask('Enter your admin username');
$password = $this->ask('Enter your admin password');
$user = new User();
$user->username = $username;
$user->password = bcrypt($password);
$user->save();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Init extends Command {
protected $signature = 'flox:init';
protected $description = 'Create .env file, set the app key and fill database credentials';
private $requests = [
'DB_DATABASE' => 'Name',
'DB_USERNAME' => 'Username',
'DB_PASSWORD' => 'Password',
];
public function __construct()
{
parent::__construct();
}
public function handle()
{
$this->createENVFile();
$this->fillDatabaseCredentials();
$this->setAppKey();
}
private function createENVFile()
{
if( ! file_exists('.env')) {
$this->info('CREATING .ENV FILE');
copy('.env.example', '.env');
}
}
private function fillDatabaseCredentials()
{
foreach($this->requests as $type => $text) {
if( ! env($type)) {
$value = $this->ask('Enter your Database ' . $text);
$this->changeENV($type, $value);
}
}
}
private function setAppKey()
{
if( ! env('APP_KEY')) {
$this->info('GENERATING APP KEY');
$this->callSilent('key:generate');
}
}
private function changeENV($type, $value)
{
file_put_contents('.env', str_replace(
$type . '=',
$type . '=' . $value,
file_get_contents('.env')
));
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Sync extends Command {
protected $signature = 'flox:sync';
protected $description = 'Synchronize your movies with your scout search driver';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$scoutDriver = env('SCOUT_DRIVER');
try {
$this->info('TRYING TO SYNC YOUR MOVIES TO ' . strtoupper($scoutDriver));
$this->call('scout:import', ['model' => 'App\\Item']);
$this->info('SYNCHRONIZATION COMPLETED');
} catch(\Exception $e) {
$this->error('Can not connect to ' . $scoutDriver . '. Error: ' . $e->getMessage());
$this->error('Make sure your ' . $scoutDriver . ' credentials in .env are correct');
return;
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
Commands\Init::class,
Commands\DB::class,
Commands\Sync::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')
// ->hourly();
}
/**
* Register the Closure based commands for the application.
*
* @return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@ -0,0 +1,152 @@
<?php
namespace App\Http\Controllers;
use App\Item;
use App\Services\TMDB;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Storage;
class ItemController {
private $loadingItems;
private $item;
/**
* Get the amout of loading items and create an instance for 'item'.
*
* @param Item $item
*/
public function __construct(Item $item)
{
$this->loadingItems = config('app.LOADING_ITEMS');
$this->item = $item;
}
/**
* Return all items for home with pagination.
*
* @param $orderBy
* @return mixed
*/
public function items($orderBy)
{
$orderType = $orderBy == 'rating' ? 'asc' : 'desc';
return $this->item->orderBy($orderBy, $orderType)->simplePaginate($this->loadingItems);
}
/**
* Search for items by 'title' in database or with Laravel Scout.
*
* @return mixed
*/
public function search()
{
$title = Input::get('q');
if(config('scout.driver')) {
return $this->item->search($title)->get();
}
// We don't have an smart search driver and return an simple 'like' query.
return $this->item->where('title', 'LIKE', '%' . $title . '%')
->orWhere('alternative_title', 'LIKE', '%' . $title . '%')
->get();
}
/**
* Update rating for an movie.
*
* @param $itemID
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function changeRating($itemID)
{
$item = $this->item->find($itemID);
if( ! $item) {
return response('Not Found', 404);
}
$item->update([
'rating' => Input::get('rating')
]);
}
/**
* Add a new movie to database and create the poster image file.
*
* @param TMDB $tmdb
* @return Item
*/
public function add(TMDB $tmdb)
{
$data = Input::get('item');
$this->createPosterFile($data['poster']);
return $this->createItem($data, $tmdb);
}
/**
* Delete movie in database and delete the poster image file.
*
* @param $itemID
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function remove($itemID)
{
$item = $this->item->find($itemID);
if( ! $item) {
return response('Not Found', 404);
}
$this->removePosterFile($item->poster);
$item->delete();
}
/**
* Create the new movie.
*
* @param $data
* @param $tmdb
* @return Item
*/
private function createItem($data, $tmdb)
{
return $this->item->create([
'tmdb_id' => $data['tmdb_id'],
'title' => $data['title'],
'alternative_title' => $tmdb->alternativeMovieTitle($data["tmdb_id"]),
'poster' => $data['poster'],
'rating' => 1,
'released' => $data['released'],
'created_at' => time(),
]);
}
/**
* Create the poster image file.
*
* @param $poster
*/
private function createPosterFile($poster)
{
if($poster) {
Storage::put($poster, file_get_contents('http://image.tmdb.org/t/p/w185' . $poster));
}
}
/**
* Delete the poster image file.
*
* @param $poster
*/
private function removePosterFile($poster)
{
Storage::delete($poster);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use App\Services\TMDB;
use Illuminate\Support\Facades\Input;
class TMDBController {
private $tmdb;
public function __construct(TMDB $tmdb)
{
$this->tmdb = $tmdb;
}
public function search()
{
return $this->tmdb->search(Input::get('q'));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Facades\Input;
class UserController {
private $auth;
/**
* Create an instance for 'auth'.
*
* @param Guard $auth
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Login user and return correct response.
*
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function login()
{
$username = Input::get('username');
$password = Input::get('password');
if($this->auth->attempt(['username' => $username, 'password' => $password], true)) {
return response('Success', 200);
}
return response('Unauthorized', 401);
}
/**
* Logout user and redirect to home.
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function logout()
{
$this->auth->logout();
return redirect('/');
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
class EncryptCookies extends BaseEncrypter
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

24
backend/app/Item.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Item extends Model {
// Uncomment this if you are using Laravel Scout.
//use Searchable;
public $timestamps = false;
protected $fillable = [
'tmdb_id',
'title',
'alternative_title',
'poster',
'rating',
'released',
'created_at',
];
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Broadcast::routes();
/*
* Authenticate the user's personal channel...
*/
Broadcast::channel('App.User.*', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\SomeEvent' => [
'App\Listeners\EventListener',
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
parent::boot();
//
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to your controller routes.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = 'App\Http\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
//
parent::boot();
}
/**
* Define the routes for the application.
*
* @return void
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
//
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::group([
'middleware' => 'api',
'namespace' => $this->namespace,
'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');
});
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Services;
use DateTime;
use GuzzleHttp\Client;
class TMDB {
private $client;
private $apiKey;
/**
* Get the API Key for TMDB and create an instance of Guzzle.
*/
public function __construct()
{
$this->apiKey = config('app.TMDB_API_KEY');
$this->client = new Client(['base_uri' => 'http://api.themoviedb.org/']);
}
/**
* Search TMDB for an movie by 'title'.
*
* @param $title
* @return array
*/
public function search($title)
{
$items = [];
$response = $this->client->get('/3/search/movie', ['query' => ['api_key' => $this->apiKey, 'query' => $title]]);
$response = json_decode($response->getBody());
foreach($response->results as $result) {
$dtime = DateTime::createFromFormat('Y-m-d', ($result->release_date ?: '1970-12-1'));
$items[] = [
'tmdb_id' => $result->id,
'title' => $result->title,
'poster' => $result->poster_path,
'released' => $dtime->getTimestamp(),
];
}
return $items;
}
/**
* Make a new request to TMDB to get the 'alternative language' movie title.
*
* @param $tmdb_id
* @return null|string
*/
public function alternativeMovieTitle($tmdb_id)
{
$alternativeLanguage = config('app.ALTERNATIVE_LANGUAGE');
$response = $this->client->get('/3/movie/' . $tmdb_id . '/alternative_titles', ['query' => ['api_key' => $this->apiKey]]);
$titles = collect(json_decode($response->getBody())->titles);
$title = $titles->where('iso_3166_1', $alternativeLanguage);
if($title->isEmpty()) {
return null;
}
// '3D' is often used in the title. We don't need them.
return trim(str_replace('3D', '', $title->first()->title));
}
}

16
backend/app/User.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable {
protected $fillable = [
'username', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
}

51
backend/artisan Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env php
<?php
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/bootstrap/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running. We will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);

55
backend/bootstrap/app.php Normal file
View File

@ -0,0 +1,55 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

View File

@ -0,0 +1,34 @@
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Composer Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Include The Compiled Class File
|--------------------------------------------------------------------------
|
| To dramatically increase your application's performance, you may use a
| compiled class file which contains all of the classes commonly used
| by a request. The Artisan "optimize" is used to create this file.
|
*/
$compiledPath = __DIR__.'/cache/compiled.php';
if (file_exists($compiledPath)) {
require $compiledPath;
}

2
backend/bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

53
backend/composer.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.3.*",
"laravel/scout": "^1.1",
"guzzlehttp/guzzle": "^6.2",
"algolia/algoliasearch-client-php": "^1.10"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~5.0",
"symfony/css-selector": "3.1.*",
"symfony/dom-crawler": "3.1.*"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
"scripts": {
"post-root-package-install": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate"
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimize"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
]
},
"config": {
"preferred-install": "dist"
}
}

3786
backend/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

238
backend/config/app.php Normal file
View File

@ -0,0 +1,238 @@
<?php
return [
'TMDB_API_KEY' => env('TMDB_API_KEY'),
'ALTERNATIVE_LANGUAGE' => env('ALTERNATIVE_LANGUAGE'),
'LOADING_ITEMS' => env('LOADING_ITEMS'),
'CLIENT_URI' => env('CLIENT_URI'),
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
*/
'name' => 'Flox',
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services your application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Logging Configuration
|--------------------------------------------------------------------------
|
| Here you may configure the log settings for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Settings: "single", "daily", "syslog", "errorlog"
|
*/
'log' => env('APP_LOG', 'daily'),
'log_level' => env('APP_LOG_LEVEL', 'debug'),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
//
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
Laravel\Scout\ScoutServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
];

102
backend/config/auth.php Normal file
View File

@ -0,0 +1,102 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
],
];

View File

@ -0,0 +1,58 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "pusher", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_DRIVER', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_KEY'),
'secret' => env('PUSHER_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
//
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

91
backend/config/cache.php Normal file
View File

@ -0,0 +1,91 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache connection that gets used while
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
| Supported: "apc", "array", "database", "file", "memcached", "redis"
|
*/
'default' => env('CACHE_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
*/
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing a RAM based store such as APC or Memcached, there might
| be other applications utilizing the same cache. So, we'll specify a
| value to get prefixed to all our keys so we can avoid collisions.
|
*/
'prefix' => 'laravel',
];

View File

@ -0,0 +1,35 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Additional Compiled Classes
|--------------------------------------------------------------------------
|
| Here you may specify additional classes to include in the compiled file
| generated by the `artisan optimize` command. These should be classes
| that are included on basically every request into the application.
|
*/
'files' => [
//
],
/*
|--------------------------------------------------------------------------
| Compiled File Providers
|--------------------------------------------------------------------------
|
| Here you may list service providers which define a "compiles" function
| that returns additional files that should be compiled, providing an
| easy way to get common files from any packages you are utilizing.
|
*/
'providers' => [
//
],
];

121
backend/config/database.php Normal file
View File

@ -0,0 +1,121 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| PDO Fetch Style
|--------------------------------------------------------------------------
|
| By default, database results will be returned as instances of the PHP
| stdClass object; however, you may desire to retrieve records in an
| array format for simplicity. Here you can tweak the fetch style.
|
*/
'fetch' => PDO::FETCH_OBJ,
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'cluster' => false,
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
];

View File

@ -0,0 +1,67 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. A "local" driver, as well as a variety of cloud
| based drivers are available for your choosing. Just store away!
|
| Supported: "local", "ftp", "s3", "rackspace"
|
*/
'default' => 'local',
/*
|--------------------------------------------------------------------------
| Default Cloud Filesystem Disk
|--------------------------------------------------------------------------
|
| Many applications store files both locally and in the cloud. For this
| reason, you may specify a default "cloud" driver here. This driver
| will be bound as the Cloud disk implementation in the container.
|
*/
'cloud' => 's3',
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Here you may configure as many filesystem "disks" as you wish, and you
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => base_path('../public/assets/poster'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => 'your-key',
'secret' => 'your-secret',
'region' => 'your-region',
'bucket' => 'your-bucket',
],
],
];

115
backend/config/mail.php Normal file
View File

@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Mail Driver
|--------------------------------------------------------------------------
|
| Laravel supports both SMTP and PHP's "mail" function as drivers for the
| sending of e-mail. You may specify which one you're using throughout
| your application here. By default, Laravel is setup for SMTP mail.
|
| Supported: "smtp", "mail", "sendmail", "mailgun", "mandrill",
| "ses", "sparkpost", "log"
|
*/
'driver' => env('MAIL_DRIVER', 'smtp'),
/*
|--------------------------------------------------------------------------
| SMTP Host Address
|--------------------------------------------------------------------------
|
| Here you may provide the host address of the SMTP server used by your
| applications. A default option is provided that is compatible with
| the Mailgun mail service which will provide reliable deliveries.
|
*/
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
/*
|--------------------------------------------------------------------------
| SMTP Host Port
|--------------------------------------------------------------------------
|
| This is the SMTP port used by your application to deliver e-mails to
| users of the application. Like the host we have set this value to
| stay compatible with the Mailgun e-mail application by default.
|
*/
'port' => env('MAIL_PORT', 587),
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => [
'address' => 'hello@example.com',
'name' => 'Example',
],
/*
|--------------------------------------------------------------------------
| E-Mail Encryption Protocol
|--------------------------------------------------------------------------
|
| Here you may specify the encryption protocol that should be used when
| the application send e-mail messages. A sensible default using the
| transport layer security protocol should provide great security.
|
*/
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
/*
|--------------------------------------------------------------------------
| SMTP Server Username
|--------------------------------------------------------------------------
|
| If your SMTP server requires a username for authentication, you should
| set it here. This will get used to authenticate with your server on
| connection. You may also set the "password" value below this one.
|
*/
'username' => env('MAIL_USERNAME'),
/*
|--------------------------------------------------------------------------
| SMTP Server Password
|--------------------------------------------------------------------------
|
| Here you may set the password required by your SMTP server to send out
| messages from your application. This will be given to the server on
| connection so that the application will be able to send messages.
|
*/
'password' => env('MAIL_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Sendmail System Path
|--------------------------------------------------------------------------
|
| When using the "sendmail" driver to send e-mails, we will need to know
| the path to where Sendmail lives on this server. A default path has
| been provided here, which will work well on most of your systems.
|
*/
'sendmail' => '/usr/sbin/sendmail -bs',
];

85
backend/config/queue.php Normal file
View File

@ -0,0 +1,85 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Driver
|--------------------------------------------------------------------------
|
| The Laravel queue API supports a variety of back-ends via an unified
| API, giving you convenient access to each back-end using the same
| syntax for each one. Here you may set the default queue driver.
|
| Supported: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
*/
'default' => env('QUEUE_DRIVER', 'sync'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection information for each server that
| is used by your application. A default configuration has been added
| for each back-end shipped with Laravel. You are free to add more.
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => 'localhost',
'queue' => 'default',
'retry_after' => 90,
],
'sqs' => [
'driver' => 'sqs',
'key' => 'your-public-key',
'secret' => 'your-secret-key',
'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id',
'queue' => 'your-queue-name',
'region' => 'us-east-1',
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
],
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control which database and table are used to store the jobs that
| have failed. You may change them to any database / table you wish.
|
*/
'failed' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],
];

83
backend/config/scout.php Normal file
View File

@ -0,0 +1,83 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "elasticsearch", "null"
|
*/
'driver' => env('SCOUT_DRIVER'),
/*
|--------------------------------------------------------------------------
| Index Prefix
|--------------------------------------------------------------------------
|
| Here you may specify a prefix that will be applied to all search index
| names used by Scout. This prefix may be useful if you have multiple
| "tenants" or applications sharing the same search infrastructure.
|
*/
'prefix' => env('SCOUT_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Queue Data Syncing
|--------------------------------------------------------------------------
|
| This option allows you to control if the operations that sync your data
| with your search engines are queued. When this is set to "true" then
| all automatic data syncing will get queued for better performance.
|
*/
'queue' => false,
/*
|--------------------------------------------------------------------------
| Algolia Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Algolia settings. Algolia is a cloud hosted
| search engine which works great with Scout out of the box. Just plug
| in your application ID and admin API key to get started searching.
|
*/
'algolia' => [
'id' => env('ALGOLIA_APP_ID'),
'secret' => env('ALGOLIA_SECRET'),
],
/*
|--------------------------------------------------------------------------
| Elasticsearch Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for Elasticsearch, which is a
| distributed, open source search and analytics engine. Feel free
| to add as many Elasticsearch servers as required by your app.
|
*/
'elasticsearch' => [
'index' => env('ELASTICSEARCH_INDEX', 'laravel'),
'config' => [
'hosts' => [
env('ELASTICSEARCH_HOST', 'localhost'),
],
],
],
];

View File

@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Stripe, Mailgun, SparkPost and others. This file provides a sane
| default location for this type of information, allowing packages
| to have a conventional place to find your various credentials.
|
*/
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],
'ses' => [
'key' => env('SES_KEY'),
'secret' => env('SES_SECRET'),
'region' => 'us-east-1',
],
'sparkpost' => [
'secret' => env('SPARKPOST_SECRET'),
],
'stripe' => [
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
];

179
backend/config/session.php Normal file
View File

@ -0,0 +1,179 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option controls the default session "driver" that will be used on
| requests. By default, we will use the lightweight native driver but
| you may specify any of the other wonderful drivers provided here.
|
| Supported: "file", "cookie", "database", "apc",
| "memcached", "redis", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
'lifetime' => 120,
'expire_on_close' => false,
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it is stored. All encryption will be run
| automatically by Laravel and you can use the Session like normal.
|
*/
'encrypt' => false,
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When using the native session driver, we need a location where session
| files may be stored. A default has been set for you but a different
| location may be specified. This is only needed for file sessions.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => null,
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table we
| should use to manage the sessions. Of course, a sensible default is
| provided for you; however, you are free to change this as needed.
|
*/
'table' => 'sessions',
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using the "apc" or "memcached" session drivers, you may specify a
| cache store that should be used for these sessions. This value must
| correspond with one of the application's configured cache stores.
|
*/
'store' => null,
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the cookie used to identify a session
| instance by ID. The name specified here will get used every time a
| new session cookie is created by the framework for every driver.
|
*/
'cookie' => 'laravel_session',
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application but you are free to change this when necessary.
|
*/
'path' => '/',
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| Here you may change the domain of the cookie used to identify a session
| in your application. This will determine which domains the cookie is
| available to in your application. A sensible default has been set.
|
*/
'domain' => env('SESSION_DOMAIN', null),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you if it can not be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE', false),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. You are free to modify this option if needed.
|
*/
'http_only' => true,
];

33
backend/config/view.php Normal file
View File

@ -0,0 +1,33 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| View Storage Paths
|--------------------------------------------------------------------------
|
| Most templating systems load templates from disk. Here you may specify
| an array of paths that should be checked for your views. Of course
| the usual Laravel view path has already been registered for you.
|
*/
'paths' => [
realpath(base_path('../client/resources')),
],
/*
|--------------------------------------------------------------------------
| Compiled View Path
|--------------------------------------------------------------------------
|
| This option determines where all the compiled Blade templates will be
| stored for your application. Typically, this is within the storage
| directory. However, as usual, you are free to change this value.
|
*/
'compiled' => realpath(storage_path('framework/views')),
];

1
backend/database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite

View File

@ -0,0 +1,23 @@
<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/
/*$factory->define(App\User::class, function (Faker\Generator $faker) {
static $password;
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => $password ?: $password = bcrypt('secret'),
'remember_token' => str_random(10),
];
});*/

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('items', function(Blueprint $table) {
$table->increments('id');
$table->integer('tmdb_id')->unique();
$table->string('title')->index();
$table->string('alternative_title')->index()->nullable();
$table->string('poster');
$table->string('rating');
$table->integer('released');
$table->integer('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('items');
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('username')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,16 @@
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// $this->call(UsersTableSeeder::class);
}
}

27
backend/phpunit.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>

0
backend/routes/api.php Normal file
View File

View File

21
backend/routes/web.php Normal file
View File

@ -0,0 +1,21 @@
<?php
Route::group(['prefix' => 'api'], function() {
Route::post('/login', 'UserController@login');
Route::get('/logout', 'UserController@logout');
Route::get('/items/{orderBy}', 'ItemController@items');
Route::get('/search-items', 'ItemController@search');
Route::group(['middleware' => 'auth'], function() {
Route::get('/search-tmdb', 'TMDBController@search');
Route::post('/add', 'ItemController@add');
Route::patch('/change-rating/{itemID}', 'ItemController@changeRating');
Route::delete('/remove/{itemID}', 'ItemController@remove');
});
});
Route::get('/{uri?}', function($uri = null) {
return view('app');
})->where('uri', '(.*)');

21
backend/server.php Normal file
View File

@ -0,0 +1,21 @@
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
$uri = urldecode(
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);
// This file allows us to emulate Apache's "mod_rewrite" functionality from the
// built-in PHP web server. This provides a convenient way to test a Laravel
// application without having installed a "real" web server software here.
if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
return false;
}
require_once __DIR__.'/public/index.php';

3
backend/storage/app/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!public/
!.gitignore

2
backend/storage/app/public/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

8
backend/storage/framework/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
config.php
routes.php
schedule-*
compiled.php
services.json
events.scanned.php
routes.scanned.php
down

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,2 @@
*
!.gitignore

2
backend/storage/logs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,19 @@
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
/**
* A basic functional test example.
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel');
}
}

View File

@ -0,0 +1,25 @@
<?php
abstract class TestCase extends Illuminate\Foundation\Testing\TestCase
{
/**
* The base URL to use while testing the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
}

5
client/.babelrc Normal file
View File

@ -0,0 +1,5 @@
{
"presets": ["es2015", "stage-2"],
"plugins": ["transform-runtime"],
"comments": false
}

45
client/app/app.js Normal file
View File

@ -0,0 +1,45 @@
require('../resources/sass/app.scss');
import Vue from 'vue';
import { mapActions, mapState } from 'vuex'
import SiteHeader from './components/Header.vue';
import Search from './components/Search.vue';
import SiteFooter from './components/Footer.vue';
import Login from './components/Login.vue';
import router from './routes';
import store from './store/index';
const App = new Vue({
store,
router,
created() {
this.checkForUserColorScheme();
},
computed: {
...mapState({
colorScheme: state => state.colorScheme
})
},
components: {
SiteHeader, Search, SiteFooter, Login
},
methods: {
...mapActions([ 'setColorScheme' ]),
checkForUserColorScheme() {
if( ! localStorage.getItem('color')) {
localStorage.setItem('color', 'light');
}
this.setColorScheme(localStorage.getItem('color'));
}
}
});
App.$mount('#app');

View File

@ -0,0 +1,58 @@
<template>
<main>
<div class="wrap-content" v-if=" ! loading">
<Item :item="item" v-for="(item, index) in items" :key="index"></Item>
<span class="nothing-found" v-if=" ! items.length">No Movies Found</span>
<div class="load-more-wrap">
<span class="load-more" v-if=" ! clickedMoreLoading && paginator" @click="loadMore()">LOAD MORE</span>
<span class="loader" v-if="clickedMoreLoading"><i></i></span>
</div>
</div>
<span class="loader fullsize-loader" v-if="loading"><i></i></span>
</main>
</template>
<script>
import Item from './Item.vue';
import { mapActions, mapState } from 'vuex'
export default {
created() {
this.fetchData();
},
computed: {
...mapState({
loading: state => state.loading,
items: state => state.items,
userFilter: state => state.userFilter,
clickedMoreLoading: state => state.clickedMoreLoading,
paginator: state => state.paginator
})
},
methods: {
...mapActions([ 'loadItems', 'loadMoreItems', 'setSearchTitle' ]),
fetchData() {
this.loadItems(this.userFilter);
this.setSearchTitle('');
},
loadMore() {
this.loadMoreItems(this.paginator);
}
},
components: {
Item
},
watch: {
'$route': 'fetchData'
}
}
</script>

View File

@ -0,0 +1,109 @@
<template>
<transition mode="out-in" name="fade">
<div class="item-wrap">
<div class="item-image-wrap">
<span v-if="localItem.rating" :class="'item-rating rating-' + localItem.rating" @click="changeRating()">
<i class="icon-rating"></i>
</span>
<span v-if=" ! localItem.rating" class="item-rating item-new" @click="addNewItem()">
<span class="loader smallsize-loader" v-if="rated"><i></i></span>
<i class="icon-add" v-if=" ! rated"></i>
</span>
<span class="remove-item" v-if="localItem.rating && auth" @click="removeItem()"><i class="icon-remove"></i></span>
<img v-if="localItem.poster" :src="poster" class="item-image" width="185" height="278">
<span class="no-image" v-if=" ! localItem.poster"></span>
</div>
<div class="item-content">
<span class="item-year">{{ released }}</span>
<a :href="'https://www.youtube.com/results?search_query=' + title + ' Trailer'" target="_blank" :title="title" class="item-title">{{ title }}</a>
</div>
</div>
</transition>
</template>
<script>
export default {
props: ['item'],
data() {
return {
localItem: this.item,
saveTimeout: null,
auth: config.auth,
prevRating: null,
rated: false
}
},
computed: {
title() {
return this.localItem.alternative_title ? this.localItem.alternative_title : this.localItem.title;
},
poster() {
if(this.localItem.rating) {
return config.poster + this.localItem.poster;
}
return config.posterTMDB + this.localItem.poster;
},
released() {
const released = new Date(this.localItem.released * 1000);
return released.getFullYear();
}
},
methods: {
changeRating() {
if(this.auth) {
clearTimeout(this.saveTimeout);
this.prevRating = this.localItem.rating;
this.localItem.rating = this.prevRating == 3 ? 1 : +this.prevRating + 1;
this.saveTimeout = setTimeout(() => {
this.saveNewRating();
}, 500);
}
},
saveNewRating() {
this.$http.patch(`${config.api}/change-rating/${this.localItem.id}`, {rating: this.localItem.rating}).catch(error => {
this.localItem.rating = this.prevRating;
alert('Error in saveNewRating()');
});
},
addNewItem() {
if(this.auth) {
this.rated = true;
this.$http.post(`${config.api}/add`, {item: this.localItem}).then(value => {
this.localItem = value.data;
}, error => {
if(error.status == 409) {
alert(this.title + ' already exists!');
}
});
}
},
removeItem() {
if(this.auth) {
const confirm = window.confirm('Are you sure?');
if(confirm) {
this.$http.delete(`${config.api}/remove/${this.localItem.id}`).then(value => {
this.localItem.rating = null;
}, error => {
alert('Error in removeItem()');
});
}
}
}
}
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<main>
<div class="wrap-content" v-if=" ! loading">
<Item v-for="(item, index) in floxItems" :item="item" :key="index"></Item>
<Item v-for="(item, index) in tmdbItems" :item="item" :key="index"></Item>
<span class="nothing-found" v-if=" ! floxItems.length && ! tmdbItems.length">Nothing Found</span>
</div>
<span class="loader fullsize-loader" v-if="loading"><i></i></span>
</main>
</template>
<script>
import Item from './Item.vue';
import Helper from '../../helper';
import { mapState } from 'vuex'
export default {
mixins: [Helper],
created() {
this.initSearch();
},
data() {
return {
floxItems: [],
tmdbItems: []
}
},
computed: {
...mapState({
searchTitle: state => state.searchTitle,
loading: state => state.loading
})
},
methods: {
initSearch() {
this.$store.commit('SET_SEARCH_TITLE', this.$route.query.q);
this.$store.commit('SET_LOADING', true);
this.searchFlox();
this.searchTMDB().then(() => {
setTimeout(() => {
this.$store.commit('SET_LOADING', false);
}, 500);
});
},
searchFlox() {
this.$http.get(`${config.api}/search-items?q=${this.searchTitle}`).then(value => {
this.floxItems = value.data;
});
},
async searchTMDB() {
if(config.auth) {
await this.$http.get(`${config.api}/search-tmdb?q=${this.searchTitle}`).then(value => {
const floxItems = this.floxItems.map(item => item.tmdb_id);
this.tmdbItems = value.data.filter(item => ! floxItems.includes(item.tmdb_id));
}).catch(error => {
alert('Error in searchTMDB(): ' + error);
});
}
}
},
components: {
Item
},
watch: {
$route() {
this.scrollToTop();
this.initSearch();
}
}
}
</script>

View File

@ -0,0 +1,32 @@
<template>
<main>
<div class="wrap-content" v-if=" ! loading">
<span class="nothing-found">Settings coming soon...</span>
</div>
<span class="loader fullsize-loader" v-if="loading"><i></i></span>
</main>
</template>
<script>
export default {
created() {
this.fetchUserData();
},
data() {
return {
loading: true,
username: ''
}
},
methods: {
fetchUserData() {
setTimeout(() => {
this.loading = false;
}, 800);
}
}
}
</script>

View File

@ -0,0 +1,41 @@
<template>
<footer v-show=" ! loading">
<div class="wrap">
<span class="attribution">
<a href="https://www.themoviedb.org/" target="_blank">
<i class="icon-tmdb"></i>
</a>
This product uses the TMDb API but is not endorsed or certified by TMDb
</span>
<a class="icon-github" href="https://github.com/devfake/flox" target="_blank"></a>
<div class="sub-links">
<a v-if="auth" :href="settings" class="login-btn">Settings</a>
<a v-if="auth" :href="logout" class="login-btn">Logout</a>
<a v-if=" ! auth" :href="login" class="login-btn">Login</a>
</div>
</div>
</footer>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
auth: config.auth,
logout: config.api + '/logout',
login: config.url + '/login',
settings: config.url + '/settings'
}
},
computed: {
...mapState({
loading: state => state.loading
})
}
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<header>
<div class="wrap">
<router-link to="/" class="logo" >
<img src="../../../public/assets/img/logo.png" alt="Flox" width="108" height="32">
</router-link>
<span class="sort-wrap">
<i class="icon-sort-time" :class="{active: userFilter == 'created_at'}" @click="setUserFilter('created_at')"></i>
<i class="icon-sort-star" :class="{active: userFilter == 'rating'}" @click="setUserFilter('rating')"></i>
<span class="icon-constrast" @click="toggleColorScheme()"><i></i></span>
</span>
</div>
</header>
</template>
<script>
import store from '../store/index';
import { mapActions, mapMutations, mapState } from 'vuex'
export default {
created() {
this.checkForUserFilter();
},
computed: {
...mapState({
userFilter: state => state.userFilter,
colorScheme: state => state.colorScheme
}),
root() {
return config.uri;
}
},
methods: {
...mapActions([ 'setColorScheme', 'loadItems' ]),
...mapMutations([ 'SET_USER_FILTER' ]),
toggleColorScheme() {
const color = this.colorScheme == 'light' ? 'dark' : 'light';
this.setColorScheme(color);
},
checkForUserFilter() {
if( ! localStorage.getItem('filter')) {
localStorage.setItem('filter', 'created_at');
}
this.SET_USER_FILTER(localStorage.getItem('filter'));
},
setUserFilter(type) {
localStorage.setItem('filter', type);
this.SET_USER_FILTER(type);
this.loadItems(type);
}
}
}
</script>

View File

@ -0,0 +1,55 @@
<template>
<div>
<span class="top-bar"></span>
<div class="login-wrap">
<img src="../../../public/assets/img/logo-login.png" class="logo-login" alt="Flox" width="108" height="32">
<form class="login-form" @submit.prevent="login()">
<input type="text" placeholder="Username" v-model="username" autofocus>
<input type="password" placeholder="Password" v-model="password">
<span class="login-error"><span v-if="error">Wrong username or password</span></span>
<input type="submit" :class="errorShake ? 'shake-horizontal shake-constant' : ''" value="Sign In">
</form>
</div>
</div>
</template>
<script>
export default {
created() {
document.body.className += ' dark';
},
data() {
return {
username: '',
password: '',
error: false,
errorShake: false
}
},
methods: {
login() {
this.error = false;
const username = this.username;
const password = this.password;
this.$http.post(`${config.api}/login`, {username, password}).then(value => {
window.location.href = config.url;
}, error => {
this.error = true;
this.errorShake = true;
setTimeout(() => {
this.errorShake = false;
}, 500);
})
}
}
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<section class="search-wrap" :class="{sticky: sticky}">
<div class="wrap">
<form class="search-form" @submit.prevent="search()">
<router-link to="/"><i @click="scrollToTop()" class="icon-logo-small"></i></router-link>
<i class="icon-search"></i>
<input type="text" :placeholder="placeholder" v-model="title" class="search-input" autofocus>
<i class="icon-algolia" v-if="algolia"></i>
</form>
</div>
</section>
</template>
<script>
import Helper from '../helper';
export default {
mixins: [Helper],
mounted() {
this.initSticky();
},
data() {
return {
sticky: false
}
},
computed: {
algolia() {
return config.scoutDriver == 'algolia' && this.$route.query.q;
},
title: {
get() {
return this.$store.state.searchTitle;
},
set(title) {
this.$store.commit('SET_SEARCH_TITLE', title);
}
},
placeholder() {
return config.auth ? 'Search or add movie' : 'Search movie';
}
},
methods: {
initSticky() {
const height = document.querySelector('header').scrollHeight;
window.onscroll = () => {
this.sticky = document.body.scrollTop + document.documentElement.scrollTop > height;
};
},
search() {
if(this.title != '') {
this.$router.push({
path: '/search?q=' + this.title
});
}
}
}
}
</script>

18
client/app/config.js Normal file
View File

@ -0,0 +1,18 @@
import Vue from 'vue';
import Resource from 'vue-resource';
Vue.use(Resource);
Vue.http.headers.common['X-CSRF-TOKEN'] = document.querySelector('#token').getAttribute('content');
const {url, uri, auth, scoutDriver} = document.body.dataset;
export default {
uri,
url,
auth,
scoutDriver,
poster: url + '/assets/poster',
posterTMDB: 'http://image.tmdb.org/t/p/w185',
api: url + '/api'
};

23
client/app/helper.js Normal file
View File

@ -0,0 +1,23 @@
export default {
methods: {
// http://stackoverflow.com/a/24559613
scrollToTop(scrollDuration = 300) {
let cosParameter = window.scrollY / 2;
let scrollCount = 0;
let oldTimestamp = performance.now();
function step(newTimestamp) {
scrollCount += Math.PI / (scrollDuration / (newTimestamp - oldTimestamp));
if(scrollCount >= Math.PI) window.scrollTo(0, 0);
if(window.scrollY === 0) return;
window.scrollTo(0, Math.round(cosParameter + cosParameter * Math.cos(scrollCount)));
oldTimestamp = newTimestamp;
window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
}
}
}

22
client/app/routes.js Normal file
View File

@ -0,0 +1,22 @@
import Vue from 'vue';
import Router from 'vue-router';
import config from './config';
window.config = config;
import Content from './components/Content/Content.vue';
import SearchContent from './components/Content/SearchContent.vue';
import Settings from './components/Content/Settings.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
base: config.uri,
routes: [
{ path: '/', component: Content },
{ path: '/search', component: SearchContent },
{ path: '/settings', component: Settings },
{ path: '*', component: Content }
]
});

View File

@ -0,0 +1,45 @@
import Vue from 'vue';
import Resource from 'vue-resource';
Vue.use(Resource);
export function loadItems({commit}, filter) {
commit('SET_LOADING', true);
Vue.http.get(`${config.api}/items/${filter}`).then(value => {
const {data, next_page_url} = value.data;
commit('SET_ITEMS', data);
commit('SET_PAGINATOR', next_page_url);
setTimeout(() => {
commit('SET_LOADING', false);
}, 500);
}, error => {
if(error.status == 404) {
window.location.href = config.url;
}
});
}
export function loadMoreItems({commit}, next_page_url) {
commit('SET_CLICKED_LOADING', true);
Vue.http.get(next_page_url).then(value => {
const {data, next_page_url} = value.data;
commit('SET_PAGINATOR', next_page_url);
setTimeout(() => {
commit('PUSH_TO_ITEMS', data);
commit('SET_CLICKED_LOADING', false);
}, 500);
});
}
export function setSearchTitle({commit}, title) {
commit('SET_SEARCH_TITLE', title);
}
export function setColorScheme({commit}, color) {
localStorage.setItem('color', color);
commit('SET_COLOR_SCHEME', color);
}

21
client/app/store/index.js Normal file
View File

@ -0,0 +1,21 @@
import Vue from 'vue';
import Vuex from 'vuex'
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
items: [],
searchTitle: '',
userFilter: '',
loading: false,
clickedMoreLoading: false,
paginator: null,
colorScheme: ''
},
mutations,
actions
});

View File

@ -0,0 +1,35 @@
import * as type from './types';
export default {
[type.SET_SEARCH_TITLE](state, title) {
state.searchTitle = title;
},
[type.SET_USER_FILTER](state, filter) {
state.userFilter = filter;
},
[type.SET_ITEMS](state, items) {
state.items = items;
},
[type.PUSH_TO_ITEMS](state, items) {
state.items.push(...items);
},
[type.SET_LOADING](state, loading) {
state.loading = loading;
},
[type.SET_PAGINATOR](state, data) {
state.paginator = data;
},
[type.SET_CLICKED_LOADING](state, loading) {
state.clickedMoreLoading = loading;
},
[type.SET_COLOR_SCHEME](state, color) {
state.colorScheme = color;
}
}

View File

@ -0,0 +1,8 @@
export const SET_SEARCH_TITLE = 'SET_SEARCH_TITLE';
export const SET_USER_FILTER = 'SET_USER_FILTER';
export const SET_ITEMS = 'SET_ITEMS';
export const PUSH_TO_ITEMS = 'PUSH_TO_ITEMS';
export const SET_LOADING = 'SET_LOADING';
export const SET_PAGINATOR = 'SET_PAGINATOR';
export const SET_CLICKED_LOADING = 'SET_CLICKED_LOADING';
export const SET_COLOR_SCHEME = 'SET_COLOR_SCHEME';

19
client/gulpfile.js Normal file
View File

@ -0,0 +1,19 @@
const elixir = require('laravel-elixir');
require('laravel-elixir-vue-2');
/*
|--------------------------------------------------------------------------
| Elixir Asset Management
|--------------------------------------------------------------------------
|
| Elixir provides a clean, fluent API for defining some basic Gulp tasks
| for your Laravel application. By default, we are compiling the Sass
| file for our application, as well as publishing vendor resources.
|
*/
elixir(mix => {
mix.sass('app.scss')
.webpack('app.js');
});

34
client/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"private": true,
"scripts": {
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
"dev": "webpack -w"
},
"dependencies": {
"babel-runtime": "^6.11.6",
"vue": "^2.0.1",
"vue-resource": "^1.0.3",
"vue-router": "^2.0.0",
"vuex": "^2.0.0"
},
"devDependencies": {
"autoprefixer": "^6.5.0",
"babel-core": "^6.17.0",
"babel-loader": "^6.2.5",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.16.0",
"babel-preset-stage-2": "^6.17.0",
"cross-env": "^3.1.1",
"css-loader": "^0.25.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"lost": "^7.1.0",
"postcss-loader": "^0.13.0",
"sass-loader": "^4.0.2",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"vue-html-loader": "^1.2.3",
"vue-loader": "^9.5.1",
"webpack": "^1.13.2"
}
}

View File

@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta id="token" content="{{ csrf_token() }}">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<title>Flox - Collect Your Movie Watch List</title>
<link rel="stylesheet" href="{{ url('assets/app.css') }}">
<link href="{{ url('assets/favicon.ico?v=3') }}" rel="icon" type="image/x-icon">
</head>
<body
data-url="{{ url('/') }}"
data-uri="{{ config('app.CLIENT_URI') }}"
data-scout-driver="{{ config('scout.driver') }}"
data-auth="{{ Auth::check() }}"
class="{{ Auth::check() ? 'logged' : 'guest' }}"
>
<div id="app" :class="colorScheme">
@if(Request::is('login'))
<login></login>
@else
<site-header></site-header>
<search></search>
<router-view></router-view>
<site-footer></site-footer>
@endif
</div>
<script src="{{ url('assets/vendor.js') }}"></script>
<script src="{{ url('assets/app.js') }}"></script>
</body>
</html>

46
client/resources/sass/_base.scss vendored Normal file
View File

@ -0,0 +1,46 @@
* {
box-sizing: border-box;
font-family: 'Open Sans', sans-serif;
}
body {
overflow-y: scroll;
background: #fff;
&.dark {
background: #1c1c1c;
}
}
// todo: webkit placeholder opacity
html {
-webkit-text-size-adjust: 100%;
}
input {
-webkit-appearance: none !important;
}
.wrap,
.wrap-content {
lost-center: 1300px 20px;
}
.wrap-content {
@include media(1) { lost-center: 1120px 20px; }
@include media(2) { lost-center: 960px 20px; }
@include media(3) { lost-center: 800px 20px; }
@include media(4) { lost-center: 620px 20px; }
@include media(6) { lost-center: 290px 20px; }
}
input,
a {
outline: 0
}
::selection {
background: rgba($main1, .99);
color: #fff;
}

153
client/resources/sass/_misc.scss vendored Normal file
View File

@ -0,0 +1,153 @@
$main1: #895bff;
$main2: #f1309a;
$dark: #484848;
$rating1: #6bb01a;
$rating2: #da9527;
$rating3: #de2020;
@mixin transition($type, $type2: '') {
@if $type2 == '' {
transition: $type .2s ease 0s;
}
@else {
transition: $type .2s ease 0s, $type2 .2s ease 0s;
}
}
@mixin media($point) {
@if $point == 1 {
@media (max-width: 1320px) { @content; }
}
@else if $point == 2 {
@media (max-width: 1140px) { @content; }
}
@else if $point == 3 {
@media (max-width: 860px) { @content; }
}
@else if $point == 4 {
@media (max-width: 740px) { @content; }
}
@else if $point == 5 {
@media (max-width: 620px) { @content; }
}
@else if $point == 6 {
@media (max-width: 460px) { @content; }
}
@else if $point == sticky {
@media (min-width: 901px) { @content; }
}
@else {
@media (max-width: $point) { @content; }
}
}
.loader {
width: 29px;
height: 29px;
display: block;
margin: 0 auto;
border: 4px solid rgb(241,48,154);
animation: cssload-loader 2.3s infinite ease;
.dark & {
opacity: .6;
}
i {
vertical-align: top;
display: inline-block;
width: 100%;
background-color: rgb(241,48,154);
animation: cssload-loader-inner 2.3s infinite ease-in;
}
}
.fullsize-loader {
position: absolute;
left: calc(50% - 14px);
top: calc(50% - 14px);
}
.smallsize-loader {
border: 4px solid #fff;
width: 19px;
height: 19px;
margin: 15px auto;
i {
background-color: #fff;
}
}
@keyframes cssload-loader {
0% { transform: rotate(0deg); }
25% { transform: rotate(180deg); }
50% { transform: rotate(180deg); }
75% { transform: rotate(360deg); }
100% { transform: rotate(360deg); }
}
@keyframes cssload-loader-inner {
0% { height: 0; }
25% { height: 0; }
50% { height: 100%; }
75% { height: 100%; }
100% { height: 0; }
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}

419
client/resources/sass/_normalize.scss vendored Normal file
View File

@ -0,0 +1,419 @@
/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Prevent adjustments of font size after orientation changes in IE and iOS.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
* 2. Add the correct display in IE.
*/
article,
aside,
details, /* 1 */
figcaption,
figure,
footer,
header,
main, /* 2 */
menu,
nav,
section,
summary { /* 1 */
display: block;
}
/**
* Add the correct display in IE 9-.
*/
audio,
canvas,
progress,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Add the correct display in IE 10-.
* 1. Add the correct display in IE.
*/
template, /* 1 */
[hidden] {
display: none;
}
/* Links
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/* Forms
========================================================================== */
/**
* 1. Change font properties to `inherit` in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
select,
textarea {
font: inherit; /* 1 */
margin: 0; /* 2 */
}
/**
* Restore the font weight unset by the previous rule.
*/
optgroup {
font-weight: bold;
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on OS X.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Correct the text style of placeholders in Chrome, Edge, and Safari.
*/
::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}

129
client/resources/sass/_shake.scss vendored Normal file
View File

@ -0,0 +1,129 @@
/* * * * * * * * * * * * * * * * * * * * *\
CSShake :: shake-horizontal
v1.5.0
CSS classes to move your DOM
(c) 2015 @elrumordelaluz
http://elrumordelaluz.github.io/csshake/
Licensed under MIT
\* * * * * * * * * * * * * * * * * * * * */
.shake-horizontal {
display: inline-block;
transform-origin: center center; }
.shake-freeze,
.shake-constant.shake-constant--hover:hover,
.shake-trigger:hover .shake-constant.shake-constant--hover {
animation-play-state: paused; }
.shake-freeze:hover,
.shake-trigger:hover .shake-freeze, .shake-horizontal:hover,
.shake-trigger:hover .shake-horizontal {
animation-play-state: running; }
@keyframes shake-horizontal {
2% {
transform: translate(-2px, 0) rotate(0); }
4% {
transform: translate(-8px, 0) rotate(0); }
6% {
transform: translate(7px, 0) rotate(0); }
8% {
transform: translate(3px, 0) rotate(0); }
10% {
transform: translate(-6px, 0) rotate(0); }
12% {
transform: translate(0px, 0) rotate(0); }
14% {
transform: translate(-9px, 0) rotate(0); }
16% {
transform: translate(-2px, 0) rotate(0); }
18% {
transform: translate(3px, 0) rotate(0); }
20% {
transform: translate(0px, 0) rotate(0); }
22% {
transform: translate(9px, 0) rotate(0); }
24% {
transform: translate(-5px, 0) rotate(0); }
26% {
transform: translate(6px, 0) rotate(0); }
28% {
transform: translate(5px, 0) rotate(0); }
30% {
transform: translate(4px, 0) rotate(0); }
32% {
transform: translate(-5px, 0) rotate(0); }
34% {
transform: translate(9px, 0) rotate(0); }
36% {
transform: translate(1px, 0) rotate(0); }
38% {
transform: translate(7px, 0) rotate(0); }
40% {
transform: translate(0px, 0) rotate(0); }
42% {
transform: translate(2px, 0) rotate(0); }
44% {
transform: translate(-3px, 0) rotate(0); }
46% {
transform: translate(10px, 0) rotate(0); }
48% {
transform: translate(-3px, 0) rotate(0); }
50% {
transform: translate(10px, 0) rotate(0); }
52% {
transform: translate(-3px, 0) rotate(0); }
54% {
transform: translate(-5px, 0) rotate(0); }
56% {
transform: translate(6px, 0) rotate(0); }
58% {
transform: translate(-4px, 0) rotate(0); }
60% {
transform: translate(10px, 0) rotate(0); }
62% {
transform: translate(6px, 0) rotate(0); }
64% {
transform: translate(-3px, 0) rotate(0); }
66% {
transform: translate(1px, 0) rotate(0); }
68% {
transform: translate(-5px, 0) rotate(0); }
70% {
transform: translate(3px, 0) rotate(0); }
72% {
transform: translate(-9px, 0) rotate(0); }
74% {
transform: translate(-3px, 0) rotate(0); }
76% {
transform: translate(6px, 0) rotate(0); }
78% {
transform: translate(-7px, 0) rotate(0); }
80% {
transform: translate(-3px, 0) rotate(0); }
82% {
transform: translate(7px, 0) rotate(0); }
84% {
transform: translate(1px, 0) rotate(0); }
86% {
transform: translate(1px, 0) rotate(0); }
88% {
transform: translate(8px, 0) rotate(0); }
90% {
transform: translate(5px, 0) rotate(0); }
92% {
transform: translate(10px, 0) rotate(0); }
94% {
transform: translate(-4px, 0) rotate(0); }
96% {
transform: translate(7px, 0) rotate(0); }
98% {
transform: translate(-4px, 0) rotate(0); }
0%, 100% {
transform: translate(0, 0) rotate(0); } }
.shake-horizontal:hover,
.shake-trigger:hover .shake-horizontal,
.shake-horizontal.shake-freeze,
.shake-horizontal.shake-constant {
animation: shake-horizontal 100ms ease-in-out infinite; }

96
client/resources/sass/_sprite.scss vendored Normal file
View File

@ -0,0 +1,96 @@
/*
SCSS variables are information about icon's compiled state, stored under its original file name
.icon-home {
width: $icon-home-width;
}
The large array-like variables contain all information about a single icon
$icon-home: x y offset_x offset_y width height total_width total_height image_path;
At the bottom of this section, we provide information about the spritesheet itself
$spritesheet: width height image $spritesheet-sprites;
*/
$search-name: 'search';
$search-x: 0px;
$search-y: 0px;
$search-offset-x: 0px;
$search-offset-y: 0px;
$search-width: 20px;
$search-height: 20px;
$search-total-width: 20px;
$search-total-height: 20px;
$search-image: '../public/assets/img/sprite.png';
$search: (0px, 0px, 0px, 0px, 20px, 20px, 20px, 20px, '../public/assets/img/sprite.png', 'search', );
$spritesheet-width: 20px;
$spritesheet-height: 20px;
$spritesheet-image: '../public/assets/img/sprite.png';
$spritesheet-sprites: ($search, );
$spritesheet: (20px, 20px, '../public/assets/img/sprite.png', $spritesheet-sprites, );
/*
The provided mixins are intended to be used with the array-like variables
.icon-home {
@include sprite-width($icon-home);
}
.icon-email {
@include sprite($icon-email);
}
Example usage in HTML:
`display: block` sprite:
<div class="icon-home"></div>
To change `display` (e.g. `display: inline-block;`), we suggest using a common CSS class:
// CSS
.icon {
display: inline-block;
}
// HTML
<i class="icon icon-home"></i>
*/
@mixin sprite-width($sprite) {
width: nth($sprite, 5);
}
@mixin sprite-height($sprite) {
height: nth($sprite, 6);
}
@mixin sprite-position($sprite) {
$sprite-offset-x: nth($sprite, 3);
$sprite-offset-y: nth($sprite, 4);
background-position: $sprite-offset-x $sprite-offset-y;
}
@mixin sprite-image($sprite) {
$sprite-image: nth($sprite, 9);
background-image: url(#{$sprite-image});
}
@mixin sprite($sprite) {
@include sprite-image($sprite);
@include sprite-position($sprite);
@include sprite-width($sprite);
@include sprite-height($sprite);
}
/*
The `sprites` mixin generates identical output to the CSS template
but can be overridden inside of SCSS
@include sprites($spritesheet-sprites);
*/
@mixin sprites($sprites) {
@each $sprite in $sprites {
$sprite-name: nth($sprite, 10);
.#{$sprite-name} {
@include sprite($sprite);
}
}
}

13
client/resources/sass/app.scss vendored Normal file
View File

@ -0,0 +1,13 @@
@import
'normalize',
'misc',
'sprite',
'shake',
'base',
'components/header',
'components/search',
'components/content',
'components/login',
'components/footer';

View File

@ -0,0 +1,262 @@
main {
float: left;
width: 100%;
padding: 110px 0 0 0;
min-height: 100vh;
.dark & {
background: #1c1c1c;
}
}
.item-wrap {
margin: 0 0 60px 0;
lost-column: 1/6;
@include media(1) { lost-column: 1/4; }
@include media(3) { lost-column: 1/5; }
@include media(4) { lost-column: 1/4; }
@include media(5) { lost-column: 1/3; }
@include media(6) { lost-column: 1/2; }
}
.item-image-wrap {
position: relative;
float: left;
max-height: 278px;
@include media(3) {
width: 120px;
height: auto;
}
@include media(6) {
width: 100px;
}
&:hover {
.item-new {
display: block;
}
}
}
.item-image {
box-shadow: 0 12px 15px 0 rgba(0, 0, 0, .5);
@include media(3) {
width: 100%;
height: auto;
}
}
.no-image {
width: 185px;
height: 270px;
background: $dark;
float: left;
box-shadow: 0 5px 10px 0 rgba(0,0,0,.5);
}
.item-content {
float: left;
width: 100%;
margin: 20px 0 0 0;
@include media(5) {
margin: 10px 0 0 0;
}
}
.item-year {
float: left;
color: #888;
font-size: 14px;
.dark & {
color: #626262;
}
}
.item-title {
color: $dark;
clear: both;
font-size: 17px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
text-decoration: none;
float: left;
.dark & {
color: #717171;
}
&:hover {
color: $main2;
}
&:active {
color: $main1;
}
}
.item-rating {
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
transform: translate(-50%, -50%);
box-shadow: 0 0 15px 0 rgba(0,0,0,.7);
border-radius: 25px;
.logged & {
cursor: pointer;
&:hover {
transform: translate(-50%, -50%) scale(1.2);
}
&:active {
transform: translate(-50%, -50%) scale(1.1);
}
@include transition(transform, background);
}
}
.rating-1 {
background: $rating1;
.icon-rating {
background: url(../../../public/assets/img/rating-1.png);
}
}
.rating-2 {
background: $rating2;
.icon-rating {
background: url(../../../public/assets/img/rating-2.png);
}
}
.rating-3 {
background: $rating3;
.icon-rating {
background: url(../../../public/assets/img/rating-3.png);
}
}
.icon-rating {
width: 50px;
height: 50px;
display: block;
}
.item-new {
background: $main1;
display: none;
@include media(3) {
display: block;
}
}
.icon-add {
background: url(../../../public/assets/img/add.png);
width: 50px;
height: 50px;
display: block;
}
.remove-item {
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
background: $rating3;
padding: 12px;
cursor: pointer;
.dark & {
background: darken($rating3, 10%);
}
@include transition(opacity);
&:hover {
opacity: 1;
}
@include media(3) {
// opacity: 1;
}
}
.icon-remove {
background: url(../../../public/assets/img/remove.png);
width: 17px;
height: 17px;
display: block;
}
.fade-enter-active {
transition: opacity .5s ease;
opacity: 1;
visibility: visible;
}
.fade-enter {
opacity: 0;
visibility: hidden;
}
.box {
float: left;
width: 100%;
h2 {
float: left;
width: 100%;
margin: 0 0 30px 0;
color: $main1;
}
}
.nothing-found {
float: left;
width: 100%;
font-size: 32px;
margin: 0 0 30px 0;
color: $dark;
}
.load-more-wrap {
float: left;
height: 100px;
position: relative;
width: 100%;
}
.load-more {
float: left;
width: 100%;
padding: 15px;
background: #e1e1e1;
color: $dark;
text-align: center;
font-size: 15px;
cursor: pointer;
.dark & {
background: #2f2f2f;
color: #626262;
}
&:active {
opacity: .8;
}
}

View File

@ -0,0 +1,61 @@
footer {
padding: 40px 0;
width: 100%;
float: left;
background: $main2;
background: linear-gradient(to right, $main1, $main2);
.dark & {
opacity: .9;
}
}
.attribution {
color: #fff;
float: left;
}
.icon-tmdb {
background: url(../../../public/assets/img/tmdb.png);
width: 139px;
height: 18px;
float: left;
margin: 3px 10px 0 0;
&:active {
opacity: .6;
}
}
.icon-github {
background: url(../../../public/assets/img/github.png);
width: 33px;
height: 27px;
float: right;
@include media(3) {
float: left;
clear: both;
margin: 30px 0 0 0;
}
&:active {
opacity: .6;
}
}
.sub-links {
float: left;
clear: both;
}
.login-btn {
float: left;
color: #fff;
text-decoration: none;
margin: 20px 20px 0 0;
&:active {
opacity: .6;
}
}

View File

@ -0,0 +1,92 @@
header {
background: $main2;
background: linear-gradient(to right, $main1, $main2);
width: 100%;
padding: 25px 0;
@include media(5) {
padding: 15px 0;
}
.dark & {
opacity: .9;
}
}
.logo {
float: left;
@include media(5) {
width: 80px;
height: auto;
margin: 7px 0 0 0;
img {
width: 100%;
height: auto;
}
}
}
.sort-wrap {
float: right;
margin: 4px 0 0 0;
width: auto;
.icon-sort-time,
.icon-sort-star {
float: left;
width: 32px;
height: 30px;
margin: 0 0 0 15px;
cursor: pointer;
opacity: .5;
@include transition(opacity);
&.active {
opacity: 1;
}
&:active {
opacity: .3;
}
&:first-child {
margin: 0;
}
}
}
.icon-sort-time {
background: url(../../../public/assets/img/sort-time.png);
}
.icon-sort-star {
background: url(../../../public/assets/img/sort-star.png);
}
.icon-constrast {
float: left;
width: 34px;
height: 34px;
margin: 0 0 0 30px;
cursor: pointer;
padding: 10px;
&:active {
opacity: .8;
}
i {
background: darken($dark, 20%);
border-radius: 100%;
width: 100%;
height: 100%;
float: left;
.dark & {
background: #fff;
}
}
}

View File

@ -0,0 +1,61 @@
.login-wrap {
lost-center: 320px 20px;
@include media(4) {
width: 100%;
}
}
.top-bar {
float: left;
width: 100%;
height: 30px;
background: $main2;
background: linear-gradient(to right, $main1, $main2);
}
.logo-login {
display: block;
margin: 30vh auto 50px auto;
}
.login-form {
float: left;
width: 100%;
input[type="text"],
input[type="password"] {
float: left;
width: 100%;
font-size: 15px;
margin: 0 0 5px 0;
background: #333;
padding: 12px;
color: #fff;
border: 0;
}
input[type="submit"] {
background: $main2;
background: linear-gradient(to right, $main1, $main2);
color: #fff;
font-size: 17px;
border: 0;
text-transform: uppercase;
padding: 8px 20px;
cursor: pointer;
}
}
.login-error {
float: left;
height: 20px;
width: 100%;
margin: 10px 0;
span {
float: left;
color: $rating3;
font-size: 14px;
}
}

View File

@ -0,0 +1,104 @@
.search-wrap {
float: left;
width: 100%;
position: absolute;
box-shadow: 0 0 70px 0 rgba(0, 0, 0, .3);
background: #fff;
opacity: .97;
.dark & {
background: #2f2f2f;
}
&.sticky {
@include media(sticky) {
position: fixed;
top: 0;
left: 0;
z-index: 10;
}
}
}
.search-form {
float: right;
width: 100%;
position: relative;
@include transition(padding);
.sticky & {
padding: 0 0 0 50px;
}
}
.search-input {
float: left;
background: transparent;
width: 100%;
border: 0;
font-size: 19px;
padding: 12px 0 12px 30px;
color: $dark;
.dark & {
color: #989898;
}
}
.icon-search {
background: url(../../../public/assets/img/search.png);
height: 20px;
width: 20px;
position: absolute;
left: 0;
top: 15px;
@include transition(left);
.dark & {
background: url(../../../public/assets/img/search-dark.png);
}
.sticky & {
left: 50px;
}
}
.icon-algolia {
background: url(../../../public/assets/img/algolia-white.png);
height: 25px;
width: 80px;
position: absolute;
right: 0;
top: 15px;
.dark & {
background: url(../../../public/assets/img/algolia-dark.png);
}
}
.icon-logo-small {
background: url(../../../public/assets/img/logo-small.png);
width: 32px;
height: 25px;
opacity: 0;
top: 12px;
left: -50px;
position: absolute;
@include transition(opacity, left);
.sticky & {
opacity: 1;
left: 0;
.dark & {
opacity: .9;
}
&:active {
opacity: .6;
}
}
}

70
client/webpack.config.js Normal file
View File

@ -0,0 +1,70 @@
const webpack = require('webpack');
const path = require('path');
const autoprefixer = require('autoprefixer');
const lost = require('lost');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
app: './app/app.js',
vendor: ['vue', 'vue-resource', 'vuex']
},
output: {
path: path.resolve('../public/assets'),
filename: 'app.js'
},
resolve: {
alias: {
vue: 'vue/dist/vue.js'
}
},
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
},
{
test: /\.(png|jpg|svg)$/,
loader: 'url',
query: {
limit: 10000,
name: 'img/[name].[ext]',
emitFile: false
}
},
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract('style', 'css!postcss!sass')
}
]
},
postcss() {
return [autoprefixer, lost];
},
plugins: [
new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js'),
new ExtractTextPlugin('app.css')
]
};
if(process.env.NODE_ENV === 'production') {
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new webpack.optimize.OccurenceOrderPlugin()
])
}

20
public/.htaccess Normal file
View File

@ -0,0 +1,20 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
RewriteEngine On
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>

1555
public/assets/app.css vendored Normal file

File diff suppressed because it is too large Load Diff

6225
public/assets/app.js Normal file

File diff suppressed because it is too large Load Diff

BIN
public/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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