KOKOSHOP is a ready-to-deploy e-commerce starter built on Laravel and Vite. It delivers a modern storefront with a dynamic product catalog, user accounts, purchase workflows, and an admin dashboard for managing inventory and sales.
-
Product Catalog
Browse and filter featured items. Home page uses a full-screen video background and Blade templates to render the latest products. -
User Accounts & Authentication
Register, login, reset passwords. Protect routes via Laravel’s built-in auth middleware. -
Purchase Flow
Add to cart, checkout, order history. Sales resources handle CRUD operations and order management. -
Admin Dashboard
Manage users, products, and sales through resourceful controllers and Blade views.
- You need a Laravel-based e-commerce prototype with production-ready defaults.
- You prefer Blade views with minimal JavaScript overhead.
- You want a base you can extend—add payment gateways, search, or PWA support.
Explore a live instance (login with [email protected] / password):
https://demo.kokoshop.example.com
- Backend: Laravel 10, PHP 8
- Frontend: Vite, Bootstrap 5, Axios, Sass
- Routing: routes/web.php defines public and resource routes for products, sales, users
- Build Tools:
- composer.json for PHP dependencies and post-install scripts
- package.json for npm scripts, Vite, and frontend plugins
# Clone and install PHP dependencies
git clone https://github.com/kevindluna/KOKOSHOP-Laravel.git
cd KOKOSHOP-Laravel
composer install
# Install JS dependencies and compile assets
npm install
npm run dev
# Configure environment
cp .env.example .env
php artisan key:generate
# Set database credentials in .env
# Run migrations and seed demo data
php artisan migrate --seed
# Serve locally
php artisan serveVisit http://localhost:8000 to see your KOKOSHOP store in action.
Follow these steps to get a local copy running.
- PHP ≥ 8.1
- Composer
- Node.js ≥ 16
- MySQL or PostgreSQL
git clone https://github.com/kevindluna/KOKOSHOP-Laravel.git
cd KOKOSHOP-LaravelInstall PHP packages via Composer:
composer installInstall frontend packages via npm:
npm installCopy and edit your .env:
cp .env.example .envUpdate database settings in .env:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kokoshop
DB_USERNAME=root
DB_PASSWORD=secret
Generate application key:
php artisan key:generateCreate tables and default data (roles + admin user):
php artisan migrate
php artisan db:seedTo reset and re-seed in one command:
php artisan migrate:fresh --seedStart the frontend dev server with Vite (auto-refresh):
npm run devCompile production assets:
npm run buildServe the backend on port 8000:
php artisan serveVisit http://127.0.0.1:8000 in your browser.
php artisan route:listList all registered routesphp artisan config:cacheCache configuration filesphp artisan cache:clearClear application cachephp artisan migrate:rollbackRollback last batch of migrationsphp artisan db:seed --class=AdminUserSeederSeed only the admin userphp artisan make:controller MyControllerGenerate a new controllerphp artisan make:model MyModel -mGenerate model with migration
Use these commands to manage and inspect your application during development.
This section details core business entities and their database mappings. It covers key fields, foreign-key relationships, cascade rules, and common Eloquent usage patterns.
Maps to roles table. Grants mass assignment on the rol column and defines its users.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Rol extends Model
{
use HasFactory;
protected $table = 'roles';
protected $fillable = ['rol'];
/** Users assigned to this role */
public function users()
{
return $this->hasMany(User::class, 'id_rol');
}
}Maps to users table. Handles authentication fields, assigns a role, and links to sales as client or employee.
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class User extends Authenticatable
{
use HasFactory;
protected $fillable = [
'name', 'lastname', 'email', 'password', 'id_rol'
];
protected $hidden = ['password', 'remember_token'];
protected $casts = ['email_verified_at' => 'datetime'];
/** Role assigned to the user */
public function rol()
{
return $this->belongsTo(Rol::class, 'id_rol');
}
/** Sales where user is the client */
public function ventasComoCliente()
{
return $this->hasMany(Venta::class, 'Id_cliente');
}
/** Sales where user is the employee */
public function ventasComoEmpleado()
{
return $this->hasMany(Venta::class, 'Id_empleado');
}
}Usage Example
Eager-load roles and sales to avoid N+1:
$users = User::with(['rol', 'ventasComoCliente', 'ventasComoEmpleado'])->get();
foreach ($users as $user) {
echo "{$user->name} ({$user->rol->rol}) has made {$user->ventasComoCliente->count()} purchases\n";
}Represents the productos table. Manages one-to-many link to size quantities.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Producto extends Model
{
use HasFactory;
protected $table = 'productos';
protected $primaryKey = 'Id_producto';
protected $fillable = [
'nombre', 'descripcion', 'precio', // add other fields as needed
];
/** All size–quantity records for this product */
public function tallas()
{
return $this->hasMany(CantidadTalla::class, 'Id_producto', 'Id_producto');
}
}Represents the cantidad_talla table. Tracks inventory per size.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class CantidadTalla extends Model
{
use HasFactory;
protected $table = 'cantidad_talla';
public $timestamps = false;
protected $fillable = ['Id_producto', 'talla', 'cantidad'];
/** Associated product */
public function producto()
{
return $this->belongsTo(Producto::class, 'Id_producto', 'Id_producto');
}
}Common Operations
-
Retrieve product with all sizes
$product = Producto::with('tallas')->find($id); foreach ($product->tallas as $t) { echo "Size {$t->talla}: {$t->cantidad}\n"; }
-
Add a new size
$product->tallas()->create([ 'talla' => 'XL', 'cantidad' => 15, ]);
-
Update quantity
$tallaM = $product->tallas()->where('talla', 'M')->first(); $tallaM->increment('cantidad', 5);
-
Delete a size entry
$product->tallas()->where('talla', 'XS')->delete();
Cascade: Deleting a Producto cascades to its cantidad_talla records via the onDelete('cascade') foreign key.
Maps to ventas table and connects clients, employees, and sold products.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Venta extends Model
{
use HasFactory;
protected $table = 'ventas';
protected $primaryKey = 'Id_venta';
protected $fillable = [
'Id_cliente', 'Id_empleado', 'fecha', 'total'
];
/** Client who made the sale */
public function cliente()
{
return $this->belongsTo(User::class, 'Id_cliente');
}
/** Employee who handled the sale */
public function empleado()
{
return $this->belongsTo(User::class, 'Id_empleado');
}
/** Pivot records linking products to this sale */
public function productos()
{
return $this->hasMany(ProductosVenta::class, 'Id_venta', 'Id_venta');
}
}Represents the productos_venta pivot table. Captures quantity and size per product sale.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class ProductosVenta extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'productos_venta';
protected $fillable = [
'Id_venta', 'Id_producto', 'cantidad_producto', 'talla_producto'
];
/** The sale this record belongs to */
public function venta()
{
return $this->belongsTo(Venta::class, 'Id_venta', 'Id_venta');
}
/** The product sold */
public function producto()
{
return $this->belongsTo(Producto::class, 'Id_producto', 'Id_producto');
}
}Pivot Operations
-
Add product to a sale
$venta = Venta::find($idVenta); $venta->productos()->create([ 'Id_producto' => $idProducto, 'cantidad_producto' => 2, 'talla_producto' => 'M', ]);
-
Retrieve sale with product details
$venta = Venta::with('productos.producto')->find($idVenta); foreach ($venta->productos as $pv) { echo "{$pv->producto->nombre}: {$pv->cantidad_producto} × {$pv->talla_producto}\n"; }
-
Update pivot record
$pv = ProductosVenta::where([ ['Id_venta', $idVenta], ['Id_producto', $idProducto], ])->first(); $pv->update(['cantidad_producto' => 5, 'talla_producto' => 'L']);
-
Remove product from sale
$venta->productos()->where('Id_producto', $idProducto)->delete();
Schema Notes:
productos_ventauses a composite primary key on (Id_venta,Id_producto).- Deleting a
Ventacascades to itsproductos_ventaentries.
Map user journeys from incoming URLs through route definitions, controller actions, model interactions, and view responses.
Routes (routes/web.php)
Auth::routes();
// Expands to:
// GET /login → Auth\LoginController@showLoginForm
// POST /login → Auth\LoginController@login
// POST /logout → Auth\LoginController@logout
// GET /register → Auth\RegisterController@showRegistrationForm
// POST /register → Auth\RegisterController@register
// GET /password/reset → Auth\ForgotPasswordController@showLinkRequestForm
// …other password routes…Request Flow
- User visits
/login - LoginController@showLoginForm returns
resources/views/auth/login.blade.php - On submit, LoginController@login validates credentials, issues session, and redirects to
/home
Example: guarding a route
Route::get('/dashboard', 'DashboardController@index')
->middleware('auth')
->name('dashboard');Routes
Route::resource('users', 'UserController');
// Maps:
// GET /users → index
// GET /users/create → create
// POST /users → store
// GET /users/{user} → show
// GET /users/{user}/edit → edit
// PUT/PATCH /users/{user} → update
// DELETE /users/{user} → destroyFlow Example: Viewing a user list
- GET
/users - UserController@index
• Fetches$users = User::paginate(15);
• Returns viewusers.indexwith compact('users') - Blade renders table, links to edit/delete
Code snippet (index):
public function index()
{
$users = User::orderBy('created_at','desc')->paginate(15);
return view('users.index', compact('users'));
}Routes
Route::resource('productos', 'ProductosController');
// Named routes: productos.index, productos.create, …Flow Example: Creating a product
- GET
/productos/create→ ProductosController@create → viewproductos.create - POST
/productos→ ProductosController@store
• Validates fields & tallas array
• Calculates total stock
• Persists Producto and CantidadTalla records
• Redirects to productos.show
Key store logic:
public function store(Request $request)
{
$validated = $request->validate([
'nombre' => 'required|string|max:255',
'precio' => 'required|integer',
'tipo_producto' => 'required|string',
'tallas' => 'required|array',
'tallas.*.talla' => 'required|string',
'tallas.*.cantidad' => 'required|integer',
]);
// sum quantities
$cantidadTotal = array_sum(array_column($validated['tallas'], 'cantidad'));
$producto = Producto::create(array_merge($validated, ['cantidad_total' => $cantidadTotal]));
foreach ($validated['tallas'] as $t) {
CantidadTalla::create([
'Id_producto' => $producto->Id_producto,
'talla' => $t['talla'],
'cantidad' => $t['cantidad'],
]);
}
return redirect()->route('productos.show', $producto);
}Routes
Route::resource('ventas', 'VentaController');
// Additional form routes if needed:
Route::get('comprar/{producto}', 'CompraController@mostrarFormulario')->name('compra.form');
Route::post('comprar', 'CompraController@realizarCompra')->name('compra.submit');Flow: “Comprar ahora”
- GET
/comprar/5(producto ID) → CompraController@mostrarFormulario
• Loads product, tallas, returnscompraFormulario.blade.php - POST
/comprar→ CompraController@realizarCompra
• Validates client/employee IDs, fecha, productos array
• Wraps in DB::transaction():
– Calculates total
– Creates Venta record
– Inserts ProductosVenta
– Updates CantidadTalla stock
• Commits and returns viewcompraRealizada
Core transaction snippet:
DB::transaction(function() use ($validated) {
$total = collect($validated['productos'])
->reduce(function($sum, $item) {
$p = Producto::findOrFail($item['Id_producto']);
return $sum + ($p->precio * $item['cantidad_producto']);
}, ($validated['tipo_venta']==='Virtual'?17000:0));
$venta = Venta::create([...,'precio_Total'=>$total]);
foreach ($validated['productos'] as $item) {
ProductosVenta::create([...]);
CantidadTalla::where('Id_producto',$item['Id_producto'])
->where('talla',$item['talla_producto'])
->decrement('cantidad',$item['cantidad_producto']);
}
});Routes
Route::get('/catalogo', 'CatalogController@index')->name('catalogo');
Route::get('/producto/{Id_producto}', 'CatalogController@show')->name('producto.show');
Route::get('/nosotros', 'PublicController@about')->name('about');
Route::get('/asesoramiento', 'PublicController@advisoryForm')->name('advisory.form');
Route::post('/asesoramiento/enviar', 'PublicController@sendAdvisory')->name('advisory.send');Catalog flow:
- GET
/catalogo→ CatalogController@index
• FetchesProducto::with('tallas')->paginate(12)
• Returnscatalogo.blade.php - GET
/producto/7→ CatalogController@show
• Loads single Producto + stock per talla
• ReturnsinterfazProducto.blade.php
Advisory form:
public function sendAdvisory(Request $req)
{
$data = $req->validate([
'nombre' => 'required',
'email' => 'required|email',
'consulta'=> 'required|string',
]);
Advisory::create($data);
return back()->with('status','Consulta enviada con éxito');
}This mapping ensures clear, maintainable user journeys from URL to final response.
Customize global styles, the main Blade layout, and the Vite build to shape your storefront’s look and feel.
Override Bootstrap’s defaults, import fonts and define global rules in resources/sass/app.scss.
- Create your overrides partial at
resources/sass/_variables.scss
// resources/sass/_variables.scss
// Colors
$primary: #1d4ed8;
$secondary: #9333ea;
$body-bg: #f3f4f6;
// Typography
$font-family-sans-serif: 'Nunito', sans-serif;
$font-size-base: 0.95rem;
$line-height-base: 1.6;- Update
resources/sass/app.scss
// Load Nunito from BunnyCDN
@import url('https://fonts.bunny.net/css?family=Nunito:400,600,700');
// Apply your overrides before Bootstrap
@import 'variables';
@import 'bootstrap/scss/bootstrap';
// Global styles
body {
font-family: $font-family-sans-serif;
background-color: $body-bg;
color: $gray-800;
}
// Example: custom button
.btn-custom {
@include button-variant($primary, #fff);
border-radius: .5rem;
padding: .75rem 1.5rem;
&:hover { background-color: darken($primary, 8%); }
}- Rebuild your CSS
npm run dev # or npm run buildPractical Tips
- Always import
_variables.scssbefore Bootstrap. - Use partials (
_mixins.scss,_components.scss) for organization. - Enable source maps in Vite (default) to trace variable values.
Tailor resources/views/layouts/app.blade.php to inject your branding, navigation links and social footer.
<!DOCTYPE html>
<html lang="{{ str_replace('_','-',app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>@yield('title', 'KOKOSHOP')</title>
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body class="flex flex-col min-h-screen bg-gray-100">
<nav class="bg-white shadow">
<div class="container mx-auto px-4 flex justify-between h-16">
<a href="{{ route('home') }}" class="flex items-center space-x-2">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="h-8">
<span class="font-semibold text-xl">KOKOSHOP</span>
</a>
<div class="flex items-center space-x-4">
@guest
<a href="{{ route('login') }}" class="text-gray-700">Login</a>
<a href="{{ route('register') }}" class="text-gray-700">Register</a>
@else
<x-dropdown align="right">
<x-slot name="trigger">
<button class="text-gray-700">{{ Auth::user()->name }}</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link href="{{ route('profile') }}">Profile</x-dropdown-link>
<x-dropdown-link href="{{ route('logout') }}"
onclick="event.preventDefault();document.getElementById('logout').submit();">
Logout
</x-dropdown-link>
<form id="logout" action="{{ route('logout') }}" method="POST" class="hidden">@csrf</form>
</x-slot>
</x-dropdown>
@endguest
</div>
</div>
</nav>
<main class="flex-grow container mx-auto px-4 py-6">
@yield('content')
</main>
<footer class="bg-white border-t">
<div class="container mx-auto px-4 py-4 flex justify-between text-gray-600">
<span>© {{ date('Y') }} KOKOSHOP</span>
<div class="space-x-4">
<a href="/contact" class="underline">Contact</a>
<a href="https://github.com/kevindluna/KOKOSHOP-Laravel" target="_blank">GitHub</a>
</div>
</div>
</footer>
</body>
</html>Practical Tips
- Use
@viteto include your compiled CSS/JS. - Leverage Blade components (
x-dropdown-link) for reusable UI. - Keep nav and footer markup DRY by extracting into includes (
@include('layouts.nav')).
Manage your frontend build in vite.config.js via the laravel-vite-plugin.
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/sass/app.scss',
'resources/js/app.js',
// add new entries here:
// 'resources/js/producto.js',
// 'resources/sass/store.scss',
],
refresh: true,
}),
],
server: {
hmr: { host: 'localhost' }
},
});Extending Your Build
- Add new JS/SCSS files to the
inputarray. - For code splitting: dynamic
import('./path')insideapp.js. - Use
resolve.aliasin Vite for shorter import paths.
Rebuild & Watch
npm run dev # watches files, triggers live reload
npm run build # production-ready assets
## Contribution & Testing
This section covers running and writing tests, seeding demo data, enforcing code style, and submitting issues or pull requests.
### Running Tests
#### PHPUnit
Run the full test suite with environment settings from **phpunit.xml**:
```bash
# Run all tests
vendor/bin/phpunit
# Run only unit tests
vendor/bin/phpunit --testsuite="Unit"
# Run only feature tests
vendor/bin/phpunit --testsuite="Feature"phpunit.xml defines:
<testsuites>
<testsuite name="Unit">
<directory>./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>./tests/Feature</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- other testing env -->
</php>If you prefer Pest syntax:
# Run Pest tests
vendor/bin/pestPest uses the same test directories and bootstrap logic as PHPUnit.
Place under tests/Unit. Focus on isolated class logic without HTTP or database.
<?php
namespace Tests\Unit;
use App\Models\User;
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function test_name_is_mutated_correctly()
{
$user = new User();
$user->name = 'john doe';
$this->assertEquals('John Doe', $user->name);
}
}Place under tests/Feature. Use Laravel’s HTTP and database helpers.
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthenticationTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_login_with_valid_credentials()
{
// Seed a verified user
$user = User::factory()->create([
'email' => '[email protected]',
'password' => bcrypt('secret'),
]);
$response = $this->postJson('/login', [
'email' => $user->email,
'password' => 'secret',
]);
$response->assertStatus(200)
->assertJsonStructure(['token']);
}
}Use model factories to generate realistic demo data before manual testing or CI builds.
use App\Models\User;
// Create 10 verified users
User::factory()->count(10)->create();
// Create 3 unverified users
User::factory()->count(3)->unverified()->create();In DatabaseSeeder.php:
public function run()
{
\App\Models\User::factory()->count(20)->create();
}Then:
php artisan migrate:fresh --seedWe follow PSR-12 and use PHP CS Fixer to enforce consistency.
# Check coding style (no changes)
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff
# Apply fixes
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.phpIntegrate into CI or pre-commit hooks to ensure compliance.
- Fork the repository and branch from
main. - Reference an existing issue or open a new one using the template.
- In your PR description:
- Summarize changes
- Link related issues (
Fixes #123) - List added/updated tests
- Confirm style checks and tests pass
- Run:
vendor/bin/phpunit && vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run - Submit your PR against the
mainbranch. Maintain clear commit messages and follow PSR-12.