Imported from FikoAbdgst/PanoricamLaravel (
AGENTS.md). Install upstream withnpx skills add FikoAbdgst/PanoricamLaravel. Copyright stays with the author.
AGENTS.md
Project Overview
PanoricamLaravel — a Laravel 12 photobooth web app. Users browse frame templates, pick a frame, take photos in-browser (camera → canvas compositing → GIF), and receive the finished PNG + GIF strip by email. Paid frames require users to submit a payment proof which an admin manually approves. Admin panel manages frames, categories, transactions, and testimonials.
Indonesian-language UI throughout (validation messages, flash messages, controller responses).
Tech Stack
- PHP 8.2+, Laravel 12, SQLite (default DB)
- Tailwind CSS 4 + Vite 6 (via
@tailwindcss/vite),laravel-vite-plugin midtrans/midtrans-phpis installed incomposer.jsonbut never used — noMidtrans\Configsetup, noMIDTRANS_*env keys, no references anywhere. Do not search for the Midtrans integration; payment is manual proof upload.- PHPUnit 11 (tests use SQLite in-memory)
vercel.jsonis empty — deployment is not actually configured
Key Commands
# Full dev stack (artisan serve + queue:listen + pail + vite, via concurrently)
composer dev
# Build frontend assets only
npm run build
# Run tests (clears config cache first, then php artisan test)
composer test
php artisan test
php artisan test --filter=test_method_name
# Create a frame template from default
php artisan frame:template {frameId} # interactive; asks to overwrite
# DB seed (only seeder is AdminSeeder)
php artisan db:seed
php artisan config:clear # sometimes needed if config changes aren't picked up
Architecture
Models & Relationships
Category→ hasManyFrame(fillable:name,icon)Frame→ belongsToCategory, hasManyTestimoni; fillable:name,slug,category_id,image_path,price,used; helpersisFree()(price == 0),getTemplatePath()(falls back todefault),scopeWithAverageRating/scopeOrderByRatingTransaction→ belongsToFrame; fillable:order_id,frame_id,customer_name,whatsapp_number,payment_method,amount,payment_proof,status,approved_at; status:pending→approved/rejected;order_idformat isORD{YYYYMMDD}{6 random uppercased chars}(unique, regenerated in a loop)Testimoni→ belongsToFrame; fillable:rating(1–5),emoji,name,message,frame_idAdmin— standalone model, not linked toUser
Admin Auth
There is no auth middleware anywhere — bootstrap/app.php is stock (empty middleware closures). Auth is raw session:
- Login:
AdminController::loginvalidates, checksHash::check, thenSession::put(['admin_id','admin_name','admin_email']). Logout forgets them. - Credits only exist in the seeder:
database/seeders/AdminSeeder.phpcreatesadmin123@gmail.com/Admin123(hashed). - Guards are hand-rolled per controller method:
Admin\FrameControllerandAdmin\CategoryControllereach define a privatecheckAdminAuth()andreturn redirect()->route('admin.login')ifsession()->has('admin_id')is false. - Gotchas:
Admin\TestimoniControllerhas no auth checks at all, andAdminController::dashboard()/transactions()also do not check auth.AdminController::checkAdminLogin()is a dead static method nobody calls. Follow the existing per-method session-check convention — do not reach for middleware unless adding it consistently.
Public Flow (routes in routes/web.php)
/HomeController::index— home with categories, frames, top-3 byused/frameFrameTempController::index— frame catalog filtered by?category=,?free=true,?sort_rating=/?sort_popular=- Free frame → straight to
/booth?frame_id=X. Paid frame → payment form posts toPOST /payment/create POST /payment/create→ validates (rejectspayment_methodnot one ofbank_transfer/qris), uploads proof tostorage/app/public/payment_proofs/, createsTransaction(pending), returns JSON withorder_id- Admin approves via
POST /admin/transactions/{id}/approveor rejects via.../{id}/reject(both return JSON, only work onpending) /booth?frame_id=X&order_id=Y—PhotoboothController::indexverifies:frame_idrequired, then if frame is paid it requires anapprovedtransaction matchingorder_id+frame_id, else redirects back to/framewith an error./chear-check-statusof transaction viaGET /check-payment-status/{orderId}→ JSON{success, status}- AJAX helpers the frontends hit:
GET /get-frame-template/{id}(rendered overlay HTML),GET /get-frame-status/{id}(price/isFree JSON),POST /save-frame-photos(no-op, returns success),POST /submitTestimoni,GET /api/testimonis,GET /api/testimoni-stats
Frame Templates
Each frame slug has a matching Blade view at resources/views/admin/frames/templates/{slug}.blade.php — 84 files total (default.blade.php + 83 custom). New templates are scaffolded from default.blade.php by php artisan frame:template {id} (str-replaces the root class and title).
Template anatomy (important for editing/creating templates):
- A full-bleed
<div>overlay; photo slots are absolutely-positioned[data-photo-index="N"]wrappers containing.photo-slotwith<img id="photo{N}"> - Per-slot retake/recrop buttons:
.retake-buttonand.recrop-buttonwithdata-index - The booth page JS stamps captured photos into
img#photo{N}and renders the overlay — no backend rendering of strip content - If
{slug}.blade.phpdoesn't exist for the requested frame,PhotoboothController::indexandHomeController::getFrameTemplateusedefaultandLog::warning(...)(these controllers duplicate the fallback logic inline;Frame::getTemplatePath()/Frame::templateExists()exist but are not used by the controllers)
Photo Capture & Email Delivery
The entire booth UI + camera/canvas/GIF logic lives client-side in the massive resources/views/booth/index.blade.php (~5800 lines). Same story for the catalog in resources/views/frame.blade.php (~3200 lines) and resources/views/components/content.blade.php (~1700 lines). Boot/setup JS is negligible (resources/js/app.js is nearly pristine). JS is NOT in resources/js — edit the blade files.
POST /savePhoto(route namesavePhoto) andPOST /save-photoare both registered toPhotoboothController::savePhoto— it only validates the in-memory photo payload andincrement('used')on the frame. The base64photos/final_imageare not persisted.POST /booth/reset-used→PhotoboothController::resetUsedStatussetsframe.used = 0(no auth).- Delivery: the frontend POSTs
photo(PNG),gif,email,frame_idtoDriveController::uploadToDrive(POST /upload-to-drive). Despite the name it just sends an email with both files attached via rawMail::send, no Mailable class, no Google Drive. Max 10MB per file.
Email Config
config/mail.php is stock; default MAIL_MAILER=log in .env.example (emails go to the log file, "success" is fake). Real delivery needs MAIL_MAILER=smtp + SMTP creds in .env. On SMTP failure the controller returns a friendly "masalah konfigurasi email server" message.
Testing
- PHPUnit with
Unit/Featuresuites;phpunit.xmlhardcodesDB_CONNECTION=sqlite+DB_DATABASE=:memory:,MAIL_MAILER=array,SESSION_DRIVER=array - Only default
tests/Feature/ExampleTest.phpexists;RefreshDatabaseis still commented out — enabling it only matters if you add real tests - Run focused tests:
php artisan test --filter=...orphp artisan test tests/Feature/ExampleTest.php - No lint CI; Pint is available (
vendor/bin/pint) but not wired into any script
Gotchas
.envis committed and tracked in git (lives in.gitignore? No —.gitignoredoes not exclude it). It's currently modified in the working tree. Never commit new secrets or the modified.env.composer.pharis committed to the repo (unusual — keep it).public/build(compiled assets) andpublic/hotare tracked; deletepublic/hotlocally if stale.- The frontend references
POST /notify-download(booth/index.blade.php) which has no route — it 404s by design (incomplete feature), don't hunt for a handler. Route::fallbackreturns a plain "halaman tidak ada" string.- No
RefreshDatabase-based test infra; the app DB is the filedatabase/database.sqlite. - Dev Vite server binds
0.0.0.0+ HMRlocalhostfor ngrok/tunnel usage — don't "fix" that. - UI copy, comments, and JSON messages are Indonesian — keep new messages in Indonesian for consistency.