Initial commit

This commit is contained in:
2026-10-31 13:24:24 +01:00
commit e6805f2245
87 changed files with 16180 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4
+65
View File
@@ -0,0 +1,65 @@
APP_NAME="Jouwdriedee.nl"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://jouwdriedee.nl
APP_LOCALE=nl
APP_FALLBACK_LOCALE=nl
APP_FAKER_LOCALE=nl_NL
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+27
View File
@@ -0,0 +1,27 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.codex
/.cursor/
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/fonts-manifest.dev.json
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db
+2
View File
@@ -0,0 +1,2 @@
ignore-scripts=true
audit=true
+47
View File
@@ -0,0 +1,47 @@
<laravel-boost-guidelines>
# Laravel Application
This repository contains a Laravel application. Complete the following setup before working on the user's request.
## Prerequisites
Verify that PHP and Composer are available:
```sh
php -v
composer -V
```
If either command is unavailable, detect the user's operating system and install the prerequisites with the appropriate command:
macOS:
```sh
/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.5)"
```
Windows PowerShell:
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.5'))
```
Linux:
```sh
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
```
After installation, ask the user to restart their terminal. If the agent needs the restarted shell to continue, ask the user to reopen their terminal and rerun their original prompt.
## Agent Setup
Install Laravel Boost from the application root before making application changes:
```sh
composer require laravel/boost --dev
php artisan boost:install
```
Boost replaces these bootstrap instructions with guidelines tailored to the application. After installation, read `AGENTS.md` again and continue with the user's original request using the generated guidelines.
</laravel-boost-guidelines>
+47
View File
@@ -0,0 +1,47 @@
<laravel-boost-guidelines>
# Laravel Application
This repository contains a Laravel application. Complete the following setup before working on the user's request.
## Prerequisites
Verify that PHP and Composer are available:
```sh
php -v
composer -V
```
If either command is unavailable, detect the user's operating system and install the prerequisites with the appropriate command:
macOS:
```sh
/bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.5)"
```
Windows PowerShell:
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.5'))
```
Linux:
```sh
/bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.5)"
```
After installation, ask the user to restart their terminal. If the agent needs the restarted shell to continue, ask the user to reopen their terminal and rerun their original prompt.
## Agent Setup
Install Laravel Boost from the application root before making application changes:
```sh
composer require laravel/boost --dev
php artisan boost:install
```
Boost replaces these bootstrap instructions with guidelines tailored to the application. After installation, read `AGENTS.md` again and continue with the user's original request using the generated guidelines.
</laravel-boost-guidelines>
+52
View File
@@ -0,0 +1,52 @@
# ==========================================
# Multi-stage Dockerfile voor Jouwdriedee.nl (Coolify / CasaOS)
# ==========================================
# 1. Frontend Build stage (Vite & Tailwind CSS)
FROM node:20-alpine AS node_builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 2. PHP Production runtime stage
FROM dunglas/frankenphp:php8.3-alpine AS runner
ENV SERVER_NAME="http://"
ENV APP_ENV="production"
ENV APP_DEBUG="false"
WORKDIR /app
# Installeer benodigde PHP extensies voor Laravel & SQLite/MySQL
RUN install-php-extensions \
pdo_mysql \
pdo_sqlite \
sqlite3 \
gd \
intl \
zip \
bcmath \
opcache
# Installeer Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Kopieer applicatiebestanden
COPY . /app
COPY --from=node_builder /app/public/build /app/public/build
# Installeer backend dependencies
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
# Zorg voor de juiste opslagpermissies en database bestand
RUN mkdir -p /app/storage /app/bootstrap/cache /app/database \
&& touch /app/database/database.sqlite \
&& chown -R www-data:www-data /app/storage /app/bootstrap/cache /app/database \
&& chmod -R 775 /app/storage /app/bootstrap/cache /app/database
EXPOSE 80
CMD ["sh", "-c", "php artisan storage:link && php artisan migrate --force && frankenphp run --config /etc/caddy/Caddyfile"]
+86
View File
@@ -0,0 +1,86 @@
# 🚀 Jouwdriedee.nl — 3D Print Webshop & Maatwerk Studio
Welkom bij de broncode van **[jouwdriedee.nl](https://jouwdriedee.nl)**, een moderne webshop en maatwerkservice voor 3D-geprinte artikelen.
---
## 🛠️ Tech Stack
* **Framework:** Laravel 11 (PHP 8.3+)
* **Frontend:** Tailwind CSS v4 + Alpine.js + Lucide Icons + Vite
* **Database:** SQLite (standaard out-of-the-box, volume-persistent) of PostgreSQL / MySQL
* **Hosting & CI/CD:** CasaOS + Gitea + Coolify
---
## 🌟 Belangrijkste Features
1. **Geavanceerde 3D Print Catalogus:**
* Categorieën: Home & Interieur, Bureau & Tech, Gereedschap, Gadgets & Speelgoed, Custom prints.
* Filters op materiaal (PLA+, PETG, TPU Flexibel, Resin) en sortering.
2. **Interactieve 3D Product Configurator:**
* Live materiaalkiezer met realtime prijscalculator (verschillende toeslagen per filamenttype).
* Kleur-swatches met visuele indicators.
* Schaal & afmetingenkiezer (100%, 150%, 200%).
3. **Eigen STL / CAD Upload Service:**
* Klanten kunnen rechtstreeks hun eigen `.stl`, `.step` of `.obj` bestand uploaden met gewenste specificaties voor een vrijblijvende offerte.
4. **Winkelwagen & Afrekenmodule:**
* Overzichtelijke winkelwagen met live calculatie en gratis verzending drempelwaarde (€ 50,-).
* Geoptimaliseerd afrekenformulier met ondersteuning voor iDEAL, Bancontact en handmatige overboeking.
* Live **Print & Productie Status Tracker** (Ontvangen → Slicing & Printwachtrij → 3D Printen → Kwaliteitscontrole & Verzenden).
---
## 💻 Lokale Ontwikkeling (Windows)
### 1. Repository clonen & dependencies
```bash
# Ga naar de map
cd c:\Development\jouwdriedee
# Installeer backend dependencies
composer install
# Installeer frontend dependencies
npm install
```
### 2. Database & Migraties
```bash
# Voer migraties en demonstratie-data uit (categorieën, materialen, kleuren, producten)
php artisan migrate --seed
```
### 3. Ontwikkelserver starten
```bash
# Terminal 1: Laravel webserver (standaard op http://localhost:8000)
php artisan serve
# Terminal 2: Vite Hot-Module-Replacement
npm run dev
```
---
## 🐳 Deployment op Coolify (CasaOS + Gitea)
Coolify detecteert dit project automatisch dankzij de meegeleverde `nixpacks.toml` en `Dockerfile`.
### Stap-voor-stap inrichten in Coolify:
1. Open je **Coolify Dashboard**.
2. Klik op **+ New Resource** &rarr; **Application** &rarr; **Public or Private Git Repository**.
3. Vul de Gitea URL in: `https://git.webenpcstudio.nl/NiekRabelink/jouwdriedee.git` (en selecteer je Gitea credentials).
4. **Build Pack:** Laat op **Nixpacks** staan (of kies **Dockerfile**).
5. **Environment Variables (`.env`) in Coolify:**
* `APP_NAME="Jouwdriedee.nl"`
* `APP_ENV=production`
* `APP_DEBUG=false`
* `APP_URL=https://jouwdriedee.nl`
* `APP_KEY` *(genereer een sleutel met `php artisan key:generate --show`)*
* `DB_CONNECTION=sqlite`
6. **Persistent Storage (Volumes) in Coolify:**
* Koppel een volume voor de database en uploads zodat data behouden blijft bij nieuwe deployments:
* `/app/database` &rarr; persistente SQLite database
* `/app/storage/app/public` &rarr; geüploade 3D bestanden
7. **Domein:** Koppel `https://jouwdriedee.nl`. Coolify regelt automatisch het SSL-certificaat (Let's Encrypt).
8. Klik op **Deploy**!
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers;
use App\Models\Material;
use App\Models\Product;
use Illuminate\Http\Request;
class CartController extends Controller
{
public function index()
{
$cart = session()->get('cart', []);
$subtotal = 0;
foreach ($cart as $item) {
$subtotal += $item['price'] * $item['quantity'];
}
$shipping = $subtotal > 50 || empty($cart) ? 0.00 : 4.95;
$total = $subtotal + $shipping;
return view('cart.index', compact('cart', 'subtotal', 'shipping', 'total'));
}
public function add(Request $request)
{
$validated = $request->validate([
'product_id' => 'required|exists:products,id',
'material' => 'required|string',
'color' => 'required|string',
'scale' => 'nullable|string',
'infill' => 'nullable|string',
'quantity' => 'required|integer|min:1|max:50',
]);
$product = Product::findOrFail($validated['product_id']);
$material = Material::where('code', $validated['material'])->first();
// Prijs berekening inclusief materiaal vermenigvuldiger & schaal
$unitPrice = $product->base_price;
if ($material) {
$unitPrice *= $material->price_multiplier;
}
// Schaal aanpassing
$scale = $validated['scale'] ?? '100%';
if ($scale === '150%') {
$unitPrice *= 1.45;
} elseif ($scale === '200%') {
$unitPrice *= 1.95;
}
$unitPrice = round($unitPrice, 2);
// Unieke sleutel in sessie op basis van product + configuratie
$cartKey = md5($product->id . '_' . $validated['material'] . '_' . $validated['color'] . '_' . $scale);
$cart = session()->get('cart', []);
if (isset($cart[$cartKey])) {
$cart[$cartKey]['quantity'] += (int)$validated['quantity'];
} else {
$cart[$cartKey] = [
'id' => $product->id,
'name' => $product->name,
'slug' => $product->slug,
'image_url' => $product->image_url,
'material' => $material ? $material->name : $validated['material'],
'material_code' => $validated['material'],
'color' => $validated['color'],
'scale' => $scale,
'infill' => $validated['infill'] ?? 'Standaard (20%)',
'price' => $unitPrice,
'quantity' => (int)$validated['quantity'],
];
}
session()->put('cart', $cart);
if ($request->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Artikel toegevoegd aan je winkelwagen!',
'cartCount' => array_sum(array_column($cart, 'quantity')),
]);
}
return redirect()->route('cart.index')->with('success', "{$product->name} is toegevoegd aan je winkelwagen!");
}
public function update(Request $request)
{
$validated = $request->validate([
'key' => 'required|string',
'quantity' => 'required|integer|min:1|max:50',
]);
$cart = session()->get('cart', []);
if (isset($cart[$validated['key']])) {
$cart[$validated['key']]['quantity'] = (int)$validated['quantity'];
session()->put('cart', $cart);
}
return redirect()->route('cart.index')->with('success', 'Winkelwagen bijgewerkt.');
}
public function remove($key)
{
$cart = session()->get('cart', []);
if (isset($cart[$key])) {
unset($cart[$key]);
session()->put('cart', $cart);
}
return redirect()->route('cart.index')->with('success', 'Artikel verwijderd uit je winkelwagen.');
}
public function count()
{
$cart = session()->get('cart', []);
$totalItems = array_sum(array_column($cart, 'quantity'));
return response()->json(['count' => $totalItems]);
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Models\OrderItem;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class CheckoutController extends Controller
{
public function index()
{
$cart = session()->get('cart', []);
if (empty($cart)) {
return redirect()->route('cart.index')->with('error', 'Je winkelwagen is nog leeg.');
}
$subtotal = 0;
foreach ($cart as $item) {
$subtotal += $item['price'] * $item['quantity'];
}
$shipping = $subtotal > 50 ? 0.00 : 4.95;
$total = $subtotal + $shipping;
return view('checkout.index', compact('cart', 'subtotal', 'shipping', 'total'));
}
public function store(Request $request)
{
$cart = session()->get('cart', []);
if (empty($cart)) {
return redirect()->route('cart.index')->with('error', 'Je winkelwagen is leeg.');
}
$validated = $request->validate([
'customer_name' => 'required|string|max:255',
'customer_email' => 'required|email|max:255',
'customer_phone' => 'nullable|string|max:50',
'shipping_street' => 'required|string|max:255',
'shipping_number' => 'required|string|max:50',
'shipping_postal_code' => 'required|string|max:20',
'shipping_city' => 'required|string|max:100',
'notes' => 'nullable|string|max:1000',
'payment_method' => 'required|string|in:ideal,bancontact,overboeking',
]);
$subtotal = 0;
foreach ($cart as $item) {
$subtotal += $item['price'] * $item['quantity'];
}
$shipping = $subtotal > 50 ? 0.00 : 4.95;
$total = $subtotal + $shipping;
$orderNumber = 'J3D-' . date('Y') . '-' . strtoupper(Str::random(6));
$order = Order::create([
'order_number' => $orderNumber,
'customer_name' => $validated['customer_name'],
'customer_email' => $validated['customer_email'],
'customer_phone' => $validated['customer_phone'] ?? null,
'shipping_street' => $validated['shipping_street'],
'shipping_number' => $validated['shipping_number'],
'shipping_postal_code' => $validated['shipping_postal_code'],
'shipping_city' => $validated['shipping_city'],
'shipping_country' => 'Nederland',
'notes' => $validated['notes'] ?? null,
'status' => 'in_behandeling',
'subtotal' => $subtotal,
'shipping_cost' => $shipping,
'total' => $total,
'payment_status' => 'in_afwachting',
'payment_method' => $validated['payment_method'],
]);
foreach ($cart as $item) {
OrderItem::create([
'order_id' => $order->id,
'product_id' => $item['id'],
'product_name' => $item['name'],
'material' => $item['material'] ?? null,
'color' => $item['color'] ?? null,
'scale' => $item['scale'] ?? '100%',
'infill' => $item['infill'] ?? 'Standaard (20%)',
'quantity' => $item['quantity'],
'unit_price' => $item['price'],
'total_price' => $item['price'] * $item['quantity'],
]);
}
// Leeg winkelwagen na succesvolle plaatsing
session()->forget('cart');
return redirect()->route('checkout.success', $order->order_number);
}
public function success($orderNumber)
{
$order = Order::with('items')->where('order_number', $orderNumber)->firstOrFail();
return view('checkout.success', compact('order'));
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Color;
use App\Models\CustomRequest;
use App\Models\Material;
use App\Models\Product;
use Illuminate\Http\Request;
class ShopController extends Controller
{
public function index()
{
$categories = Category::where('is_featured', true)
->orderBy('sort_order')
->take(6)
->get();
$featuredProducts = Product::with('category')
->where('is_active', true)
->where('is_featured', true)
->latest()
->take(6)
->get();
$materials = Material::where('is_active', true)->get();
return view('shop.index', compact('categories', 'featuredProducts', 'materials'));
}
public function catalog(Request $request)
{
$query = Product::with('category')->where('is_active', true);
// Filter op categorie
if ($request->filled('category')) {
$query->whereHas('category', function ($q) use ($request) {
$q->where('slug', $request->category);
});
}
// Filter op materiaal
if ($request->filled('material')) {
$query->whereJsonContains('available_materials', $request->material);
}
// Zoekfunctie
if ($request->filled('q')) {
$search = $request->q;
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('short_description', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
// Sortering
$sort = $request->get('sort', 'newest');
if ($sort === 'price_asc') {
$query->orderBy('base_price', 'asc');
} elseif ($sort === 'price_desc') {
$query->orderBy('base_price', 'desc');
} else {
$query->latest();
}
$products = $query->paginate(12)->withQueryString();
$categories = Category::orderBy('name')->get();
$materials = Material::where('is_active', true)->get();
return view('shop.catalog', compact('products', 'categories', 'materials'));
}
public function show($slug)
{
$product = Product::with('category')->where('slug', $slug)->firstOrFail();
$allMaterials = Material::where('is_active', true)->get()->keyBy('code');
$allColors = Color::where('in_stock', true)->get();
$relatedProducts = Product::where('id', '!=', $product->id)
->where('category_id', $product->category_id)
->where('is_active', true)
->take(3)
->get();
return view('shop.show', compact('product', 'allMaterials', 'allColors', 'relatedProducts'));
}
public function customRequest()
{
$materials = Material::where('is_active', true)->get();
$colors = Color::where('in_stock', true)->get();
return view('shop.custom', compact('materials', 'colors'));
}
public function storeCustomRequest(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'nullable|string|max:50',
'description' => 'required|string|max:2000',
'preferred_material' => 'nullable|string|max:50',
'preferred_color' => 'nullable|string|max:50',
'dimensions' => 'nullable|string|max:100',
'model_file' => 'nullable|file|max:51200', // max 50MB (stl, step, obj, zip)
]);
$filePath = null;
$originalName = null;
if ($request->hasFile('model_file')) {
$file = $request->file('model_file');
$originalName = $file->getClientOriginalName();
$filePath = $file->store('custom_models', 'public');
}
CustomRequest::create([
'name' => $validated['name'],
'email' => $validated['email'],
'phone' => $validated['phone'] ?? null,
'description' => $validated['description'],
'preferred_material' => $validated['preferred_material'] ?? null,
'preferred_color' => $validated['preferred_color'] ?? null,
'dimensions' => $validated['dimensions'] ?? null,
'file_path' => $filePath,
'original_filename' => $originalName,
'status' => 'nieuw',
]);
return redirect()->route('custom.request')->with('success', 'Bedankt! Jouw 3D print aanvraag is succesvol ontvangen. We beoordelen je model en sturen binnen 24 uur een vrijblijvende prijsopgave.');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'description',
'icon',
'is_featured',
'sort_order',
];
protected $casts = [
'is_featured' => 'boolean',
'sort_order' => 'integer',
];
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Color extends Model
{
use HasFactory;
protected $fillable = [
'name',
'hex',
'in_stock',
];
protected $casts = [
'in_stock' => 'boolean',
];
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CustomRequest extends Model
{
use HasFactory;
protected $fillable = [
'name',
'email',
'phone',
'description',
'file_path',
'original_filename',
'preferred_material',
'preferred_color',
'dimensions',
'status',
'quoted_price',
];
protected $casts = [
'quoted_price' => 'float',
];
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Material extends Model
{
use HasFactory;
protected $fillable = [
'name',
'code',
'description',
'badge_color',
'price_multiplier',
'is_active',
];
protected $casts = [
'price_multiplier' => 'float',
'is_active' => 'boolean',
];
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Order extends Model
{
use HasFactory;
protected $fillable = [
'order_number',
'customer_name',
'customer_email',
'customer_phone',
'shipping_street',
'shipping_number',
'shipping_postal_code',
'shipping_city',
'shipping_country',
'notes',
'status',
'subtotal',
'shipping_cost',
'total',
'payment_status',
'payment_method',
];
protected $casts = [
'subtotal' => 'float',
'shipping_cost' => 'float',
'total' => 'float',
];
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public function getFormattedTotalAttribute(): string
{
return '€ ' . number_format($this->total, 2, ',', '.');
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class OrderItem extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'product_id',
'product_name',
'material',
'color',
'scale',
'infill',
'quantity',
'unit_price',
'total_price',
];
protected $casts = [
'unit_price' => 'float',
'total_price' => 'float',
'quantity' => 'integer',
];
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'category_id',
'name',
'slug',
'short_description',
'description',
'base_price',
'sku',
'is_print_on_demand',
'stock',
'is_featured',
'is_active',
'dimensions',
'weight_grams',
'print_time_hours',
'image_url',
'gallery_images',
'available_materials',
'available_colors',
];
protected $casts = [
'base_price' => 'float',
'is_print_on_demand' => 'boolean',
'is_featured' => 'boolean',
'is_active' => 'boolean',
'print_time_hours' => 'float',
'gallery_images' => 'array',
'available_materials' => 'array',
'available_colors' => 'array',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function orderItems(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public function getFormattedPriceAttribute(): string
{
return '€ ' . number_format($this->base_price, 2, ',', '.');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);
+21
View File
@@ -0,0 +1,21 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
})->create();
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+7
View File
@@ -0,0 +1,7 @@
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];
+86
View File
@@ -0,0 +1,86 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.3",
"laravel/framework": "^13.17",
"laravel/tinker": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6",
"laravel/pint": "^1.27",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^12.5.12"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install --ignore-scripts",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"@php artisan dev"
],
"test": [
"@php artisan config:clear --ansi @no_additional_args",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
Generated
+8687
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the 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' => (bool) 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
| the application so that it's available within Artisan commands.
|
*/
'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. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache", "array"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];
+117
View File
@@ -0,0 +1,117 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];
+136
View File
@@ -0,0 +1,136 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| 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.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "storage", "octane",
| "session", "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/locks'),
],
'storage' => [
'driver' => 'storage',
'disk' => env('CACHE_STORAGE_DISK'),
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],
'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' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This value determines the classes that can be unserialized from cache
| storage. By default, no PHP classes will be unserialized from your
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
*/
'serializable_classes' => false,
];
+184
View File
@@ -0,0 +1,184 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| 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 on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];
+80
View File
@@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
+140
View File
@@ -0,0 +1,140 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "monthly", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'max_files' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'monthly' => [
'driver' => 'monthly',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'max_files' => 3,
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
+118
View File
@@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails 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 emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
],
];
+129
View File
@@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];
+38
View File
@@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Resend, Postmark, AWS, and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];
+233
View File
@@ -0,0 +1,233 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| 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 expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'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' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| 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 session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', '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're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| 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 when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| 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. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This value controls the serialization strategy for session data, which
| is JSON by default. Setting this to "php" allows the storage of PHP
| objects in the session but can make an application vulnerable to
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
| Supported: "json", "php"
|
*/
'serialization' => 'json',
];
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('icon')->default('box');
$table->boolean('is_featured')->default(false);
$table->integer('sort_order')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->string('short_description', 500)->nullable();
$table->longText('description')->nullable();
$table->decimal('base_price', 8, 2);
$table->string('sku')->nullable();
$table->boolean('is_print_on_demand')->default(true);
$table->integer('stock')->default(10);
$table->boolean('is_featured')->default(false);
$table->boolean('is_active')->default(true);
$table->string('dimensions')->nullable(); // bijv. "12 x 8 x 15 cm"
$table->integer('weight_grams')->nullable(); // bijv. 85
$table->decimal('print_time_hours', 4, 1)->nullable(); // bijv. 4.5
$table->string('image_url')->nullable();
$table->json('gallery_images')->nullable();
$table->json('available_materials')->nullable(); // ["pla", "petg", "tpu"]
$table->json('available_colors')->nullable(); // ["Mat Zwart", "Sneeuwwit", ...]
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('materials', function (Blueprint $table) {
$table->id();
$table->string('name'); // bv. PLA+, PETG, TPU Flex, ABS
$table->string('code')->unique(); // pla, petg, tpu
$table->text('description')->nullable();
$table->string('badge_color')->default('emerald'); // emerald, blue, purple, amber
$table->decimal('price_multiplier', 4, 2)->default(1.00); // bv. 1.00, 1.25, 1.40
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('materials');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('colors', function (Blueprint $table) {
$table->id();
$table->string('name'); // Mat Zwart, Sneeuwwit, Signaal Oranje, etc.
$table->string('hex'); // #111827, #FFFFFF, etc.
$table->boolean('in_stock')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('colors');
}
};
@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('order_number')->unique(); // J3D-2026-0001
$table->string('customer_name');
$table->string('customer_email');
$table->string('customer_phone')->nullable();
$table->string('shipping_street');
$table->string('shipping_number');
$table->string('shipping_postal_code');
$table->string('shipping_city');
$table->string('shipping_country')->default('Nederland');
$table->text('notes')->nullable();
$table->string('status')->default('in_behandeling'); // in_behandeling, in_print, afgerond, verzonden, geannuleerd
$table->decimal('subtotal', 8, 2);
$table->decimal('shipping_cost', 8, 2)->default(4.95);
$table->decimal('total', 8, 2);
$table->string('payment_status')->default('in_afwachting'); // in_afwachting, betaald, geannuleerd
$table->string('payment_method')->default('ideal');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->cascadeOnDelete();
$table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
$table->string('product_name');
$table->string('material')->nullable(); // PLA+, PETG, etc.
$table->string('color')->nullable(); // Mat Zwart
$table->string('scale')->default('100%'); // 100%, 150%, etc.
$table->string('infill')->default('Standaard (20%)');
$table->integer('quantity')->default(1);
$table->decimal('unit_price', 8, 2);
$table->decimal('total_price', 8, 2);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('order_items');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('custom_requests', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email');
$table->string('phone')->nullable();
$table->text('description');
$table->string('file_path')->nullable(); // Uploaded .stl, .step, .obj
$table->string('original_filename')->nullable();
$table->string('preferred_material')->nullable();
$table->string('preferred_color')->nullable();
$table->string('dimensions')->nullable();
$table->string('status')->default('nieuw'); // nieuw, offerte_verstuurd, in_behandeling, afgerond
$table->decimal('quoted_price', 8, 2)->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('custom_requests');
}
};
+217
View File
@@ -0,0 +1,217 @@
<?php
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Color;
use App\Models\Material;
use App\Models\Product;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// 1. Materialen
$materials = [
[
'name' => 'PLA+ Premium',
'code' => 'pla',
'description' => 'Milieuvriendelijk, vormvast en strakke printlagen. Dé universele keuze voor interieur, houders en decoratie.',
'badge_color' => 'emerald',
'price_multiplier' => 1.00,
],
[
'name' => 'PETG Tough',
'code' => 'petg',
'description' => 'Hittebestendig tot 75°C, slagvast en waterbestendig. Ideaal voor zwaarder belaste gereedschappen en buitentoepassingen.',
'badge_color' => 'blue',
'price_multiplier' => 1.20,
],
[
'name' => 'TPU 95A Flexibel',
'code' => 'tpu',
'description' => 'Rubberachtig elastomeer. Schokabsorberend, slijtvast en nagenoeg onverwoestbaar.',
'badge_color' => 'amber',
'price_multiplier' => 1.35,
],
[
'name' => 'Resin Ultra-Detail',
'code' => 'resin',
'description' => 'Vloeibare fotopolymeer hars voor spuitgietkwaliteit met microscopische laaglijnen en perfecte details.',
'badge_color' => 'purple',
'price_multiplier' => 1.50,
],
];
foreach ($materials as $m) {
Material::updateOrCreate(['code' => $m['code']], $m);
}
// 2. Kleuren
$colors = [
['name' => 'Mat Zwart', 'hex' => '#171717', 'in_stock' => true],
['name' => 'Sneeuwwit', 'hex' => '#F8FAFC', 'in_stock' => true],
['name' => 'Galaxy Grijs', 'hex' => '#475569', 'in_stock' => true],
['name' => 'Signaal Oranje', 'hex' => '#EA580C', 'in_stock' => true],
['name' => 'Bamboe Groen', 'hex' => '#16A34A', 'in_stock' => true],
['name' => 'Kobaltblauw', 'hex' => '#2563EB', 'in_stock' => true],
['name' => 'Koper Zijdeglans', 'hex' => '#B45309', 'in_stock' => true],
['name' => 'Vurig Rood', 'hex' => '#DC2626', 'in_stock' => true],
];
foreach ($colors as $c) {
Color::updateOrCreate(['name' => $c['name']], $c);
}
// 3. Categorieën
$catInterieur = Category::updateOrCreate(['slug' => 'home-interieur'], [
'name' => 'Home & Interieur',
'slug' => 'home-interieur',
'description' => 'Stijlvolle geometrische vazen, wanddecoratie en functioneel design voor in huis.',
'icon' => 'home',
'is_featured' => true,
'sort_order' => 1,
]);
$catBureau = Category::updateOrCreate(['slug' => 'bureau-tech'], [
'name' => 'Bureau & Tech Accessoires',
'slug' => 'bureau-tech',
'description' => 'Organizers, telefoonstandaarden, hoofdtelefoonhouders en kabelmanagement.',
'icon' => 'laptop',
'is_featured' => true,
'sort_order' => 2,
]);
$catTools = Category::updateOrCreate(['slug' => 'gereedschap-werkplaats'], [
'name' => 'Gereedschap & Werkplaats',
'slug' => 'gereedschap-werkplaats',
'description' => 'Robuuste houders, borensets, meetgereedschap en handige garage-hulpstukken.',
'icon' => 'wrench',
'is_featured' => false,
'sort_order' => 3,
]);
$catGadgets = Category::updateOrCreate(['slug' => 'gadgets-speelgoed'], [
'name' => 'Gadgets & Speelgoed',
'slug' => 'gadgets-speelgoed',
'description' => 'Articulated beweegbare figuren, fidget gadgets en unieke giveaways.',
'icon' => 'sparkles',
'is_featured' => true,
'sort_order' => 4,
]);
$catCustom = Category::updateOrCreate(['slug' => 'custom-printservice'], [
'name' => 'Custom Printservice',
'slug' => 'custom-printservice',
'description' => 'Laat jouw eigen STL- of CAD-ontwerp professioneel printen op maat.',
'icon' => 'printer',
'is_featured' => true,
'sort_order' => 5,
]);
// 4. Producten
$products = [
[
'category_id' => $catInterieur->id,
'name' => 'Origami Geometrische Vaas',
'slug' => 'origami-geometrische-vaas',
'short_description' => 'Moderne vaas met scherpe facetten en minimalistische vouwlijnen. Een eyecatcher op elke tafel.',
'description' => "De Origami Vaas combineert traditionele vouwkunst met hypermoderne additieve fabricage. Geprint in spiralize vaasmodus of met versterkte wanden voor een 100% waterdichte afwerking bij keuze voor PETG. Ideaal voor droogbloemen of als sculptuur in je woonkamer.",
'base_price' => 18.95,
'sku' => 'J3D-VAS-001',
'is_featured' => true,
'dimensions' => '11 x 11 x 22 cm',
'weight_grams' => 140,
'print_time_hours' => 5.5,
'image_url' => 'https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&w=800&q=80',
'available_materials' => ['pla', 'petg'],
'available_colors' => ['Mat Zwart', 'Sneeuwwit', 'Galaxy Grijs', 'Koper Zijdeglans'],
],
[
'category_id' => $catBureau->id,
'name' => 'Ergonomische MagSafe Telefoon- & Watchhouder',
'slug' => 'ergonomische-magsafe-telefoonhouder',
'short_description' => 'Strakke bureau-standaard met kabelgoot, geschikt voor iPhone MagSafe en smartwatch.',
'description' => "Houd je bureau opgeruimd en je smartphone in de perfecte 65-graden kijkhoek voor videocalls of StandBy-modus. Voorzien van antislip voetpads en een slimme verborgen kabelgoot.",
'base_price' => 14.50,
'sku' => 'J3D-DSK-002',
'is_featured' => true,
'dimensions' => '9 x 8 x 12 cm',
'weight_grams' => 95,
'print_time_hours' => 3.5,
'image_url' => 'https://images.unsplash.com/photo-1586105251261-72a756497a11?auto=format&fit=crop&w=800&q=80',
'available_materials' => ['pla', 'petg'],
'available_colors' => ['Mat Zwart', 'Sneeuwwit', 'Galaxy Grijs'],
],
[
'category_id' => $catBureau->id,
'name' => 'Minimal Hoofdtelefoon Bureaustandaard',
'slug' => 'minimal-hoofdtelefoon-standaard',
'short_description' => 'Elegante boogstandaard om je headset veilig en vormvast op te bergen.',
'description' => "Geen rondslingerende hoofdtelefoons meer. Het brede steunvlak ontziet de hoofdband en voorkomt drukplekken in de kussens. Geprint met versterkte infill voor optimale stabiliteit.",
'base_price' => 22.50,
'sku' => 'J3D-DSK-003',
'is_featured' => true,
'dimensions' => '14 x 12 x 26 cm',
'weight_grams' => 210,
'print_time_hours' => 8.0,
'image_url' => 'https://images.unsplash.com/photo-1546435770-a3e426bf472b?auto=format&fit=crop&w=800&q=80',
'available_materials' => ['pla', 'petg'],
'available_colors' => ['Mat Zwart', 'Galaxy Grijs', 'Sneeuwwit'],
],
[
'category_id' => $catGadgets->id,
'name' => 'Articulated Flexibele Kristaldraak',
'slug' => 'articulated-flexibele-kristaldraak',
'short_description' => 'Print-in-place scharnierende draak met tientallen soepele schakels. Fascinerend fidget & decoratie-item.',
'description' => "In één stuk geprint zonder montage of schroeven! De schakels bewegen soepel en natuurlijk in je handen. Verkrijgbaar in zijdeglans en tweekleurige effecten.",
'base_price' => 19.95,
'sku' => 'J3D-GAD-004',
'is_featured' => true,
'dimensions' => '45 x 6 x 4 cm',
'weight_grams' => 120,
'print_time_hours' => 6.0,
'image_url' => 'https://images.unsplash.com/photo-1563089145-599997674d42?auto=format&fit=crop&w=800&q=80',
'available_materials' => ['pla'],
'available_colors' => ['Koper Zijdeglans', 'Signaal Oranje', 'Bamboe Groen', 'Mat Zwart'],
],
[
'category_id' => $catInterieur->id,
'name' => 'Hexagon Modulaire Wandplankjes (Set van 3)',
'slug' => 'hexagon-modulaire-wandplankjes',
'short_description' => 'Honingraat wanddisplay voor kleine plantjes, miniaturen of edelstenen. Inclusief onzichtbare wandmontage.',
'description' => "Creëer je eigen geometrische wandpatroon. Eenvoudig op te hangen met dubbelzijdige montagepads of kleine spijkers. Robuust ontworpen om kleine vetplantjes en beeldjes moeiteloos te dragen.",
'base_price' => 27.50,
'sku' => 'J3D-HOM-005',
'is_featured' => false,
'dimensions' => '15 x 13 x 6 cm (per stuk)',
'weight_grams' => 260,
'print_time_hours' => 9.5,
'image_url' => 'https://images.unsplash.com/photo-1513519245088-0e12902e5a38?auto=format&fit=crop&w=800&q=80',
'available_materials' => ['pla', 'petg'],
'available_colors' => ['Mat Zwart', 'Sneeuwwit', 'Koper Zijdeglans'],
],
[
'category_id' => $catTools->id,
'name' => 'Heavy-Duty Boorkophouder & Magnetische Bit Tray',
'slug' => 'heavy-duty-boorkophouder-bit-tray',
'short_description' => 'Georganiseerde opberger voor 1/4" hex bits en boorkoppen met krachtige Neodymium magneten.',
'description' => "Geprint in taai en chemicaliënbestendig PETG. Nooit meer losse schroefbits zoeken in je gereedschapskist. De magnetische bodem houdt al je metalen accessoires stevig op hun plek.",
'base_price' => 16.95,
'sku' => 'J3D-WRK-006',
'is_featured' => false,
'dimensions' => '16 x 7 x 3.5 cm',
'weight_grams' => 110,
'print_time_hours' => 4.0,
'image_url' => 'https://images.unsplash.com/photo-1581783898377-1c85bf937427?auto=format&fit=crop&w=800&q=80',
'available_materials' => ['petg', 'tpu'],
'available_colors' => ['Mat Zwart', 'Signaal Oranje', 'Kobaltblauw'],
],
];
foreach ($products as $p) {
Product::updateOrCreate(['slug' => $p['slug']], $p);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
# Nixpacks configuratie voor Coolify
[phases.setup]
nixPkgs = ["php83", "php83Packages.composer", "nodejs_20"]
[phases.install]
cmds = [
"composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader",
"npm ci || npm install"
]
[phases.build]
cmds = [
"npm run build",
"php artisan config:cache",
"php artisan route:cache",
"php artisan view:cache"
]
[start]
cmd = "php artisan storage:link && touch database/database.sqlite && php artisan migrate --force && php artisan serve --host=0.0.0.0 --port=80"
+2416
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"concurrently": "^10.0.3",
"laravel-vite-plugin": "^3.1",
"tailwindcss": "^4.0.0",
"vite": "^8.0.0"
},
"optionalDependencies": {
"@laravel/multiplex": "^0.4.1"
},
"dependencies": {
"alpinejs": "^3.17.5",
"lucide": "^1.66.0"
}
}
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>
+25
View File
@@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
View File
+20
View File
@@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+9
View File
@@ -0,0 +1,9 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
+9
View File
@@ -0,0 +1,9 @@
import Alpine from 'alpinejs';
import { createIcons, icons } from 'lucide';
window.Alpine = Alpine;
Alpine.start();
document.addEventListener('DOMContentLoaded', () => {
createIcons({ icons });
});
+130
View File
@@ -0,0 +1,130 @@
@extends('layouts.app')
@section('title', 'Winkelwagen | Jouwdriedee.nl')
@section('content')
<div class="bg-white border-b border-slate-200 py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Winkelwagen</h1>
<p class="text-xs text-slate-500 mt-0.5">Controleer jouw geselecteerde 3D prints, materialen en kleuren.</p>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
@if(empty($cart))
<div class="p-12 text-center rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4 max-w-lg mx-auto">
<div class="w-16 h-16 rounded-2xl bg-indigo-50 text-indigo-600 flex items-center justify-center mx-auto">
<i data-lucide="shopping-bag" class="w-8 h-8"></i>
</div>
<h2 class="text-lg font-bold text-slate-900">Je winkelwagen is leeg</h2>
<p class="text-xs text-slate-500">Ontdek onze collectie 3D geprinte designs of vraag een custom print aan.</p>
<div class="pt-2">
<a href="{{ route('catalog') }}" class="px-6 py-3 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition inline-flex items-center space-x-2">
<span>Bekijk Catalogus</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</a>
</div>
</div>
@else
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Items Lijst -->
<div class="lg:col-span-8 space-y-4">
@foreach($cart as $key => $item)
<div class="p-5 rounded-3xl bg-white border border-slate-200 shadow-2xs flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center space-x-4 w-full sm:w-auto">
<div class="w-20 h-20 rounded-2xl bg-slate-100 overflow-hidden flex-shrink-0 border border-slate-100">
<img src="{{ $item['image_url'] }}" alt="{{ $item['name'] }}" class="w-full h-full object-cover">
</div>
<div class="space-y-1">
<h3 class="text-sm font-bold text-slate-900">{{ $item['name'] }}</h3>
<div class="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
<span class="px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 font-semibold">{{ $item['material'] }}</span>
<span>•</span>
<span class="font-medium text-slate-700">Kleur: {{ $item['color'] }}</span>
<span>•</span>
<span class="font-mono text-slate-700">Schaal: {{ $item['scale'] }}</span>
</div>
<div class="text-xs font-mono font-bold text-slate-900 pt-1">
€ {{ number_format($item['price'], 2, ',', '.') }} per stuk
</div>
</div>
</div>
<!-- Aantal en acties -->
<div class="flex items-center justify-between sm:justify-end space-x-4 w-full sm:w-auto pt-3 sm:pt-0 border-t sm:border-t-0 border-slate-100">
<!-- Update Formulier -->
<form action="{{ route('cart.update') }}" method="POST" class="flex items-center space-x-1">
@csrf
<input type="hidden" name="key" value="{{ $key }}">
<input type="number" name="quantity" min="1" max="50" value="{{ $item['quantity'] }}" class="w-16 bg-slate-50 border border-slate-200 rounded-xl px-2 py-1.5 text-xs text-center font-mono font-bold text-slate-900 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<button type="submit" class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 transition" title="Bijwerken">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
</button>
</form>
<span class="text-sm font-black text-slate-900 font-mono w-24 text-right">
€ {{ number_format($item['price'] * $item['quantity'], 2, ',', '.') }}
</span>
<!-- Verwijder Formulier -->
<form action="{{ route('cart.remove', $key) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="p-2 text-slate-300 hover:text-rose-500 transition cursor-pointer" title="Verwijderen">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</form>
</div>
</div>
@endforeach
</div>
<!-- Samenvatting -->
<div class="lg:col-span-4">
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-md space-y-5 sticky top-24">
<h3 class="text-sm font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100">Overzicht Bestelling</h3>
<div class="space-y-2.5 text-xs">
<div class="flex justify-between text-slate-600">
<span>Subtotaal</span>
<span class="font-mono font-bold text-slate-900">€ {{ number_format($subtotal, 2, ',', '.') }}</span>
</div>
<div class="flex justify-between text-slate-600">
<span>Verzendkosten</span>
@if($shipping == 0)
<span class="text-emerald-600 font-bold">GRATIS</span>
@else
<span class="font-mono font-bold text-slate-900">€ {{ number_format($shipping, 2, ',', '.') }}</span>
@endif
</div>
@if($subtotal < 50)
<p class="text-[11px] text-amber-600 bg-amber-50 p-2.5 rounded-xl border border-amber-100">
Tip: Voeg nog <strong>€ {{ number_format(50 - $subtotal, 2, ',', '.') }}</strong> toe voor gratis verzending!
</p>
@endif
</div>
<div class="pt-4 border-t border-slate-100 flex justify-between items-baseline">
<span class="text-sm font-bold text-slate-900">Totaal (incl. btw)</span>
<span class="text-2xl font-black text-indigo-600 font-mono">€ {{ number_format($total, 2, ',', '.') }}</span>
</div>
<a href="{{ route('checkout.index') }}" class="w-full py-4 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs uppercase tracking-wider transition shadow-lg shadow-indigo-600/25 flex items-center justify-center space-x-2">
<span>Doorgaan Naar Afrekenen</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</a>
<div class="flex items-center justify-center space-x-3 text-[11px] text-slate-400 pt-1">
<span>🔒 Veilig afrekenen</span>
<span>•</span>
<span>PostNL Bezorging</span>
</div>
</div>
</div>
</div>
@endif
</div>
@endsection
+171
View File
@@ -0,0 +1,171 @@
@extends('layouts.app')
@section('title', 'Afrekenen | Jouwdriedee.nl')
@section('content')
<div class="bg-white border-b border-slate-200 py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Afrekenen & Gegevens</h1>
<p class="text-xs text-slate-500 mt-0.5">Vul je bezorgadres in om jouw 3D-prints in de printwachtrij te plaatsen.</p>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<form action="{{ route('checkout.store') }}" method="POST">
@csrf
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Linkerkant: Formulier -->
<div class="lg:col-span-7 space-y-6">
<!-- 1. Contactgegevens -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4">
<h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="user" class="w-4 h-4 text-indigo-600"></i>
<span>1. Contactgegevens</span>
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="sm:col-span-2">
<label class="block text-xs font-semibold text-slate-700 mb-1">Volledige Naam *</label>
<input type="text" name="customer_name" required value="{{ old('customer_name') }}" placeholder="Jan de Vries" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">E-mailadres *</label>
<input type="email" name="customer_email" required value="{{ old('customer_email') }}" placeholder="jan@voorbeeld.nl" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Telefoonnummer (voor bezorgupdates)</label>
<input type="text" name="customer_phone" value="{{ old('customer_phone') }}" placeholder="06-12345678" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
</div>
<!-- 2. Verzendadres -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4">
<h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="map-pin" class="w-4 h-4 text-indigo-600"></i>
<span>2. Verzendadres (Nederland)</span>
</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="sm:col-span-2">
<label class="block text-xs font-semibold text-slate-700 mb-1">Straatnaam *</label>
<input type="text" name="shipping_street" required value="{{ old('shipping_street') }}" placeholder="Hoofdstraat" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Huisnummer *</label>
<input type="text" name="shipping_number" required value="{{ old('shipping_number') }}" placeholder="12A" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Postcode *</label>
<input type="text" name="shipping_postal_code" required value="{{ old('shipping_postal_code') }}" placeholder="1234 AB" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-semibold text-slate-700 mb-1">Woonplaats *</label>
<input type="text" name="shipping_city" required value="{{ old('shipping_city') }}" placeholder="Amsterdam" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
</div>
<!-- 3. Betaalmethode -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4">
<h2 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="credit-card" class="w-4 h-4 text-indigo-600"></i>
<span>3. Betaalmethode</span>
</h2>
<div class="space-y-2.5">
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-indigo-200 bg-indigo-50/40 cursor-pointer">
<div class="flex items-center space-x-3">
<input type="radio" name="payment_method" value="ideal" checked class="text-indigo-600 focus:ring-indigo-500">
<span class="text-xs font-bold text-slate-900">iDEAL</span>
</div>
<span class="text-[10px] font-mono px-2 py-0.5 rounded bg-white text-indigo-700 font-bold border border-indigo-200">Aanbevolen</span>
</label>
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-slate-200 hover:bg-slate-50 cursor-pointer">
<div class="flex items-center space-x-3">
<input type="radio" name="payment_method" value="bancontact" class="text-indigo-600 focus:ring-indigo-500">
<span class="text-xs font-bold text-slate-900">Bancontact</span>
</div>
<span class="text-[11px] text-slate-400">België</span>
</label>
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-slate-200 hover:bg-slate-50 cursor-pointer">
<div class="flex items-center space-x-3">
<input type="radio" name="payment_method" value="overboeking" class="text-indigo-600 focus:ring-indigo-500">
<span class="text-xs font-bold text-slate-900">Handmatige Bankoverschrijving</span>
</div>
<span class="text-[11px] text-slate-400">Achteraf / Factuur</span>
</label>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Opmerkingen voor de printer (optioneel)</label>
<textarea name="notes" rows="2" placeholder="Bijv. neerzetten bij de achterdeur of speciale printinstructie..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"></textarea>
</div>
</div>
</div>
<!-- Rechterkant: Besteloverzicht Card -->
<div class="lg:col-span-5">
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-md space-y-5 sticky top-24">
<h3 class="text-sm font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100">Jouw Bestelling</h3>
<div class="space-y-3 max-h-72 overflow-y-auto pr-1">
@foreach($cart as $item)
<div class="flex items-center justify-between text-xs py-2 border-b border-slate-100">
<div class="space-y-0.5">
<div class="font-bold text-slate-900">{{ $item['name'] }} <span class="text-slate-400 font-normal">&times; {{ $item['quantity'] }}</span></div>
<div class="text-[11px] text-slate-500">{{ $item['material'] }} • {{ $item['color'] }} ({{ $item['scale'] }})</div>
</div>
<span class="font-mono font-bold text-slate-900">
€ {{ number_format($item['price'] * $item['quantity'], 2, ',', '.') }}
</span>
</div>
@endforeach
</div>
<div class="space-y-2 text-xs pt-2">
<div class="flex justify-between text-slate-600">
<span>Subtotaal</span>
<span class="font-mono font-bold text-slate-900">€ {{ number_format($subtotal, 2, ',', '.') }}</span>
</div>
<div class="flex justify-between text-slate-600">
<span>Verzendkosten</span>
@if($shipping == 0)
<span class="text-emerald-600 font-bold">GRATIS</span>
@else
<span class="font-mono font-bold text-slate-900">€ {{ number_format($shipping, 2, ',', '.') }}</span>
@endif
</div>
</div>
<div class="pt-4 border-t border-slate-100 flex justify-between items-baseline">
<span class="text-sm font-bold text-slate-900">Totaalbedrag</span>
<span class="text-2xl font-black text-indigo-600 font-mono">€ {{ number_format($total, 2, ',', '.') }}</span>
</div>
<button type="submit" class="w-full py-4 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs uppercase tracking-wider transition shadow-lg shadow-indigo-600/25 flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="check-circle-2" class="w-4 h-4"></i>
<span>Bestelling Definitief Plaatsen</span>
</button>
<p class="text-center text-[10px] text-slate-400">
Door te bestellen ga je akkoord met de algemene voorwaarden van Jouwdriedee.nl.
</p>
</div>
</div>
</div>
</form>
</div>
@endsection
+105
View File
@@ -0,0 +1,105 @@
@extends('layouts.app')
@section('title', 'Bestelling Ontvangen! | Jouwdriedee.nl')
@section('content')
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center space-y-8">
<!-- Success Badge -->
<div class="space-y-4">
<div class="w-20 h-20 rounded-3xl bg-emerald-100 text-emerald-600 flex items-center justify-center mx-auto shadow-lg shadow-emerald-500/20">
<i data-lucide="check" class="w-10 h-10 stroke-[2.5]"></i>
</div>
<h1 class="text-3xl sm:text-4xl font-black text-slate-900 tracking-tight">Bedankt voor je bestelling!</h1>
<p class="text-xs sm:text-sm text-slate-500 max-w-md mx-auto">
We hebben jouw bestelling ontvangen. De printbestanden worden klaargezet voor de 3D-printers.
</p>
<div class="inline-block px-4 py-2 rounded-xl bg-slate-100 border border-slate-200 text-xs font-mono font-bold text-slate-800">
Bestelnummer: <span class="text-indigo-600">{{ $order->order_number }}</span>
</div>
</div>
<!-- Live 3D Print Status Tracker -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-sm text-left space-y-6">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="printer" class="w-4 h-4 text-indigo-600"></i>
<span>Live Print & Productie Status</span>
</h3>
<div class="relative pl-6 space-y-6 border-l-2 border-slate-200">
<!-- Stap 1: Ontvangen -->
<div class="relative">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-emerald-500 ring-4 ring-emerald-100"></span>
<h4 class="text-xs font-bold text-slate-900">1. Bestelling ontvangen & geverifieerd</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Jouw configuratie en materiaalkeuzes zijn opgeslagen.</p>
</div>
<!-- Stap 2: Slicing & Printbed -->
<div class="relative">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-indigo-600 ring-4 ring-indigo-100 animate-pulse"></span>
<h4 class="text-xs font-bold text-indigo-600">2. Slicing & printwachtrij (Huidige fase)</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Het 3D-model wordt geoptimaliseerd en klaargezet voor de printer.</p>
</div>
<!-- Stap 3: 3D Printen -->
<div class="relative opacity-60">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-slate-300"></span>
<h4 class="text-xs font-bold text-slate-700">3. 3D Printen in de studio</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Laag voor laag geprint op hoge resolutie.</p>
</div>
<!-- Stap 4: Kwaliteitscontrole & Verzenden -->
<div class="relative opacity-60">
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-slate-300"></span>
<h4 class="text-xs font-bold text-slate-700">4. Kwaliteitscontrole, inpakken & PostNL verzending</h4>
<p class="text-[11px] text-slate-500 mt-0.5">Je ontvangt per e-mail direct een Track & Trace code.</p>
</div>
</div>
</div>
<!-- Besteloverzicht Card -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-sm text-left space-y-4">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100">
Overzicht van jouw bestelling
</h3>
<div class="space-y-3">
@foreach($order->items as $item)
<div class="flex items-center justify-between text-xs py-1">
<div>
<span class="font-bold text-slate-900">{{ $item->product_name }}</span>
<span class="text-slate-400">&times; {{ $item->quantity }}</span>
<div class="text-[11px] text-slate-500">{{ $item->material }} • {{ $item->color }} ({{ $item->scale }})</div>
</div>
<span class="font-mono font-bold text-slate-900">
€ {{ number_format($item->total_price, 2, ',', '.') }}
</span>
</div>
@endforeach
</div>
<div class="pt-3 border-t border-slate-100 flex justify-between items-baseline text-xs">
<span class="font-bold text-slate-700">Totaal betaald</span>
<span class="text-lg font-black text-indigo-600 font-mono">{{ $order->formatted_total }}</span>
</div>
<div class="pt-3 border-t border-slate-100 text-xs text-slate-500">
<span class="font-bold text-slate-700 block mb-1">Bezorgadres:</span>
{{ $order->customer_name }}<br>
{{ $order->shipping_street }} {{ $order->shipping_number }}<br>
{{ $order->shipping_postal_code }} {{ $order->shipping_city }}
</div>
</div>
<div>
<a href="{{ route('home') }}" class="px-6 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition inline-flex items-center space-x-2 shadow-md">
<span>Terug Naar De Homepagina</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</a>
</div>
</div>
@endsection
+244
View File
@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="nl" class="h-full bg-slate-50 text-slate-900 antialiased">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Jouwdriedee.nl | Dé 3D Print Webshop & Printservice')</title>
<meta name="description" content="@yield('meta_description', 'Jouwdriedee.nl - Jouw ideeën tastbaar gemaakt in 3D. Unieke decoratie, tech gadgets en custom 3D printservice.')">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
.font-mono { font-family: 'JetBrains Mono', monospace; }
</style>
</head>
<body class="min-h-full flex flex-col bg-slate-50 text-slate-800" x-data="{ mobileMenuOpen: false }">
<!-- Top Announcement Bar -->
<div class="bg-indigo-950 text-indigo-200 text-xs py-2 px-4 text-center font-medium border-b border-indigo-900/60">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<span class="hidden sm:inline">🚀 <strong>Nieuw:</strong> Upload nu ook jouw eigen 3D CAD/STL bestanden voor maatwerk prints!</span>
<span class="sm:hidden mx-auto font-semibold">✨ Gratis verzending vanaf € 50,-</span>
<div class="hidden sm:flex items-center space-x-6 text-[11px] text-indigo-300">
<span class="flex items-center space-x-1.5">
<i data-lucide="shield-check" class="w-3.5 h-3.5 text-emerald-400"></i>
<span>Premium PLA+, PETG & TPU</span>
</span>
<span class="flex items-center space-x-1.5">
<i data-lucide="truck" class="w-3.5 h-3.5 text-amber-400"></i>
<span>Verzending binnen 2-3 werkdagen</span>
</span>
</div>
</div>
</div>
<!-- Main Navigation Header -->
<header class="sticky top-0 z-40 bg-white/90 backdrop-blur-md border-b border-slate-200/80 shadow-2xs">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-20">
<!-- Logo & Brand -->
<div class="flex items-center space-x-8">
<a href="{{ route('home') }}" class="flex items-center space-x-3 group">
<div class="w-11 h-11 rounded-2xl bg-gradient-to-tr from-indigo-600 via-indigo-500 to-sky-400 flex items-center justify-center text-white shadow-md shadow-indigo-500/25 group-hover:scale-105 transition-transform">
<i data-lucide="box" class="w-6 h-6 stroke-[2.2]"></i>
</div>
<div class="flex flex-col">
<div class="flex items-center space-x-1">
<span class="text-xl font-extrabold tracking-tight text-slate-900">Jouw<span class="text-indigo-600">driedee</span></span>
<span class="text-xs font-bold text-sky-500 font-mono">.nl</span>
</div>
<span class="text-[10px] tracking-wider uppercase font-semibold text-slate-400">3D Print Studio & Shop</span>
</div>
</a>
<!-- Desktop Nav Links -->
<nav class="hidden md:flex items-center space-x-1 text-sm font-semibold">
<a href="{{ route('home') }}" class="px-3.5 py-2 rounded-xl transition {{ request()->routeIs('home') ? 'bg-indigo-50 text-indigo-600 font-bold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/70' }}">
Home
</a>
<a href="{{ route('catalog') }}" class="px-3.5 py-2 rounded-xl transition {{ request()->routeIs('catalog*') ? 'bg-indigo-50 text-indigo-600 font-bold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/70' }}">
Catalogus
</a>
<a href="{{ route('custom.request') }}" class="px-3.5 py-2 rounded-xl transition flex items-center space-x-1.5 {{ request()->routeIs('custom.request') ? 'bg-indigo-50 text-indigo-600 font-bold' : 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/70' }}">
<i data-lucide="printer" class="w-4 h-4 text-indigo-500"></i>
<span>Eigen STL Printen</span>
</a>
</nav>
</div>
<!-- Right Action Bar -->
<div class="flex items-center space-x-3">
<!-- Search Bar in Header -->
<form action="{{ route('catalog') }}" method="GET" class="hidden lg:flex items-center relative">
<input
type="text"
name="q"
value="{{ request('q') }}"
placeholder="Zoek een print, houder, vaas..."
class="w-64 bg-slate-100 border border-slate-200/80 rounded-xl pl-9 pr-3.5 py-2 text-xs font-medium placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition"
>
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3 pointer-events-none"></i>
</form>
<!-- Cart Button with Counter -->
@php
$cartItems = session('cart', []);
$cartCount = array_sum(array_column($cartItems, 'quantity'));
@endphp
<a href="{{ route('cart.index') }}" class="relative p-2.5 rounded-xl border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 transition flex items-center space-x-2 shadow-2xs group">
<i data-lucide="shopping-bag" class="w-5 h-5 text-slate-600 group-hover:text-indigo-600 transition"></i>
<span class="text-xs font-bold hidden sm:inline">Winkelwagen</span>
@if($cartCount > 0)
<span class="inline-flex items-center justify-center px-2 py-0.5 text-xs font-black text-white bg-indigo-600 rounded-full animate-pulse">
{{ $cartCount }}
</span>
@endif
</a>
<!-- Mobile Menu Trigger -->
<button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="md:hidden p-2.5 rounded-xl border border-slate-200 bg-white text-slate-700 hover:bg-slate-50">
<i data-lucide="menu" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
<!-- Mobile Menu Dropdown -->
<div x-show="mobileMenuOpen" x-cloak class="md:hidden border-t border-slate-200 bg-white px-4 pt-3 pb-6 space-y-3 shadow-lg">
<form action="{{ route('catalog') }}" method="GET" class="flex items-center relative mb-3">
<input
type="text"
name="q"
value="{{ request('q') }}"
placeholder="Zoek een print, houder..."
class="w-full bg-slate-100 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs placeholder-slate-400"
>
<i data-lucide="search" class="w-4 h-4 text-slate-400 absolute left-3"></i>
</form>
<div class="flex flex-col space-y-1">
<a href="{{ route('home') }}" class="px-3.5 py-2.5 rounded-xl text-sm font-semibold text-slate-700 hover:bg-slate-50">Home</a>
<a href="{{ route('catalog') }}" class="px-3.5 py-2.5 rounded-xl text-sm font-semibold text-slate-700 hover:bg-slate-50">Alle 3D Prints & Catalogus</a>
<a href="{{ route('custom.request') }}" class="px-3.5 py-2.5 rounded-xl text-sm font-semibold text-indigo-600 bg-indigo-50/70 font-bold flex items-center space-x-2">
<i data-lucide="printer" class="w-4 h-4"></i>
<span>Eigen 3D Model Printen (Upload)</span>
</a>
</div>
</div>
</header>
<!-- Global Toast / Alerts -->
@if(session('success'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<div class="p-4 rounded-2xl bg-emerald-50 border border-emerald-200 text-emerald-900 text-sm font-semibold flex items-center justify-between shadow-xs">
<div class="flex items-center space-x-3">
<div class="w-7 h-7 rounded-lg bg-emerald-600 text-white flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 stroke-[3]"></i>
</div>
<span>{{ session('success') }}</span>
</div>
<a href="{{ route('cart.index') }}" class="text-xs font-bold underline hover:text-emerald-950">Naar winkelwagen &rarr;</a>
</div>
</div>
@endif
@if(session('error'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<div class="p-4 rounded-2xl bg-rose-50 border border-rose-200 text-rose-900 text-sm font-semibold flex items-center space-x-3 shadow-xs">
<div class="w-7 h-7 rounded-lg bg-rose-600 text-white flex items-center justify-center">
<i data-lucide="alert-circle" class="w-4 h-4 stroke-[3]"></i>
</div>
<span>{{ session('error') }}</span>
</div>
</div>
@endif
<!-- Main Content Slot -->
<main class="flex-1">
@yield('content')
</main>
<!-- Footer -->
<footer class="bg-slate-900 text-slate-300 border-t border-slate-800 pt-16 pb-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-10 pb-12 border-b border-slate-800/80">
<!-- Col 1: Brand -->
<div class="space-y-4 md:col-span-1">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-indigo-600 flex items-center justify-center text-white">
<i data-lucide="box" class="w-5 h-5"></i>
</div>
<span class="text-xl font-black text-white tracking-tight">Jouw<span class="text-indigo-400">driedee</span>.nl</span>
</div>
<p class="text-xs text-slate-400 leading-relaxed">
Van unieke interieurdecoratie en handige bureaugadgets tot industriële prototypes op maat. Geprint met professionele precisie op hypermoderne FDM- en SLA-printers.
</p>
<div class="flex items-center space-x-2 text-xs font-mono text-emerald-400 pt-1">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-ping"></span>
<span>Printers online & gereed</span>
</div>
</div>
<!-- Col 2: Snelle links -->
<div class="space-y-3">
<h3 class="text-xs font-bold uppercase tracking-wider text-white">Winkel & Producten</h3>
<ul class="space-y-2 text-xs text-slate-400">
<li><a href="{{ route('catalog') }}" class="hover:text-white transition">Alle 3D Prints</a></li>
<li><a href="{{ route('catalog', ['category' => 'home-interieur']) }}" class="hover:text-white transition">Home & Interieur</a></li>
<li><a href="{{ route('catalog', ['category' => 'bureau-tech']) }}" class="hover:text-white transition">Bureau & Tech</a></li>
<li><a href="{{ route('catalog', ['category' => 'gadgets-speelgoed']) }}" class="hover:text-white transition">Gadgets & Speelgoed</a></li>
<li><a href="{{ route('custom.request') }}" class="text-indigo-400 font-semibold hover:underline">Custom STL Uploaden</a></li>
</ul>
</div>
<!-- Col 3: Materialen Kennisbank -->
<div class="space-y-3">
<h3 class="text-xs font-bold uppercase tracking-wider text-white">3D Print Materialen</h3>
<ul class="space-y-2 text-xs text-slate-400">
<li class="flex items-center space-x-1.5"><span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span><span><strong>PLA+</strong>: Biologisch & strak</span></li>
<li class="flex items-center space-x-1.5"><span class="w-1.5 h-1.5 rounded-full bg-blue-400"></span><span><strong>PETG</strong>: Hitte- & slagvast</span></li>
<li class="flex items-center space-x-1.5"><span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span><span><strong>TPU</strong>: Flexibel rubber</span></li>
<li class="flex items-center space-x-1.5"><span class="w-1.5 h-1.5 rounded-full bg-purple-400"></span><span><strong>Resin</strong>: Ultra-fijne details</span></li>
</ul>
</div>
<!-- Col 4: Contact & Zekerheden -->
<div class="space-y-3">
<h3 class="text-xs font-bold uppercase tracking-wider text-white">Contact & Klantenservice</h3>
<p class="text-xs text-slate-400">Heb je een speciale printopdracht of grote batch nodig?</p>
<div class="p-3.5 rounded-xl bg-slate-800/80 border border-slate-700/60 text-xs space-y-1.5">
<div class="flex items-center space-x-2 text-slate-300">
<i data-lucide="mail" class="w-3.5 h-3.5 text-indigo-400"></i>
<span>info@jouwdriedee.nl</span>
</div>
<div class="flex items-center space-x-2 text-slate-300">
<i data-lucide="map-pin" class="w-3.5 h-3.5 text-indigo-400"></i>
<span>Nederland (Verzending NL & BE)</span>
</div>
</div>
</div>
</div>
<div class="pt-8 flex flex-col sm:flex-row items-center justify-between text-xs text-slate-500 gap-4">
<p>&copy; {{ date('Y') }} Jouwdriedee.nl. Alle rechten voorbehouden.</p>
<div class="flex items-center space-x-4">
<span>Veilig betalen met iDEAL & Bancontact</span>
<span>•</span>
<span>Coolify Docker Deployed</span>
</div>
</div>
</div>
</footer>
</body>
</html>
+155
View File
@@ -0,0 +1,155 @@
@extends('layouts.app')
@section('title', '3D Print Catalogus | Jouwdriedee.nl')
@section('content')
<div class="bg-white border-b border-slate-200 py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-black text-slate-900 tracking-tight">Onze 3D Prints & Modellen</h1>
<p class="text-xs text-slate-500 mt-1">Selecteer een categorie of filter op materiaal om jouw perfecte print te vinden.</p>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Filters Sidebar -->
<aside class="space-y-6">
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-6">
<div class="flex items-center justify-between pb-3 border-b border-slate-100">
<span class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-1.5">
<i data-lucide="filter" class="w-4 h-4 text-indigo-600"></i>
<span>Filters</span>
</span>
@if(request()->hasAny(['category', 'material', 'q', 'sort']))
<a href="{{ route('catalog') }}" class="text-[11px] font-bold text-rose-500 hover:underline">Wissen</a>
@endif
</div>
<!-- Zoekveld -->
<form action="{{ route('catalog') }}" method="GET" class="space-y-4">
@if(request('category')) <input type="hidden" name="category" value="{{ request('category') }}"> @endif
@if(request('material')) <input type="hidden" name="material" value="{{ request('material') }}"> @endif
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1.5">Zoeken</label>
<div class="relative">
<input
type="text"
name="q"
value="{{ request('q') }}"
placeholder="Zoek op naam..."
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-8 pr-3 py-2 text-xs text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<i data-lucide="search" class="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2"></i>
</div>
</div>
<!-- Categorie filter -->
<div>
<label class="block text-xs font-semibold text-slate-700 mb-2">Categorieën</label>
<div class="space-y-1">
<a href="{{ route('catalog', array_merge(request()->except('category', 'page'), [])) }}" class="flex items-center justify-between px-3 py-1.5 rounded-lg text-xs {{ !request('category') ? 'bg-indigo-50 text-indigo-700 font-bold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>Alle categorieën</span>
</a>
@foreach($categories as $cat)
<a href="{{ route('catalog', array_merge(request()->except('category', 'page'), ['category' => $cat->slug])) }}" class="flex items-center justify-between px-3 py-1.5 rounded-lg text-xs {{ request('category') === $cat->slug ? 'bg-indigo-50 text-indigo-700 font-bold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>{{ $cat->name }}</span>
</a>
@endforeach
</div>
</div>
<!-- Materiaal filter -->
<div>
<label class="block text-xs font-semibold text-slate-700 mb-2">Materiaal</label>
<div class="space-y-1">
<a href="{{ route('catalog', array_merge(request()->except('material', 'page'), [])) }}" class="flex items-center justify-between px-3 py-1.5 rounded-lg text-xs {{ !request('material') ? 'bg-indigo-50 text-indigo-700 font-bold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>Alle materialen</span>
</a>
@foreach($materials as $mat)
<a href="{{ route('catalog', array_merge(request()->except('material', 'page'), ['material' => $mat->code])) }}" class="flex items-center justify-between px-3 py-1.5 rounded-lg text-xs {{ request('material') === $mat->code ? 'bg-indigo-50 text-indigo-700 font-bold' : 'text-slate-600 hover:bg-slate-50' }}">
<span>{{ $mat->name }}</span>
<span class="text-[10px] text-slate-400 font-mono">{{ strtoupper($mat->code) }}</span>
</a>
@endforeach
</div>
</div>
<!-- Sortering -->
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1.5">Sorteren</label>
<select name="sort" onchange="this.form.submit()" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="newest" {{ request('sort') === 'newest' ? 'selected' : '' }}>Nieuwste eerst</option>
<option value="price_asc" {{ request('sort') === 'price_asc' ? 'selected' : '' }}>Prijs: Laag naar hoog</option>
<option value="price_desc" {{ request('sort') === 'price_desc' ? 'selected' : '' }}>Prijs: Hoog naar laag</option>
</select>
</div>
</form>
</div>
</aside>
<!-- Producten Overzicht -->
<div class="lg:col-span-3 space-y-6">
@if($products->isEmpty())
<div class="p-12 text-center rounded-3xl bg-white border border-slate-200 space-y-4">
<div class="w-16 h-16 rounded-2xl bg-indigo-50 text-indigo-600 flex items-center justify-center mx-auto">
<i data-lucide="package-search" class="w-8 h-8"></i>
</div>
<h3 class="text-base font-bold text-slate-900">Geen artikelen gevonden</h3>
<p class="text-xs text-slate-500 max-w-sm mx-auto">Er zijn geen prints gevonden die voldoen aan je huidige filters. Probeer een andere zoekopdracht of wis je filters.</p>
<a href="{{ route('catalog') }}" class="inline-flex px-4 py-2 rounded-xl bg-indigo-600 text-white text-xs font-bold hover:bg-indigo-500 transition">Alle prints bekijken</a>
</div>
@else
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($products as $product)
<div class="group rounded-3xl bg-white border border-slate-200 overflow-hidden hover:border-indigo-400 hover:shadow-lg transition flex flex-col justify-between">
<div>
<div class="relative h-56 bg-slate-100 overflow-hidden">
<img
src="{{ $product->image_url }}"
alt="{{ $product->name }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
>
@if($product->category)
<span class="absolute top-3 left-3 px-2 py-0.5 rounded-lg bg-white/90 backdrop-blur-xs text-[10px] font-bold text-slate-800 shadow-2xs">
{{ $product->category->name }}
</span>
@endif
</div>
<div class="p-5">
<h3 class="text-sm font-bold text-slate-900 group-hover:text-indigo-600 transition">
<a href="{{ route('product.show', $product->slug) }}">
{{ $product->name }}
</a>
</h3>
<p class="text-xs text-slate-500 mt-1 line-clamp-2 leading-relaxed">
{{ $product->short_description }}
</p>
</div>
</div>
<div class="p-5 pt-0 border-t border-slate-100 mt-2 flex items-center justify-between">
<div>
<span class="text-[10px] text-slate-400 block font-semibold">Vanaf</span>
<span class="text-base font-black text-slate-900 font-mono">{{ $product->formatted_price }}</span>
</div>
<a href="{{ route('product.show', $product->slug) }}" class="px-3.5 py-2 rounded-xl bg-slate-100 hover:bg-indigo-600 hover:text-white text-slate-800 text-xs font-bold transition flex items-center space-x-1">
<span>Opties</span>
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
</a>
</div>
</div>
@endforeach
</div>
<div class="pt-4">
{{ $products->links() }}
</div>
@endif
</div>
</div>
</div>
@endsection
+94
View File
@@ -0,0 +1,94 @@
@extends('layouts.app')
@section('title', 'Eigen 3D Model Printen (STL / STEP Upload) | Jouwdriedee.nl')
@section('content')
<div class="bg-gradient-to-b from-slate-900 to-indigo-950 text-white py-12">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-3">
<span class="px-3 py-1 rounded-full bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 text-xs font-bold inline-flex items-center space-x-1.5">
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
<span>3D Print op Aanvraag</span>
</span>
<h1 class="text-3xl sm:text-4xl font-black tracking-tight">Upload Jouw Eigen 3D-Bestand</h1>
<p class="text-xs sm:text-sm text-slate-300 max-w-xl mx-auto leading-relaxed">
Heb je een STL, STEP of OBJ bestand van Thingiverse, Printables of je eigen CAD-ontwerp? Upload het hieronder en ontvang binnen 24 uur een vrijblijvende prijsopgave.
</p>
</div>
</div>
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="p-8 rounded-3xl bg-white border border-slate-200 shadow-md space-y-6">
<form action="{{ route('custom.store') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
@csrf
<!-- Contactgegevens -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">Naam *</label>
<input type="text" name="name" required value="{{ old('name') }}" placeholder="Bijv. Jan de Vries" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">E-mailadres *</label>
<input type="email" name="email" required value="{{ old('email') }}" placeholder="jouw@email.nl" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">Telefoonnummer (optioneel)</label>
<input type="text" name="phone" value="{{ old('phone') }}" placeholder="06-12345678" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<!-- Bestand Uploaden -->
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">3D Model Bestand (.stl, .step, .obj, .zip)</label>
<div class="p-6 border-2 border-dashed border-slate-300 hover:border-indigo-500 rounded-2xl bg-slate-50/50 text-center transition cursor-pointer">
<input type="file" name="model_file" accept=".stl,.step,.stp,.obj,.3mf,.zip" class="w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-bold file:bg-indigo-600 file:text-white hover:file:bg-indigo-500 cursor-pointer">
<p class="text-[11px] text-slate-400 mt-2">Ondersteunde formaten: STL, STEP, OBJ, 3MF of ZIP (max 50 MB)</p>
</div>
</div>
<!-- Voorkeuren -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">Gewenst Materiaal</label>
<select name="preferred_material" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="Weet ik niet (Adviseer mij)">Weet ik niet (Adviseer mij)</option>
@foreach($materials as $mat)
<option value="{{ $mat->name }}">{{ $mat->name }} ({{ strtoupper($mat->code) }})</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">Voorkeurskleur</label>
<select name="preferred_color" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="Geen voorkeur">Geen voorkeur / In overleg</option>
@foreach($colors as $color)
<option value="{{ $color->name }}">{{ $color->name }}</option>
@endforeach
</select>
</div>
</div>
<!-- Beschrijving & Wensen -->
<div>
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1.5">Toelichting, Afmetingen & Aantal *</label>
<textarea name="description" rows="4" required placeholder="Vermeld eventuele gewenste afmetingen (bv. 10cm hoog), gewenst aantal en toepassing (binnen, buiten, waterdicht, mechanisch belast)..." class="w-full bg-slate-50 border border-slate-200 rounded-xl p-3.5 text-xs text-slate-900 focus:bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500"></textarea>
</div>
<button type="submit" class="w-full py-4 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs uppercase tracking-wider transition shadow-lg shadow-indigo-600/25 flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="send" class="w-4 h-4"></i>
<span>Aanvraag Vrijblijvend Verzenden</span>
</button>
<p class="text-center text-[11px] text-slate-400">
🔒 Jouw bestanden en gegevens worden uitsluitend gebruikt voor de offerteberekening en niet gedeeld met derden.
</p>
</form>
</div>
</div>
@endsection
+284
View File
@@ -0,0 +1,284 @@
@extends('layouts.app')
@section('title', 'Jouwdriedee.nl | Dé 3D Print Webshop & Printservice')
@section('content')
<!-- Hero Section -->
<section class="relative overflow-hidden bg-gradient-to-b from-slate-900 via-slate-900 to-indigo-950 text-white pt-16 pb-24 lg:pt-24 lg:pb-32">
<!-- Background Glow Elements -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-7xl h-full overflow-hidden pointer-events-none opacity-25">
<div class="absolute -top-40 left-1/4 w-96 h-96 bg-indigo-500 rounded-full blur-3xl"></div>
<div class="absolute top-1/3 right-10 w-80 h-80 bg-sky-400 rounded-full blur-3xl"></div>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<!-- Left Hero Content -->
<div class="lg:col-span-7 space-y-6 text-center lg:text-left">
<div class="inline-flex items-center space-x-2 px-3.5 py-1.5 rounded-full bg-indigo-900/60 border border-indigo-700/60 text-indigo-300 text-xs font-bold tracking-wide">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<span>High-Precision 3D Printing Studio</span>
</div>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight leading-tight sm:leading-none">
Jouw ideeën tastbaar gemaakt in <span class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 via-sky-300 to-teal-300">3D</span>
</h1>
<p class="text-base sm:text-lg text-slate-300 max-w-2xl mx-auto lg:mx-0 font-normal leading-relaxed">
Ontdek onze zorgvuldig ontworpen geometrische vazen, ergonomische bureau-gadgets en slimme tools. Of upload direct jouw eigen 3D-bestand voor professioneel maatwerk.
</p>
<!-- Action CTA's -->
<div class="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3.5 pt-2">
<a href="{{ route('catalog') }}" class="w-full sm:w-auto px-7 py-3.5 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-lg shadow-indigo-600/30 flex items-center justify-center space-x-2">
<span>Bekijk Onze Prints</span>
<i data-lucide="arrow-right" class="w-4 h-4"></i>
</a>
<a href="{{ route('custom.request') }}" class="w-full sm:w-auto px-7 py-3.5 rounded-2xl bg-white/10 hover:bg-white/15 border border-white/20 text-white font-bold text-sm transition backdrop-blur-xs flex items-center justify-center space-x-2">
<i data-lucide="upload-cloud" class="w-4 h-4 text-sky-400"></i>
<span>Eigen STL / 3D Model Printen</span>
</a>
</div>
<!-- Live stats / Badges -->
<div class="pt-6 border-t border-slate-800 grid grid-cols-3 gap-4 text-left max-w-lg mx-auto lg:mx-0">
<div>
<div class="text-xl sm:text-2xl font-black text-white font-mono">0.12 mm</div>
<div class="text-[11px] text-slate-400">Minimale laaghoogte</div>
</div>
<div>
<div class="text-xl sm:text-2xl font-black text-indigo-400 font-mono">4+ Types</div>
<div class="text-[11px] text-slate-400">PLA, PETG, TPU, Resin</div>
</div>
<div>
<div class="text-xl sm:text-2xl font-black text-emerald-400 font-mono">48-72u</div>
<div class="text-[11px] text-slate-400">Gemiddelde verzendtijd</div>
</div>
</div>
</div>
<!-- Right Hero Card / 3D Visual Preview -->
<div class="lg:col-span-5 relative">
<div class="relative rounded-3xl bg-gradient-to-br from-slate-800/90 to-slate-900/90 border border-slate-700/80 p-5 shadow-2xl backdrop-blur-xl">
<div class="relative h-72 sm:h-88 rounded-2xl overflow-hidden bg-slate-950">
<img
src="https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&w=1000&q=80"
alt="3D print vaas"
class="w-full h-full object-cover object-center opacity-90 hover:scale-105 transition-transform duration-700"
>
<div class="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent"></div>
<div class="absolute bottom-4 left-4 right-4 flex items-end justify-between">
<div>
<span class="px-2.5 py-1 rounded-lg bg-indigo-600/90 text-white font-mono text-[10px] font-bold uppercase tracking-wider">
Uitgelichte Print
</span>
<h3 class="text-lg font-bold text-white mt-1">Origami Geometrische Vaas</h3>
<p class="text-xs text-slate-300">Geprint in koper zijdeglans PLA+</p>
</div>
<div class="text-right">
<span class="text-xs text-slate-400 block">Vanaf</span>
<span class="text-lg font-black text-emerald-400 font-mono">€ 18,95</span>
</div>
</div>
</div>
<!-- Material tags preview -->
<div class="mt-4 flex flex-wrap gap-2 text-xs">
<span class="px-2.5 py-1 rounded-xl bg-slate-800 text-slate-300 border border-slate-700 flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span>100% Waterdicht mogelijk</span>
</span>
<span class="px-2.5 py-1 rounded-xl bg-slate-800 text-slate-300 border border-slate-700 flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-indigo-400"></span>
<span>Kleur naar wens</span>
</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- USP Bar -->
<section class="border-y border-slate-200 bg-white py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 text-slate-800">
<div class="flex items-start space-x-3.5">
<div class="w-10 h-10 rounded-xl bg-indigo-50 border border-indigo-100 flex items-center justify-center text-indigo-600 flex-shrink-0">
<i data-lucide="printer" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Print on Demand</h4>
<p class="text-xs text-slate-500 mt-0.5">Vers geprint voor jou in de exacte gewenste kleur.</p>
</div>
</div>
<div class="flex items-start space-x-3.5">
<div class="w-10 h-10 rounded-xl bg-sky-50 border border-sky-100 flex items-center justify-center text-sky-600 flex-shrink-0">
<i data-lucide="layers" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Topkwaliteit Filament</h4>
<p class="text-xs text-slate-500 mt-0.5">Uitsluitend A-merk PLA+, PETG en elastisch TPU.</p>
</div>
</div>
<div class="flex items-start space-x-3.5">
<div class="w-10 h-10 rounded-xl bg-emerald-50 border border-emerald-100 flex items-center justify-center text-emerald-600 flex-shrink-0">
<i data-lucide="truck" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Snelle Bezorging</h4>
<p class="text-xs text-slate-500 mt-0.5">Verzending binnen 2-3 werkdagen via PostNL.</p>
</div>
</div>
<div class="flex items-start space-x-3.5">
<div class="w-10 h-10 rounded-xl bg-amber-50 border border-amber-100 flex items-center justify-center text-amber-600 flex-shrink-0">
<i data-lucide="check-circle-2" class="w-5 h-5"></i>
</div>
<div>
<h4 class="text-xs font-bold text-slate-900 uppercase tracking-wider">Kwaliteitsgarantie</h4>
<p class="text-xs text-slate-500 mt-0.5">Elke print wordt met de hand geïnspecteerd en ontbraamd.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Categorieën Grid -->
<section class="py-16 bg-slate-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-8 gap-4">
<div>
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Collecties</span>
<h2 class="text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">Ontdek per Categorie</h2>
</div>
<a href="{{ route('catalog') }}" class="text-xs font-bold text-indigo-600 hover:text-indigo-800 flex items-center space-x-1">
<span>Alles bekijken</span>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</a>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
@foreach($categories as $category)
<a href="{{ route('catalog', ['category' => $category->slug]) }}" class="group p-5 rounded-2xl bg-white border border-slate-200 hover:border-indigo-400 hover:shadow-md transition text-center flex flex-col items-center">
<div class="w-14 h-14 rounded-2xl bg-indigo-50 border border-indigo-100 text-indigo-600 group-hover:bg-indigo-600 group-hover:text-white transition flex items-center justify-center mb-3 shadow-xs">
<i data-lucide="{{ $category->icon }}" class="w-7 h-7"></i>
</div>
<h3 class="text-xs font-bold text-slate-900 group-hover:text-indigo-600 transition">{{ $category->name }}</h3>
<p class="text-[11px] text-slate-500 mt-1 line-clamp-1">{{ $category->description }}</p>
</a>
@endforeach
</div>
</div>
</section>
<!-- Uitgelichte Prints / Product Showcase -->
<section class="py-16 bg-white border-t border-slate-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-10 gap-4">
<div>
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600">Populaire Artikelen</span>
<h2 class="text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">Direct te Bestellen</h2>
<p class="text-xs text-slate-500 mt-1">Kies je favoriete kleur en filamentsoort direct op de productpagina.</p>
</div>
<a href="{{ route('catalog') }}" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 text-xs font-bold transition flex items-center space-x-1.5 self-start sm:self-auto">
<span>Volledige Catalogus</span>
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
@foreach($featuredProducts as $product)
<div class="group rounded-3xl bg-slate-50/70 border border-slate-200 overflow-hidden hover:border-indigo-300 hover:shadow-xl transition flex flex-col">
<!-- Afbeelding & Badges -->
<div class="relative h-64 overflow-hidden bg-slate-200">
<img
src="{{ $product->image_url }}"
alt="{{ $product->name }}"
class="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-500"
>
<div class="absolute top-3.5 left-3.5 flex flex-col gap-1.5">
@if($product->category)
<span class="px-2.5 py-1 rounded-lg bg-white/95 backdrop-blur-xs text-slate-800 text-[10px] font-bold shadow-xs">
{{ $product->category->name }}
</span>
@endif
</div>
@if($product->print_time_hours)
<div class="absolute bottom-3 left-3 px-2 py-0.5 rounded-md bg-slate-900/80 backdrop-blur-xs text-white text-[10px] font-mono flex items-center space-x-1">
<i data-lucide="clock" class="w-3 h-3 text-sky-400"></i>
<span>{{ $product->print_time_hours }}u printtijd</span>
</div>
@endif
</div>
<!-- Inhoud -->
<div class="p-6 flex-1 flex flex-col justify-between">
<div>
<h3 class="text-base font-bold text-slate-900 group-hover:text-indigo-600 transition tracking-tight">
<a href="{{ route('product.show', $product->slug) }}">
{{ $product->name }}
</a>
</h3>
<p class="text-xs text-slate-500 mt-2 line-clamp-2 leading-relaxed">
{{ $product->short_description }}
</p>
</div>
<!-- Footer met prijs & actieknop -->
<div class="pt-5 mt-4 border-t border-slate-200/80 flex items-center justify-between">
<div>
<span class="text-[10px] text-slate-400 uppercase font-semibold block">Vanaf</span>
<span class="text-lg font-black text-slate-900 font-mono">
{{ $product->formatted_price }}
</span>
</div>
<a href="{{ route('product.show', $product->slug) }}" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition flex items-center space-x-1.5 shadow-2xs">
<span>Configureren</span>
<i data-lucide="sliders" class="w-3.5 h-3.5"></i>
</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
</section>
<!-- Custom 3D STL Upload Banner Section -->
<section class="py-16 bg-gradient-to-tr from-slate-900 via-indigo-950 to-slate-900 text-white relative overflow-hidden">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative">
<div class="p-8 sm:p-12 rounded-3xl bg-indigo-900/40 border border-indigo-700/50 backdrop-blur-md flex flex-col lg:flex-row items-center justify-between gap-8">
<div class="space-y-4 max-w-2xl text-center lg:text-left">
<span class="px-3 py-1 rounded-full bg-sky-500/20 text-sky-300 border border-sky-500/30 text-xs font-bold inline-flex items-center space-x-1.5">
<i data-lucide="file-code" class="w-3.5 h-3.5"></i>
<span>Custom Printservice (.STL / .STEP / .OBJ)</span>
</span>
<h2 class="text-2xl sm:text-3xl font-extrabold tracking-tight">
Heb je een eigen 3D-model dat je wilt laten printen?
</h2>
<p class="text-xs sm:text-sm text-indigo-200 leading-relaxed">
Upload je CAD- of STL-bestand via ons eenvoudige aanvraagformulier. Wij controleren de printbaarheid (slicing & oriëntatie), adviseren het beste materiaal en sturen je binnen 24 uur een scherpe offerte.
</p>
</div>
<a href="{{ route('custom.request') }}" class="px-8 py-4 rounded-2xl bg-white hover:bg-slate-100 text-indigo-950 font-black text-xs uppercase tracking-wider transition shadow-xl flex items-center space-x-2 flex-shrink-0">
<i data-lucide="upload" class="w-4 h-4 text-indigo-600"></i>
<span>Direct Bestand Uploaden</span>
</a>
</div>
</div>
</section>
@endsection
+289
View File
@@ -0,0 +1,289 @@
@extends('layouts.app')
@section('title', $product->name . ' | Jouwdriedee.nl')
@section('content')
<!-- Breadcrumbs -->
<div class="bg-white border-b border-slate-200 py-3.5">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-xs text-slate-500 flex items-center space-x-2">
<a href="{{ route('home') }}" class="hover:text-indigo-600">Home</a>
<span>/</span>
<a href="{{ route('catalog') }}" class="hover:text-indigo-600">Catalogus</a>
@if($product->category)
<span>/</span>
<a href="{{ route('catalog', ['category' => $product->category->slug]) }}" class="hover:text-indigo-600">{{ $product->category->name }}</a>
@endif
<span>/</span>
<span class="font-bold text-slate-900 truncate">{{ $product->name }}</span>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10"
x-data="{
basePrice: {{ $product->base_price }},
selectedMaterial: '{{ $product->available_materials[0] ?? 'pla' }}',
materialMultipliers: {
@foreach($allMaterials as $code => $mat)
'{{ $code }}': {{ $mat->price_multiplier }},
@endforeach
},
selectedColor: '{{ $product->available_colors[0] ?? 'Mat Zwart' }}',
selectedScale: '100%',
scaleMultipliers: {
'100%': 1.0,
'150%': 1.45,
'200%': 1.95
},
selectedInfill: 'Standaard (20%)',
quantity: 1,
get calculatedPrice() {
let mult = this.materialMultipliers[this.selectedMaterial] || 1.0;
let scaleMult = this.scaleMultipliers[this.selectedScale] || 1.0;
let unit = (this.basePrice * mult * scaleMult);
return (unit * this.quantity).toFixed(2).replace('.', ',');
},
get unitPriceFormatted() {
let mult = this.materialMultipliers[this.selectedMaterial] || 1.0;
let scaleMult = this.scaleMultipliers[this.selectedScale] || 1.0;
let unit = (this.basePrice * mult * scaleMult);
return unit.toFixed(2).replace('.', ',');
}
}">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<!-- Linkerkant: Productgalerij & 3D Specificaties -->
<div class="lg:col-span-6 space-y-6">
<div class="rounded-3xl bg-white border border-slate-200 overflow-hidden shadow-sm">
<div class="relative h-96 sm:h-112 bg-slate-100 flex items-center justify-center overflow-hidden">
<img
src="{{ $product->image_url }}"
alt="{{ $product->name }}"
class="w-full h-full object-cover object-center"
>
<div class="absolute top-4 left-4 flex flex-col gap-2">
@if($product->is_featured)
<span class="px-3 py-1 rounded-full bg-indigo-600 text-white font-mono text-xs font-bold shadow-xs">
Populair
</span>
@endif
</div>
</div>
</div>
<!-- 3D Print Technische Specificaties Card -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-4">
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider flex items-center space-x-2">
<i data-lucide="cpu" class="w-4 h-4 text-indigo-600"></i>
<span>Print- & Technische Specificaties</span>
</h3>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="text-slate-400 block text-[10px] uppercase font-bold">Afmetingen</span>
<span class="font-bold text-slate-800 font-mono mt-0.5 block">{{ $product->dimensions ?? 'Standaard' }}</span>
</div>
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="text-slate-400 block text-[10px] uppercase font-bold">Gewicht</span>
<span class="font-bold text-slate-800 font-mono mt-0.5 block">~{{ $product->weight_grams ?? '80' }} gram</span>
</div>
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="text-slate-400 block text-[10px] uppercase font-bold">Printtijd</span>
<span class="font-bold text-slate-800 font-mono mt-0.5 block">{{ $product->print_time_hours ?? '4.0' }} uur</span>
</div>
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="text-slate-400 block text-[10px] uppercase font-bold">Laaghoogte</span>
<span class="font-bold text-slate-800 font-mono mt-0.5 block">0.16 mm</span>
</div>
</div>
<div class="pt-2 flex items-center justify-between text-xs text-slate-500 border-t border-slate-100">
<span class="flex items-center space-x-1.5">
<i data-lucide="check" class="w-3.5 h-3.5 text-emerald-500"></i>
<span>Gekalibreerd op Bambu Lab X1 Carbon</span>
</span>
<span class="flex items-center space-x-1.5">
<i data-lucide="check" class="w-3.5 h-3.5 text-emerald-500"></i>
<span>Handmatig nagekeken & ontbraamd</span>
</span>
</div>
</div>
<!-- Productomschrijving -->
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-2xs space-y-3">
<h3 class="text-sm font-bold text-slate-900">Over dit product</h3>
<p class="text-xs text-slate-600 leading-relaxed whitespace-pre-line">
{{ $product->description }}
</p>
</div>
</div>
<!-- Rechterkant: Interactive 3D Configurator & Bestelmodule -->
<div class="lg:col-span-6 space-y-6">
<div class="p-7 rounded-3xl bg-white border border-slate-200 shadow-md space-y-6">
<div>
@if($product->category)
<span class="text-xs font-bold uppercase tracking-wider text-indigo-600 block mb-1">
{{ $product->category->name }}
</span>
@endif
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">{{ $product->name }}</h1>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">{{ $product->short_description }}</p>
</div>
<!-- Live Prijs Display -->
<div class="p-4 rounded-2xl bg-indigo-50/60 border border-indigo-100 flex items-center justify-between">
<div>
<span class="text-[11px] text-slate-500 block font-medium">Berekende stukprijs</span>
<div class="flex items-baseline space-x-2">
<span class="text-2xl font-black text-indigo-600 font-mono">€ <span x-text="unitPriceFormatted"></span></span>
<span class="text-xs text-slate-400 font-normal">incl. btw</span>
</div>
</div>
<div class="text-right">
<span class="px-2.5 py-1 rounded-full bg-emerald-100 text-emerald-800 text-[11px] font-bold inline-flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>Direct op voorraad / Print-klaar</span>
</span>
</div>
</div>
<!-- Bestelformulier met Customizer -->
<form action="{{ route('cart.add') }}" method="POST" class="space-y-6">
@csrf
<input type="hidden" name="product_id" value="{{ $product->id }}">
<input type="hidden" name="material" x-model="selectedMaterial">
<input type="hidden" name="color" x-model="selectedColor">
<input type="hidden" name="scale" x-model="selectedScale">
<input type="hidden" name="infill" x-model="selectedInfill">
<input type="hidden" name="quantity" x-model="quantity">
<!-- 1. Materiaal Keuze -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-xs font-bold text-slate-800 uppercase tracking-wider">1. Kies Filament / Materiaal</label>
<span class="text-xs font-mono font-bold text-indigo-600" x-text="selectedMaterial.toUpperCase()"></span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2.5">
@foreach($product->available_materials ?? ['pla'] as $matCode)
@php $mat = $allMaterials[$matCode] ?? null; @endphp
@if($mat)
<button
type="button"
@click="selectedMaterial = '{{ $mat->code }}'"
:class="selectedMaterial === '{{ $mat->code }}' ? 'border-indigo-600 bg-indigo-50/50 ring-2 ring-indigo-500/20 text-indigo-950' : 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'"
class="p-3 rounded-2xl border text-left transition flex flex-col justify-between cursor-pointer"
>
<div class="flex items-center justify-between">
<span class="font-bold text-xs">{{ $mat->name }}</span>
@if($mat->price_multiplier > 1.0)
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 font-bold">+{{ round(($mat->price_multiplier - 1) * 100) }}%</span>
@endif
</div>
<p class="text-[10px] text-slate-500 mt-1 line-clamp-1">{{ $mat->description }}</p>
</button>
@endif
@endforeach
</div>
</div>
<!-- 2. Kleur Selectie (Swatches) -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-xs font-bold text-slate-800 uppercase tracking-wider">2. Kies Printkleur</label>
<span class="text-xs font-bold text-slate-800" x-text="selectedColor"></span>
</div>
<div class="flex flex-wrap gap-2.5">
@foreach($allColors as $c)
<button
type="button"
@click="selectedColor = '{{ $c->name }}'"
:class="selectedColor === '{{ $c->name }}' ? 'ring-2 ring-indigo-600 ring-offset-2 scale-105' : 'hover:scale-105 border-slate-300'"
class="w-8 h-8 rounded-full border shadow-xs transition flex items-center justify-center cursor-pointer relative group"
style="background-color: {{ $c->hex }};"
title="{{ $c->name }}"
>
<template x-if="selectedColor === '{{ $c->name }}'">
<i data-lucide="check" class="w-4 h-4 {{ in_array($c->name, ['Sneeuwwit', 'Galaxy Grijs']) ? 'text-slate-900' : 'text-white' }} stroke-[3]"></i>
</template>
</button>
@endforeach
</div>
</div>
<!-- 3. Formaat & Schaal -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="text-xs font-bold text-slate-800 uppercase tracking-wider">3. Schaal / Grootte</label>
<span class="text-xs font-mono font-bold text-slate-700" x-text="selectedScale"></span>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<button
type="button"
@click="selectedScale = '100%'"
:class="selectedScale === '100%' ? 'border-indigo-600 bg-indigo-50 font-bold text-indigo-900' : 'border-slate-200 text-slate-600 hover:bg-slate-50'"
class="p-2.5 rounded-xl border text-center transition cursor-pointer"
>
<span>100% (Normaal)</span>
</button>
<button
type="button"
@click="selectedScale = '150%'"
:class="selectedScale === '150%' ? 'border-indigo-600 bg-indigo-50 font-bold text-indigo-900' : 'border-slate-200 text-slate-600 hover:bg-slate-50'"
class="p-2.5 rounded-xl border text-center transition cursor-pointer"
>
<span>150% (+45%)</span>
</button>
<button
type="button"
@click="selectedScale = '200%'"
:class="selectedScale === '200%' ? 'border-indigo-600 bg-indigo-50 font-bold text-indigo-900' : 'border-slate-200 text-slate-600 hover:bg-slate-50'"
class="p-2.5 rounded-xl border text-center transition cursor-pointer"
>
<span>200% (+95%)</span>
</button>
</div>
</div>
<!-- Aantal en Toevoegen Knop -->
<div class="pt-4 border-t border-slate-100 space-y-4">
<div class="flex items-center space-x-3">
<div class="flex items-center border border-slate-200 rounded-2xl bg-slate-50 overflow-hidden">
<button type="button" @click="if (quantity > 1) quantity--" class="px-3.5 py-3 hover:bg-slate-200 transition text-slate-600 font-bold cursor-pointer">-</button>
<span class="px-4 py-3 font-mono font-bold text-xs text-slate-900" x-text="quantity"></span>
<button type="button" @click="if (quantity < 50) quantity++" class="px-3.5 py-3 hover:bg-slate-200 transition text-slate-600 font-bold cursor-pointer">+</button>
</div>
<button type="submit" class="flex-1 py-3.5 px-6 rounded-2xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-lg shadow-indigo-600/25 flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="shopping-bag" class="w-4 h-4"></i>
<span>In Winkelwagen • € <span x-text="calculatedPrice"></span></span>
</button>
</div>
<div class="flex items-center justify-center space-x-4 text-[11px] text-slate-500 pt-1">
<span class="flex items-center space-x-1">
<i data-lucide="truck" class="w-3.5 h-3.5 text-slate-400"></i>
<span>Gratis verzending vanaf € 50,-</span>
</span>
<span>•</span>
<span class="flex items-center space-x-1">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5 text-slate-400"></i>
<span>14 dagen bedenktijd</span>
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
+25
View File
@@ -0,0 +1,25 @@
<?php
use App\Http\Controllers\CartController;
use App\Http\Controllers\CheckoutController;
use App\Http\Controllers\ShopController;
use Illuminate\Support\Facades\Route;
// Shop & Product routes
Route::get('/', [ShopController::class, 'index'])->name('home');
Route::get('/catalogus', [ShopController::class, 'catalog'])->name('catalog');
Route::get('/product/{slug}', [ShopController::class, 'show'])->name('product.show');
Route::get('/custom-3d-print', [ShopController::class, 'customRequest'])->name('custom.request');
Route::post('/custom-3d-print', [ShopController::class, 'storeCustomRequest'])->name('custom.store');
// Winkelwagen
Route::get('/winkelwagen', [CartController::class, 'index'])->name('cart.index');
Route::post('/winkelwagen/toevoegen', [CartController::class, 'add'])->name('cart.add');
Route::post('/winkelwagen/bijwerken', [CartController::class, 'update'])->name('cart.update');
Route::delete('/winkelwagen/verwijderen/{key}', [CartController::class, 'remove'])->name('cart.remove');
Route::get('/winkelwagen/aantal', [CartController::class, 'count'])->name('cart.count');
// Afrekenen & Bevestiging
Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.index');
Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store');
Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success');
+4
View File
@@ -0,0 +1,4 @@
*
!private/
!public/
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+10
View File
@@ -0,0 +1,10 @@
compiled.php
config.php
down
events.scanned.php
lsp-*.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json
+4
View File
@@ -0,0 +1,4 @@
*
!data/
!locks/
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
fonts: [
bunny('Instrument Sans', {
weights: [400, 500, 600],
}),
],
}),
tailwindcss(),
],
server: {
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
});