Skip to content

Instantly share code, notes, and snippets.

@roberthamel
Created October 3, 2016 01:28
Show Gist options
  • Save roberthamel/890d4dae83b3a87d750f526af1c9cfb1 to your computer and use it in GitHub Desktop.
Save roberthamel/890d4dae83b3a87d750f526af1c9cfb1 to your computer and use it in GitHub Desktop.

Build "Project Flyer" With Me

purchasing the domain

app scenario

  • As a home owner
  • when i provide address, price, subscription
  • and i can drag and drop about 20 photos
  • then a landing page will get created when i save it.

domain service

  • hover.com
    • try shopify business name generator

initial setup

laravel new project-flyer
cd project-flyer/
git init
git add .
git commit -m "install laravel"
  • create a new database named projectflyer
  • hook database to laravel
php artisan key:generate

basic gulp housekeeping

npm install
  • import bootstrap by uncommenting @import statement in resources/assets/sass/app.sass
// App/Html/routes.php
<?php

Route::get('/', function() {
  return view('pages.home');
});
// Resources/views/layout.blade.php
<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>Project Flyer</title>
    <link rel="stylesheet" href="/css/app.css">
  </head>
  <body>
  
    <nav class="navbar navbar-inverse navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#">Project Flyer</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Home</a></li>
            <li><a href="#about">About</a></li>
            <li><a href="#contact">Contact</a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav>
  
    <div class="container">
      @yield('content')
    </div>
  
  </body>
</html>

initial scaffolding and routing

// Resources/Views/pages/home.blade.php
@extends('layout')

@section('content')
  <div class="jumbotron">
      <div class="container">
        <h1>Project Flyer</h1>
        <p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>
        <a href="flyers/create" class="btn btn-primary">Create a Flyer</a>
      </div>
    </div>
@stop
// Resources/assets/scss/app.scss
@import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap";

@import "partials/forms";

body {
  padding-top: 70px;
}
// resources/assets/scss/partials/forms.scss
.form-control {
  height: 39px;
}
// App/Html/routes.php
...
Route::resource('flyers', FlyersController)
php artisan make:controller FlyersController
// App/Html/Controllers/FlyersController.php
...
public function create() {
  return view('flyers.create');
}
// Resources/Views/flyers/create.blade.php
@extends('layout')

@section('content')
  <h1>Selling Your Home?</h1>
@stop

the flyer form

// Resources/Views/flyers/create.blade.php
@extends('layout')

@section('content')

  <h1>Selling Your Home?</h1>
  
  <hr>
  
  <div class="row">
    <!-- Required if you are going to submit any files through a form -->
    <form enctype="multipart/form-data" method="POST" action="/flyers" class="col-md-6">
      @if(count($errors) > 0)
        <div class="alert alert-danger">
          <ul>
            @foreach($errors->all() as $error)
              <li>{{ $error }}</li>
            @endforeach
          </ul>
        </div>
      @endif
      @include('flyers.form')
    </form>
  </div>

@stop
// resources/views/flyers/form.blade.php

@inject('countries' 'App\Http\Utilities\Country')

{{ csrf_field() }}

<div class="form-group">
  <label for="street">Street:</label>
  <input type="text" name="street" id="street" class="form-control" value="{{ old('street') }}" required/>
</div>

<div class="form-group">
  <label for="city">City:</label>
  <input type="text" name="city" id="city" class="form-control" value="{{ old('city') }}" required/>
</div>

<div class="form-group">
  <label for="zip">Zip/Postal Code:</label>
  <input type="text" name="zip" id="zip" class="form-control" value="{{ old('zip') }}" required />
</div>

<div class="form-group">
  <label for="country">Country:</label>
  <select id="country" name="country" class="form-control">
    @foreach($countries::all() as $country => $code)
      <option value="{{ $code }}">{{ $country }}</option>
    @endforeach
  </select>
</div>

<div class="form-group">
  <label for="state">State:</label>
  <input type="text" name="state" id="state" class="form-control" value="{{ old('state') }}" required/>
</div>

<hr>

<div class="form-group">
  <label for="price">Sale Price:</label>
  <input type="text" name="price" id="price" class="form-control" value="{{ old('price') }}" required/>
</div>

<div class="form-group">
  <label for="description">Name:</label>
  <textarea type="text" name="description" id="description" class="form-control" rows="10" required>
    {{ old('description')  }}
  </textarea>
</div>

<div class="form-group">
  <button type="submit" class="btn btn-primary">Create Flyer</button>
</div>
// App/Http/Utilities/Country.php
<?php 

namesapce App\Http\Utilities;

class Country {
  
  // take from https://gist.github.com/JeffreyWay/224de894c4d4e228d5af.js
  protected static $countries = [ ... ]
  
  public funtion all() {
    return static::$countries;
  }
}

migrations and models

  php artisan make:migration create_flyers_table --create="flyers
