Compare commits

..

8 commits

19 changed files with 354 additions and 463 deletions

123
Makefile
View file

@ -1,123 +0,0 @@
# Dish Planner - Docker Commands
.PHONY: help
help: ## Show this help message
@echo "Dish Planner - Docker Management"
@echo ""
@echo "Usage: make [command]"
@echo ""
@echo "Available commands:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
# Development Commands
.PHONY: dev
dev: ## Start development environment
docker compose up -d
@echo "Development server running at http://localhost:8000"
@echo "Mailhog available at http://localhost:8025"
.PHONY: dev-build
dev-build: ## Build and start development environment
docker compose build
docker compose up -d
.PHONY: dev-stop
dev-stop: ## Stop development environment
docker compose down
.PHONY: dev-clean
dev-clean: ## Stop and remove volumes (CAUTION: removes database)
docker compose down -v
.PHONY: logs
logs: ## Show application logs
docker compose logs -f app
.PHONY: logs-db
logs-db: ## Show database logs
docker compose logs -f db
# Production Commands
.PHONY: prod-build
prod-build: ## Build production image for Codeberg
./bin/build-push.sh
.PHONY: prod-build-tag
prod-build-tag: ## Build with specific tag (usage: make prod-build-tag TAG=v1.0.0)
./bin/build-push.sh $(TAG)
.PHONY: prod-login
prod-login: ## Login to Codeberg registry
podman login codeberg.org
# Laravel Commands
.PHONY: artisan
artisan: ## Run artisan command (usage: make artisan cmd="migrate")
docker compose exec app php artisan $(cmd)
.PHONY: composer
composer: ## Run composer command (usage: make composer cmd="require package")
docker compose exec app composer $(cmd)
.PHONY: npm
npm: ## Run npm command (usage: make npm cmd="install package")
docker compose exec app npm $(cmd)
.PHONY: migrate
migrate: ## Run database migrations
docker compose exec app php artisan migrate
.PHONY: seed
seed: ## Seed the database
docker compose exec app php artisan db:seed
.PHONY: fresh
fresh: ## Fresh migrate and seed
docker compose exec app php artisan migrate:fresh --seed
.PHONY: tinker
tinker: ## Start Laravel tinker
docker compose exec app php artisan tinker
.PHONY: test
test: ## Run tests
docker compose exec app php artisan test
# Utility Commands
.PHONY: shell
shell: ## Enter app container shell
docker compose exec app sh
.PHONY: db-shell
db-shell: ## Enter database shell
docker compose exec db mariadb -u dishplanner -pdishplanner dishplanner
.PHONY: clear
clear: ## Clear all Laravel caches
docker compose exec app php artisan cache:clear
docker compose exec app php artisan config:clear
docker compose exec app php artisan route:clear
docker compose exec app php artisan view:clear
.PHONY: optimize
optimize: ## Optimize Laravel for production
docker compose exec app php artisan config:cache
docker compose exec app php artisan route:cache
docker compose exec app php artisan view:cache
docker compose exec app php artisan livewire:discover
# Installation
.PHONY: install
install: ## First time setup
@echo "Setting up Dish Planner..."
@cp -n .env.example .env || true
@echo "Generating application key..."
@docker compose build
@docker compose up -d
@sleep 5
@docker compose exec app php artisan key:generate
@docker compose exec app php artisan migrate
@echo ""
@echo "✅ Installation complete!"
@echo "Access the app at: http://localhost:8000"
@echo "Create your first planner and user to get started."

343
README.md
View file

