Initial commit
This commit is contained in:
@@ -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
|
||||||
@@ -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}"
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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"]
|
||||||
|
|
||||||
@@ -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** → **Application** → **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` → persistente SQLite database
|
||||||
|
* `/app/storage/app/public` → geüploade 3D bestanden
|
||||||
|
7. **Domein:** Koppel `https://jouwdriedee.nl`. Coolify regelt automatisch het SSL-certificaat (Let's Encrypt).
|
||||||
|
8. Klik op **Deploy**!
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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, ',', '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, ',', '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
];
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+126
@@ -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
@@ -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),
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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
@@ -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')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|
||||||
Generated
+2416
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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());
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Alpine from 'alpinejs';
|
||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
|
|
||||||
|
window.Alpine = Alpine;
|
||||||
|
Alpine.start();
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
createIcons({ icons });
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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">× {{ $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
|
||||||
|
|
||||||
@@ -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">× {{ $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
|
||||||
|
|
||||||
@@ -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 →</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>© {{ 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>
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
@@ -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');
|
||||||
@@ -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');
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!private/
|
||||||
|
!public/
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
compiled.php
|
||||||
|
config.php
|
||||||
|
down
|
||||||
|
events.scanned.php
|
||||||
|
lsp-*.php
|
||||||
|
maintenance.php
|
||||||
|
routes.php
|
||||||
|
routes.scanned.php
|
||||||
|
schedule-*
|
||||||
|
services.json
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!data/
|
||||||
|
!locks/
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
|
||||||
|
abstract class TestCase extends BaseTestCase
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/**'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user