// database/migrations/..._create_flyers_table.php

//...
$table->string('street');
$table->string('city', 40);
$table->string('zip', 10);
$table->string('state', 40);
$table->string('country', 40);
$table->integer('price');
$table->text('description');
//...
php artisan migrate make:migration create_flyer_photos_table --create="flyer_photos
// database/migrations/..._create_flyer_photos_table.php

//...
$table->integer('flyer_id')->unsigned();

$table->foreign('flyer_id')->references('id')->on('flyers')->onDelete('cascade');
$table->string('path');
//...
php artisan migrate
php artisan make:model Flyer
// database/factories/ModelFactory.php
<?php

//...
 $factory->define(App\Flyer::class, function(faker\Generator $faker) {
   return [
     'street' => $faker->streetAddress
     'city' => $faker->city,
     'zip' => $faker->postcard,
     'state' => $faker->state,
     'country' => $faker->country,
     'price' => $faker->numberBetween(10000, 5000000),
     'description' => $faker->paragraph(3)
    ];
 })
php artisan tinker
>>> factory('App\Flyer')->make(); 
>>> factory('App\Flyer')->make()->street;

The Flyer and Photos Relation

// app/Flyer.php
//...
class Flyer extends Model {
  protected $fillable = [
    'street',
    'city',
    'state',
    'country',
    'zip',
    'price',
    'description'
  ];
  public function photos() {
    return $this->hasMany('App\Photo');
  }
}
php artisan make:model Photo
//app/Photo.php
use Symfony\Component\HttpFoundation\File\UploadedFile;
//...
class Photo extends Model {
  protected $table = 'flyer_photos';
  protected $fillable = ['path'];
  protected $baseDir = 'flyers/photos';
  public function flyer() {
    return $this->belongsTo('App\Flyer'); 
  }
  public static method fromForm(UploadedFile $file) {
    $photo = new static;
    $name = time() . $file->getClientOriginalName();
    $photo->path = $photo->baseDir.'/'.$name;
    $file->move($photo->baseDir, $name);
    return $photo;
  }
}
php artisan tinker
>>> $flyer = factory('App\Flyer')->make(); #creates a flyer in memory
>>> $flyer = factory('App\Flyer')->create(); #persists to database
>>> $flyer->photos()->create(['photo' => 'foo.jpg']); #creates a photo
>>> $flyer #displays the flyer
>>> $flyer->photos(); #displays the collection of photos
>>> App\Flyer::with('photos')->first(); #display flyer with photos
>>> App\Photo::all() #display all records in photo table

Validating and Persisting Flyers

php artisan make:request FlyerRequest
// app/Http/Requests/FlyerRequest.php
//...
public function authorize() {
  //does the user have authorization to make this request?
  return true;
}
public function rules() {
  return [
    //what are the rules?
    'street' => 'required',
    'city' => 'required',
    'zip' => 'required',
    'country' => 'required',
    'state' => 'required',
    'price' => 'required|integer',
    'description' => 'required'
  ];
}
// app/Http/Controllers/FlyersController.php
//...
use App\Http\Requests\FlyerRequest;
use App\Flyer;
//...
public function store(FlyerRequest $request) {
  //persist a flyer
  Flyer::create($request->all());
  //flash messaging
  
  //redirect to landing page
  return redirect()->back();
}
//...

Elegant Flash Messaging

Edit The composer.json File

{
    "autoload": {
      "files": [
        "app/helpers.php"
      ]
    }
}
composer dump-autoload
// app/helpers.php
<?php
function flash($title = null, $message = null) {
  $flash = app('App\Http\Flash');
  
  if(func_num_args() == 0) {
    return $flash;
  }
  
  return $flash->info($title, $message);
}

Use Sweetalert

Download zip from t4t5.github.io/sweetalert. Copy both sweetalert-dev.js and sweetalert.css to resources/assets:

  • copy sweetalert-dev.js to resources/assets/js/libs
  • copy sweetalert.css to resources/assets/css/libs
// gulpfile.js
//...
elixir(function(mix) {
  mix.sass('app.scss')
     .scripts([
       'libs/sweetalert-dev.js'
     ], './public/js/libs.js')
     .styles([
       'libs/styles.css'
     ], './public/css/libs.css')
});
# compile js and css
gulp
// resources/views/welcome.blade.php
//...
// add in <head>
<link rel="stylesheet" href="/css/libs.css">
// add to end of <body>
<script src="/js/libs.js">
// as a test to see if it works (remove upon confirmation)
<script type="text/javascript">
  swal({
    title: "Error!",
    text: "Here's my error message!",
    type: "error",
    confirmButtonText: "Cool"
  });
</script>
// app/Http/Flash.php
<?php
namespace App\Http;
class Flash {
  public function create($title, $message, $level, $key = 'flash_message') {
    return session()->flash($key, [
      'title' => $title,
      'message' => $message,
      'level' => $level,
    ]);
  }
  public function info($title, $message) {
    return $this->create($title, $message, 'info');
  }
  
