Project Overview
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.
Key Features
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.
When to Use KOKOSHOP
- 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.
Live Demo
Explore a live instance (login with demo@kokoshop.test / password):
https://demo.kokoshop.example.com
Technology Stack
- 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
Quick Start
# 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 serve
Visit http://localhost:8000 to see your KOKOSHOP store in action.
Getting Started
Follow these steps to get a local copy running.
Prerequisites
- PHP ≥ 8.1
- Composer
- Node.js ≥ 16
- MySQL or PostgreSQL
1. Clone the Repository
git clone https://github.com/kevindluna/KOKOSHOP-Laravel.git
cd KOKOSHOP-Laravel
2. Install PHP & JS Dependencies
Install PHP packages via Composer:
composer install
Install frontend packages via npm:
npm install
3. Configure Environment
Copy and edit your .env
:
cp .env.example .env
Update 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:generate
4. Run Migrations & Seeders
Create tables and default data (roles + admin user):
php artisan migrate
php artisan db:seed
To reset and re-seed in one command:
php artisan migrate:fresh --seed
5. Build Assets & Start Dev Server
Start the frontend dev server with Vite (auto-refresh):
npm run dev
Compile production assets:
npm run build
Serve the backend on port 8000:
php artisan serve
Visit http://127.0.0.1:8000 in your browser.
6. Common Artisan Commands
php artisan route:list
List all registered routesphp artisan config:cache
Cache configuration filesphp artisan cache:clear
Clear application cachephp artisan migrate:rollback
Rollback last batch of migrationsphp artisan db:seed --class=AdminUserSeeder
Seed only the admin userphp artisan make:controller MyController
Generate a new controllerphp artisan make:model MyModel -m
Generate model with migration
Use these commands to manage and inspect your application during development.
Domain Models & Database Schema
This section details core business entities and their database mappings. It covers key fields, foreign-key relationships, cascade rules, and common Eloquent usage patterns.
Rol & User
Rol Model
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');
}
}
User Model
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";
}
Producto & CantidadTalla
Producto Model
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');
}
}
CantidadTalla Model
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.
Venta & ProductosVenta
Venta Model
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');
}
}
ProductosVenta Model
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_venta
uses a composite primary key on (Id_venta
,Id_producto
).- Deleting a
Venta
cascades to itsproductos_venta
entries.
Application Workflows & Routing
Map user journeys from incoming URLs through route definitions, controller actions, model interactions, and view responses.
1. Authentication Flow
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');
2. User Management (CRUD)
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} → destroy
Flow Example: Viewing a user list
- GET
/users
- UserController@index
• Fetches$users = User::paginate(15);
• Returns viewusers.index
with 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'));
}
3. Product Management (CRUD)
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);
}
4. Sales Workflow
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']);
}
});
5. Public Pages
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.
Frontend & UI Customization
Customize global styles, the main Blade layout, and the Vite build to shape your storefront’s look and feel.
SCSS Variable Overrides & Global Styles
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 build
Practical Tips
- Always import
_variables.scss
before Bootstrap. - Use partials (
_mixins.scss
,_components.scss
) for organization. - Enable source maps in Vite (default) to trace variable values.
Blade Layout Modification
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
@vite
to 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')
).
Vite Configuration for Asset Bundling
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
input
array. - For code splitting: dynamic
import('./path')
insideapp.js
. - Use
resolve.alias
in 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>
Pest
If you prefer Pest syntax:
# Run Pest tests
vendor/bin/pest
Pest uses the same test directories and bootstrap logic as PHPUnit.
Writing Tests
Unit Tests
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);
}
}
Feature Tests
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' => 'jane@example.com',
'password' => bcrypt('secret'),
]);
$response = $this->postJson('/login', [
'email' => $user->email,
'password' => 'secret',
]);
$response->assertStatus(200)
->assertJsonStructure(['token']);
}
}
Seeding Demo Data
Use model factories to generate realistic demo data before manual testing or CI builds.
UserFactory Usage
use App\Models\User;
// Create 10 verified users
User::factory()->count(10)->create();
// Create 3 unverified users
User::factory()->count(3)->unverified()->create();
Running Seeders
In DatabaseSeeder.php
:
public function run()
{
\App\Models\User::factory()->count(20)->create();
}
Then:
php artisan migrate:fresh --seed
Code Style
We follow PSR-12 and use PHP CS Fixer to enforce consistency.
Checking & Fixing
# 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.php
Integrate into CI or pre-commit hooks to ensure compliance.
Issue & PR Guidelines
- 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
main
branch. Maintain clear commit messages and follow PSR-12.