@ -11,285 +11,118 @@ ## ✨ Features
- **Dark theme UI** - Modern interface with purple/pink accents
- **Single container deployment** - Simplified hosting with FrankenPHP
## 🚀 Quick Start
## 🚀 Self-hosting
### Prerequisites
- Docker and Docker Compose
- Make (optional, for convenience commands)
The production image is available at `codeberg.org/dish-planner/app:latest`.
### First Time Setup
### docker-compose.yml
```bash
# Clone the repository
git clone https://github.com/yourusername/dish-planner.git
cd dish-planner
```yaml
services:
app:
image: codeberg.org/dish-planner/app:latest
container_name: dishplanner_app
restart: always
ports:
- "8000:8000"
environment:
APP_KEY: "${APP_KEY}"
APP_URL: "${APP_URL}"
DB_DATABASE: "${DB_DATABASE}"
DB_USERNAME: "${DB_USERNAME}"
DB_PASSWORD: "${DB_PASSWORD}"
MAIL_HOST: "${MAIL_HOST:-}"
MAIL_PORT: "${MAIL_PORT:-587}"
MAIL_USERNAME: "${MAIL_USERNAME:-}"
MAIL_PASSWORD: "${MAIL_PASSWORD:-}"
MAIL_FROM_ADDRESS: "${MAIL_FROM_ADDRESS:-noreply@example.com}"
volumes:
- app_storage:/app/storage
depends_on:
- db
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/up"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Quick install with Make
make install
db:
image: mariadb:11
container_name: dishplanner_db
restart: always
environment:
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
volumes:
- db_data:/var/lib/mysql
# Or manually:
cp .env.example .env
docker compose build
docker compose up -d
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
volumes:
db_data:
app_storage:
```
The application will be available at **http://localhost:8000**
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `APP_KEY` | Yes | Encryption key. Generate with: `echo "base64:$(openssl rand -base64 32)"` |
| `APP_URL` | Yes | Your domain (e.g., `https://meals.example.com`) |
| `DB_DATABASE` | Yes | Database name |
| `DB_USERNAME` | Yes | Database user |
| `DB_PASSWORD` | Yes | Database password |
| `DB_ROOT_PASSWORD` | Yes | MariaDB root password |
| `MAIL_HOST` | No | SMTP host for email notifications |
| `MAIL_PORT` | No | SMTP port (default: 587) |
| `MAIL_USERNAME` | No | SMTP username |
| `MAIL_PASSWORD` | No | SMTP password |
| `MAIL_FROM_ADDRESS` | No | From address for emails |
## 🔧 Development
### Starting the Development Environment
### NixOS / Nix
```bash
# Start all services
make dev
# Or with Docker Compose directly
docker compose up -d
git clone https://codeberg.org/dish-planner/app.git
cd dish-planner
nix-shell
```
**Available services:**
- **App**: http://localhost:8000 (Laravel + FrankenPHP)
- **Vite**: http://localhost:5173 (Asset hot-reload)
- **Mailhog**: http://localhost:8025 (Email testing)
- **Database**: localhost:3306 (MariaDB)
The shell will display available commands and optionally start the containers for you.
### Common Development Commands
#### Available Commands
```bash
# View logs
make logs # App logs
make logs-db # Database logs
| Command | Description |
|---------|-------------|
| `dev-up` | Start development environment |
| `dev-down` | Stop development environment |
| `dev-restart` | Restart containers |
| `dev-rebuild` | Full rebuild (removes volumes) |
| `dev-rebuild-quick` | Quick rebuild (keeps volumes) |
| `dev-logs [service]` | Follow logs |
| `dev-shell` | Enter app container |
| `dev-artisan <cmd>` | Run artisan commands |
| `dev-fix-permissions` | Fix Docker-created file permissions |
# Laravel commands
make artisan cmd="migrate" # Run artisan commands
make tinker # Start Laravel tinker
make test # Run tests
#### Services
# Database
make migrate # Run migrations
make seed # Seed database
make fresh # Fresh migrate with seeds
| Service | URL |
|---------|-----|
| App | http://localhost:8000 |
| Vite | http://localhost:5173 |
| Mailhog | http://localhost:8025 |
| MariaDB | localhost:3306 |
# Testing
make test # Run tests
composer test:coverage-html # Run tests with coverage report (generates coverage/index.html)
### Other Platforms
# Utilities
make shell # Enter app container
make db-shell # Enter database shell
make clear # Clear all caches
```
### Project Structure
```
dish-planner/
├── app/
│ ├── Livewire/ # Livewire components
│ │ ├── Auth/ # Authentication
│ │ ├── Dishes/ # Dish management
│ │ ├── Schedule/ # Schedule calendar
│ │ └── Users/ # User management
│ └── Models/ # Eloquent models
├── resources/
│ └── views/
│ └── livewire/ # Livewire component views
├── docker-compose.yml # Development environment
├── docker-compose.prod.yml # Production environment
├── Dockerfile # Production image
├── Dockerfile.dev # Development image
└── Makefile # Convenience commands
```
## 🚢 Production Deployment
### Building for Production
```bash
# Build production image
make prod-build
# Start production environment
make prod
# Or with Docker Compose
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
```
### Production Environment Variables
Required environment variables for production:
```env
# Required - Generate APP_KEY (see instructions below)
APP_KEY=base64:your-generated-key-here
APP_URL=https://your-domain.com
# Database Configuration
DB_DATABASE=dishplanner
DB_USERNAME=dishplanner
DB_PASSWORD=strong-password-here
DB_ROOT_PASSWORD=strong-root-password
# Optional Email Configuration
MAIL_HOST=your-smtp-host
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_FROM_ADDRESS=noreply@your-domain.com
```
#### Generating APP_KEY
The APP_KEY is critical for encryption and must be kept consistent across deployments. Generate one using any of these methods:
**Option 1: Using OpenSSL (Linux/Mac/Windows with Git Bash)**
```bash
echo "base64:$(openssl rand -base64 32)"
```
**Option 2: Using Node.js (Cross-platform)**
```bash
node -e "console.log('base64:' + require('crypto').randomBytes(32).toString('base64'))"
```
**Option 3: Using Python (Cross-platform)**
```bash
python -c "import base64, os; print('base64:' + base64.b64encode(os.urandom(32)).decode())"
```
**Option 4: Online Generator**
Generate a random 32-character string at https://randomkeygen.com/ and prepend with `base64:`
⚠️ **Important**: Save this key securely! If lost, you won't be able to decrypt existing data.
### Deployment with DockGE
The production setup is optimized for DockGE deployment with just 2 containers:
1. **app** - Laravel application with FrankenPHP
2. **db** - MariaDB database
Simply import the `docker-compose.prod.yml` into DockGE and configure your environment variables.
## 🛠️ Technology Stack
- **Backend**: Laravel 12 with Livewire 3
- **Web Server**: FrankenPHP (PHP 8.3 + Caddy)
- **Database**: MariaDB 11
- **Frontend**: Blade + Livewire + Alpine.js
- **Styling**: TailwindCSS with custom dark theme
- **Assets**: Vite for bundling
## 📦 Docker Architecture
### Development (`docker-compose.yml`)
- **Hot reload** - Volume mounts for live code editing
- **Debug tools** - Xdebug configured for debugging
- **Email testing** - Mailhog for capturing emails
- **Asset watching** - Vite dev server for instant updates
### Production (`docker-compose.prod.yml`)
- **Optimized** - Multi-stage builds with caching
- **Secure** - No debug tools, proper permissions
- **Health checks** - Automatic container monitoring
- **Single container** - FrankenPHP serves everything
## 🔨 Make Commands Reference
```bash
# Development
make dev # Start development environment
make dev-build # Build and start development
make dev-stop # Stop development environment
make dev-clean # Stop and remove volumes (CAUTION)
# Production
make prod # Start production environment
make prod-build # Build production image
make prod-stop # Stop production environment
make prod-logs # Show production logs
# Laravel
make artisan cmd="..." # Run artisan command
make composer cmd="..." # Run composer command
make npm cmd="..." # Run npm command
make migrate # Run migrations
make seed # Seed database
make fresh # Fresh migrate and seed
make tinker # Start tinker session
make test # Run tests
# Utilities
make shell # Enter app container
make db-shell # Enter database shell
make logs # Show app logs
make logs-db # Show database logs
make clear # Clear all caches
make optimize # Optimize for production
```
## 🐛 Troubleshooting
### Container won't start
```bash
# Check logs
docker compose logs app
# Rebuild containers
docker compose build --no-cache
docker compose up -d
```
### Database connection issues
```bash
# Verify database is running
docker compose ps
# Check database logs
docker compose logs db
# Try manual connection
make db-shell
```
### Permission issues
```bash
# Fix storage permissions
docker compose exec app chmod -R 777 storage bootstrap/cache
```
### Clear all caches
```bash
make clear
# Or manually
docker compose exec app php artisan cache:clear
docker compose exec app php artisan config:clear
docker compose exec app php artisan route:clear
docker compose exec app php artisan view:clear
```
Contributions welcome for development setup instructions on other platforms.
## 📄 License
This project is open-source software licensed under the [MIT license](LICENSE.md).
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📞 Support
For issues and questions, please use the [GitHub Issues](https://github.com/yourusername/dish-planner/issues) page.
---
Built with ❤️ using Laravel and Livewire
For issues and questions, please use [Codeberg Issues](https://codeberg.org/dish-planner/app/issues).

View file

@ -0,0 +1,28 @@
<?php
namespace App\Console\Commands;
use App\Models\Planner;
use Illuminate\Console\Command;
class PurgeDemoAccountsCommand extends Command
{
protected $signature = 'demo:purge';
protected $description = 'Purge demo accounts older than 24 hours';
public function handle(): int
{
if (! is_mode_demo()) {
$this->error('This command can only run in demo mode.');
return self::FAILURE;
}
$count = Planner::where('created_at', '<', now()->subHours(24))->delete();
$this->info("Purged {$count} demo accounts.");
return self::SUCCESS;
}
}

View file

@ -6,6 +6,7 @@ enum AppModeEnum: string
{
case APP = 'app';
case SAAS = 'saas';
case DEMO = 'demo';
public static function current(): self
{
@ -21,4 +22,19 @@ public function isSaas(): bool
{
return $this === self::SAAS;
}
public function isDemo(): bool
{
return $this === self::DEMO;
}
public function requiresSubscription(): bool
{
return $this === self::SAAS;
}
public function allowsLogout(): bool
{
return $this !== self::DEMO;
}
}

View file

@ -34,6 +34,10 @@ public function login(Request $request)
public function logout(Request $request)
{
if (is_mode_demo()) {
return redirect()->route('dashboard');
}
Auth::logout();
$request->session()->invalidate();

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use App\Models\Planner;
use Closure;
use DishPlanner\Planner\Actions\SeedDemoPlannerAction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class DemoMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (! is_mode_demo()) {
return $next($request);
}
if (Auth::check()) {
return $next($request);
}
$planner = DB::transaction(function () {
$planner = Planner::create([
'name' => 'Demo User',
'email' => 'demo-' . Str::uuid() . '@demo.local',
'password' => Hash::make(Str::random(32)),
]);
resolve(SeedDemoPlannerAction::class)->execute($planner);
return $planner;
});
Auth::login($planner);
return $next($request);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Enums\AppModeEnum;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@ -10,7 +11,7 @@ class RequireSubscription
{
public function handle(Request $request, Closure $next): Response
{
if (is_mode_app()) {
if (! AppModeEnum::current()->requiresSubscription()) {
return $next($request);
}

View file

@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
@ -12,7 +13,9 @@
/**
* @property int $id
* @property static PlannerFactory factory($count = null, $state = [])
* @property Collection<User> $users
* @method static first()
* @method static create(array $array)
*/
class Planner extends Authenticatable
{
@ -34,4 +37,9 @@ public function schedules(): HasMany
{
return $this->hasMany(Schedule::class);
}
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}

View file

@ -15,3 +15,17 @@ function is_mode_saas(): bool
return AppModeEnum::current()->isSaas();
}
}
if (! function_exists('is_mode_demo')) {
function is_mode_demo(): bool
{
return AppModeEnum::current()->isDemo();
}
}
if (! function_exists('allows_logout')) {
function allows_logout(): bool
{
return AppModeEnum::current()->allowsLogout();
}
}

View file

@ -1,29 +0,0 @@
#!/bin/bash
# Build and push production image to Codeberg
set -e
# Configuration
REGISTRY="codeberg.org"
NAMESPACE="lvl0"
IMAGE_NAME="dish-planner"
TAG="${1:-latest}"
echo "🔨 Building production image..."
podman build -f Dockerfile -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} .
echo "📤 Pushing to Codeberg registry..."
echo "Please ensure you're logged in to Codeberg:"
echo " podman login codeberg.org"
podman push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}
echo "✅ Done! Image pushed to ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}"
echo ""
echo "To deploy in production:"
echo "1. Copy docker-compose.prod.yml to your server"
echo "2. Set required environment variables:"
echo " - APP_KEY (generate with: openssl rand -base64 32)"
echo " - APP_URL"
echo " - DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_ROOT_PASSWORD"
echo "3. Run: docker-compose -f docker-compose.prod.yml up -d"

View file

@ -1,5 +1,6 @@
<?php
use App\Http\Middleware\DemoMiddleware;
use App\Http\Middleware\ForceJsonResponse;
use App\Http\Middleware\RequireSaasMode;
use App\Http\Middleware\RequireSubscription;
@ -27,7 +28,7 @@
->withMiddleware(function (Middleware $middleware) {
// Apply ForceJsonResponse only to API routes
$middleware->api(ForceJsonResponse::class);
$middleware->web(DemoMiddleware::class);
$middleware->alias([
'subscription' => RequireSubscription::class,
'saas' => RequireSaasMode::class,

View file

@ -1,29 +0,0 @@
#!/bin/bash
# Build and push production image to Codeberg
set -e
# Configuration
REGISTRY="codeberg.org"
NAMESPACE="lvl0"
IMAGE_NAME="dish-planner"
TAG="${1:-latest}"
echo "🔨 Building production image..."
podman build -f Dockerfile -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG} .
echo "📤 Pushing to Codeberg registry..."
echo "Please ensure you're logged in to Codeberg:"
echo " podman login codeberg.org"
podman push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}
echo "✅ Done! Image pushed to ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${TAG}"
echo ""
echo "To deploy in production:"
echo "1. Copy docker-compose.prod.yml to your server"
echo "2. Set required environment variables:"
echo " - APP_KEY (generate with: openssl rand -base64 32)"
echo " - APP_URL"
echo " - DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_ROOT_PASSWORD"
echo "3. Run: docker-compose -f docker-compose.prod.yml up -d"

View file

@ -39,6 +39,7 @@
*/
'mode' => env('APP_MODE', 'app'),
'demo_subscribe_url' => env('APP_DEMO_SUBSCRIBE_URL', 'https://dishplanner.app'),
/*
|--------------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
# Production Docker Compose
services:
app:
image: codeberg.org/lvl0/dish-planner:latest
image: codeberg.org/dish-planner/app:latest
container_name: dishplanner_app
restart: always
ports:

View file

@ -13,6 +13,16 @@
</head>
<body class="font-sans antialiased bg-gray-600 text-gray-100" x-data="{ mobileMenuOpen: false }">
<div class="min-h-screen">
@if(is_mode_demo())
<!-- Demo Banner -->
<div class="bg-primary text-white text-center py-2 px-4 text-sm sticky top-0 z-[60]">
DEMO MODE.
<a href="{{ config('app.demo_subscribe_url') }}" target="_blank" rel="noopener noreferrer" class="underline font-semibold hover:text-gray-200 ml-1">
Subscribe for only &euro;1 / month &rarr;
</a>
</div>
@endif
<!-- Navigation -->
<nav class="border-b-2 border-secondary shadow-sm z-50 mb-8 bg-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@ -70,12 +80,14 @@ class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-
Billing
</a>
@endif
@if(allows_logout())
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-600 hover:text-accent-blue">
Logout
</button>
</form>
@endif
</div>
</div>
</div>
@ -150,12 +162,14 @@ class="block text-2xl font-medium {{ request()->routeIs('schedule.*') ? 'text-ac
Billing
</a>
@endif
@if(allows_logout())
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-xl text-danger">
Logout
</button>
</form>
@endif
</div>
@else
<div class="space-y-6 text-center">

View file

@ -2,7 +2,12 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote')->hourly();
Schedule::command('demo:purge')
->dailyAt('03:00')
->when(fn () => is_mode_demo());

View file

@ -86,11 +86,11 @@ pkgs.mkShell {
prod-build() {
local TAG="''${1:-latest}"
local REGISTRY="codeberg.org"
local NAMESPACE="lvl0"
local IMAGE_NAME="dish-planner"
local NAMESPACE="dish-planner"
local IMAGE_NAME="app"
echo "🔨 Building production image..."
podman build -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} .
podman build --format docker -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} .
echo " Build complete: ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}"
echo "Run 'prod-push' to push to Codeberg"
@ -99,8 +99,8 @@ pkgs.mkShell {
prod-push() {
local TAG="''${1:-latest}"
local REGISTRY="codeberg.org"
local NAMESPACE="lvl0"
local IMAGE_NAME="dish-planner"
local NAMESPACE="dish-planner"
local IMAGE_NAME="app"
echo "📤 Pushing to Codeberg registry..."
if podman push ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}; then
@ -115,11 +115,11 @@ pkgs.mkShell {
prod-build-nc() {
local TAG="''${1:-latest}"
local REGISTRY="codeberg.org"
local NAMESPACE="lvl0"
local IMAGE_NAME="dish-planner"
local NAMESPACE="dish-planner"
local IMAGE_NAME="app"
echo "🔨 Building production image (no cache)..."
podman build --no-cache -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} .
podman build --format docker --no-cache -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} .
echo " Build complete: ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}"
echo "Run 'prod-push' to push to Codeberg"

View file

@ -0,0 +1,104 @@
<?php
namespace DishPlanner\Planner\Actions;
use App\Models\Dish;
use App\Models\Planner;
use App\Models\User;
use DishPlanner\Schedule\Actions\GenerateScheduleForPeriodAction;
class SeedDemoPlannerAction
{
private array $dishNames = [
'Spaghetti Bolognese',
'Chicken Curry',
'Caesar Salad',
'Beef Stir Fry',
'Vegetable Lasagna',
'Fish Tacos',
'Mushroom Risotto',
'BBQ Ribs',
'Greek Salad',
'Pad Thai',
'Margherita Pizza',
'Beef Burger',
'Chicken Fajitas',
'Vegetable Soup',
'Salmon Teriyaki',
'Lamb Chops',
'Shrimp Scampi',
'Pulled Pork Sandwich',
'Caprese Salad',
'Beef Tacos',
'Chicken Alfredo',
'Vegetable Curry',
'Pork Schnitzel',
'Tuna Poke Bowl',
'Beef Stroganoff',
'Chicken Parmesan',
'Ratatouille',
'Fish and Chips',
'Lamb Kebabs',
'Shrimp Fried Rice',
'Pork Tenderloin',
'Nicoise Salad',
'Beef Burritos',
'Chicken Tikka Masala',
'Eggplant Parmesan',
'Grilled Salmon',
'Lamb Tagine',
'Lobster Roll',
'Pork Belly',
'Waldorf Salad',
'Beef Wellington',
'Chicken Satay',
'Stuffed Peppers',
'Miso Glazed Cod',
'Lamb Shanks',
'Crab Cakes',
'Pork Carnitas',
'Cobb Salad',
'Beef Enchiladas',
'Chicken Shawarma',
];
public function execute(Planner $planner): void
{
$users = $this->createUsers($planner);
$this->createDishes($planner, $users);
$this->generateSchedule($planner);
}
private function createUsers(Planner $planner): array
{
$names = ['Alice', 'Bob', 'Charlie'];
return array_map(
fn (string $name) => User::create([
'planner_id' => $planner->id,
'name' => $name,
]),
$names
);
}
private function createDishes(Planner $planner, array $users): void
{
foreach ($this->dishNames as $dishName) {
$dish = Dish::create([
'planner_id' => $planner->id,
'name' => $dishName,
]);
// Randomly assign dish to 1-3 users
$count = rand(1, count($users));
$userIds = collect($users)->random($count)->pluck('id');
$dish->users()->attach($userIds);
}
}
private function generateSchedule(Planner $planner): void
{
resolve(GenerateScheduleForPeriodAction::class)->execute($planner);
}
}

View file

@ -10,9 +10,9 @@ class RegenerateScheduleDayAction
{
public function execute(Planner $planner, Schedule $schedule, bool $overwrite = false): void
{
User::all()
->each(fn (User $user) => resolve(RegenerateScheduleDayForUserAction::class)
->execute($planner, $schedule, $user, $overwrite)
);
/** @var RegenerateScheduleDayForUserAction $action */
$action = resolve(RegenerateScheduleDayForUserAction::class);
$planner->users->each(fn (User $user) => $action->execute($planner, $schedule, $user, $overwrite));
}
}