  public function success($title, $message) {
    return $this->create($title, $message, 'success');
  }
  
  public function error($title, $message) {
    return $this->create($title, $message, 'error');
  }
  
  public function overlay($title, $message, $level = 'success') {
    return $this->create($title, $message, $level, 'flash_message_overlay');
  }
}
// app/Http/Controllers/FlyersController.php
//...
public function store(FlyerRequest $request() {
  Flyer::create($request->all());
  flash()->success('Success!', 'Your flyer has been created.'); // add this line
  return redirect()->back();
}
//...
public function create() {
  flash()->overlay('Welcome Aboard', 'Thank you for signing up.'); // add this line
  return view('flyers.create');
}
//...
// right after the last script tag
@include('flash')
//...
// resources/views/flash.blade.php
@if (session()->has('flash_message'))
    <script>
        swal({
            title: "{{ session('flash_message.title') }}",
            text: "{{ session('flash_message.message') }}",
            type: "{{ session('flash_message.level' }}",
            timer: 1700,
            showConfirmButton: false
        })
    </script>
@endif
@if (session()->has('flash_message_overlay'))
    <script>
        swal({
            title: "{{ session('flash_message_overlay.title') }}",
            text: "{{ session('flash_message_overlay.message') }}",
            type: "{{ session('flash_message_overlay.level' }}",
            confirmButtonText: 'Okay',
        })
    </script>
@endif

Preparing the Flyer Page

Create A New Flyer Using flyers/create URL

php artisan tinker
>>> App\Flyer::first();
# the flyer should have been written to the database
// app/Http/routes.php
//...
// place below Route::resource('flyers', 'FlyersController'); statement
Route::get('{zip}/{street}', ['as' => 'store_photo_path', 'uses' => 'FlyersController@show']);
//...
// app/Http/Controllers/FlyersController.php
public function show($zip, $street) {
  $flyer = Flyer::locatedAt($zip, $street);
  return view('flyers.show', compact('flyer'));
}
// app/Flyer.php
//...
public static function LocatedAt($zip, $street) {
  $street = str_replace('-', ' ', $street);
  return static::where(compact('zip', 'street'))-first();
}

public function getPriceAttribute($price) {
  return '$'.number_format($price);
}

public function addPhoto(Photo $photo) {
  return $this->photos()->save($photo);
}
//...

Now when you navigate to {zip}/{address} url, you will receive the json response.

// resources/views/flyers/show.blade.php
@extend('layout')

@section('content')
  <div class="row">
    <div class="col-md-3">
      <h1>{{ $flyer->street }}</h1>
      <h2>{!! $flyer->price !!}</h2>
    </div>
    <hr>
    <div class="description">{!! nl2br($flyer->description) !!}</div>
  </div>
  <div class="col-md-9">
    //...
  </div>
@stop
// tests/controllers/FlyersControllerTest.php
//...
/** @test */
class FlyersControllerTest extends Testcase {
  public function it_shows_the_form_to_create_a_new_flyer() {
    $this->visit('flyers/create');
  }
}

Bulk File Uploads

Download Dropzone. We'll use a CDN js and css.

// resources/views/layouts/layout.blade.php
<head>
  //...
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.0.1/dropzone.css">
  //...
</head>
<body>
  //...
  @yield('scripts.footer')
  //...
</body>
// resources/views/flyers/show.blade.php
//...
@section('content')
  <div class="row">
    //...
    <div class="col-md-9">
      @foreach($flyer->photos as $photo)
        <img src="{{ $photo->path }}" alt="">
      @endforeach
    </div>
  </div>
  <hr>
  <h2>Add Your Photos</h2>
  <form id="addPhotosForm" 
        action="{{route('store_photo_path', [
          $flyer->zip, 
          $flyer->street
        ])}}" 
        method="POST" 
        class="dropzone">
    {{ csrf_field() }}
  </form>
@stop
@section('scripts.footer')
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.0.1/dropzone.js"></script>
  <script>
    Dropzone.options.addPhotosForm = {
      paramName: 'photo',
      maxFilesize: 3,
      acceptedFiles: '.jpg, .jpeg, .png, .bmp',
      accept: 
    }
  </script>
@stop
// resources/assets/scss/app.scss
//...
img {
  max-width: 100%;
}
// app/Http/routes.php
//...
Route::post('{zip}/{street}/photos', 'FlyersController@addPhoto');
// app/Http/Controllers/FlyersController.php
use App\Photo;
//...
public function addPhoto($zip, $street, Request $request) {
  $this->validate($request, [
    'photo' => 'require|mimes:jpg,jpeg,png,bmp';
  ]);
  $photo = Photo::fromForm($request->file('photo'));
  Flyer::locatedAt($zip, $street)->addPhoto($photo);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment