Compare commits

8 Commits

122 changed files with 2263 additions and 3405 deletions

View File

@@ -1,40 +1,59 @@
APP_NAME=SkunkLounge
APP_NAME=Skunklounge
APP_ENV=local
APP_KEY=base64:EwYAjTUH78N13Kil0jkqQlxHJSsEJrc+DJzUrHa31GY=
APP_KEY=base64:kBh8DxjSOr8EnVoa1Ge5w9nfsLDpkXjZcsop0HtxpEk=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost:8000
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
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=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=skunk_lounge_db
DB_USERNAME=postgres
DB_PASSWORD=secretpassword # Change to a strong password
DB_CONNECTION=sqlite
#DB_CONNECTION=pgsql
#DB_HOST=db
#DB_PORT=5432
#DB_DATABASE=skunk_lounge
#DB_USERNAME=skunk_user
#DB_PASSWORD=skunk_pass
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
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=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
@@ -44,17 +63,19 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=your_pusher_id # Get from pusher.com
PUSHER_APP_KEY=your_key
PUSHER_APP_SECRET=your_secret
PUSHER_HOST=null
VITE_APP_NAME="${APP_NAME}"
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
PUSHER_APP_ID=your_pusher_id
PUSHER_APP_KEY=your_pusher_key
PUSHER_APP_SECRET=your_pusher_secret
PUSHER_HOST=cluster_host_from_pusher
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
PUSHER_APP_CLUSTER=your_cluster

View File

@@ -1,18 +1,35 @@
FROM php:8.3-apache
RUN apt-get update && apt-get install -y libpq-dev libzip-dev unzip libpng-dev libjpeg-dev libfreetype6-dev libicu-dev && \
docker-php-ext-install pdo_pgsql pgsql zip bcmath gd intl
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
npm install -g npm@latest
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
rm composer-setup.php
FROM php:8.3-fpm
# Install system dependencies and PHP extensions
RUN apt-get update && apt-get install -y \
libpq-dev \
zip unzip \
git \
nodejs \
npm \
libicu-dev \
libzip-dev \
&& docker-php-ext-configure intl \
&& docker-php-ext-install pdo pdo_pgsql intl zip \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy project files
COPY . .
RUN composer install --no-dev --optimize-autoloader
RUN npm install && npm run build
COPY apache.conf /etc/apache2/sites-available/000-default.conf
RUN a2enmod rewrite
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Set permissions and Git safe directory
RUN chown -R www-data:www-data /var/www/html \
&& git config --global --add safe.directory /var/www/html
# Install dependencies
RUN composer install --no-dev --optimize-autoloader \
&& npm install \
&& npm run build
EXPOSE 80
CMD ["apache2-foreground"]
CMD ["php-fpm"]

View File

@@ -1,65 +1,61 @@
# Skunk Lounge - Self-Hosted Fake Casino
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
Skunk Lounge is a self-hosted, fake casino website for personal entertainment, mimicking the look and feel of real casinos like XXX. It uses fake currency only—no real money or payments. Built with Laravel for the backend, Filament for the admin panel, and Docker for easy deployment.
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## Features
- **Casino Lobby**: Game selection grid with thumbnails (slots, plinko, wheel, card games from open-source repos).
- **User Accounts**: Signup/login, balance management, promo codes for fake deposits.
- **Admin Interface**: Manage users, balances, promos, games (odds, bets), stats (wins/losses with charts).
- **Games**: Integration-ready for open-source GitHub repos (e.g., Plinko with matter.js, slots, card games from Diablo-Lounge).
- **Chat**: Real-time lobby and per-game chat using Socket.io or Pusher.
- **Security**: Server-side bet calculations, hashing for integrity, audit logs.
- **Database**: PostgreSQL for users, transactions, promos, games.
- **Containers**: Docker for app and Postgres.
## About Laravel
## Prerequisites
- Docker and Docker Compose.
- PHP 8.3+, Composer, Node.js (for local dev).
- Gitea for repo (optional).
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
## Setup
1. **Clone Repo**:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
```
git clone https://git.skunkhotel.com/arriej/Skunk-Lounge.git
cd Skunk-Lounge
```
Laravel is accessible, powerful, and provides tools required for large, robust applications.
2. **Configure .env**:
- Copy `.env.example` to `.env`.
- Update DB credentials, Pusher keys, secret key (run `docker compose exec app php artisan key:generate`).
## Learning Laravel
3. **Build and Run Containers**:
```
docker compose build
docker compose up -d
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate --seed
```
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
4. **Access**:
- App: http://localhost:8000
- Admin: http://localhost:8000/admin (register first account for admin role).
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
## Usage
- **User Side**: Register, use promo codes (e.g., START100), select games from lobby, bet fake $, chat.
- **Admin Side**: Manage users/balances, games/odds, promos, view stats/logs.
- **Adding Games**: Copy GitHub repo code/assets to `public/games`, hook to /api/bet.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Development
- **Edit Files**: Use Code Server/VS Code.
- **Commit to Gitea**: `git add . && git commit -m "Message" && git push`.
- **Build Containers with Komodo**: Use Komodo's Docker tools or CLI: `docker compose build`.
- **Local Testing**: `docker compose exec app php artisan serve --host=0.0.0.0` (access via port 8000).
- **Extensions**: Add PHP extensions in Dockerfile if needed.
## Laravel Sponsors
## Clean Up and Maintenance
- Remove unused Laravel files (e.g., default tests).
- Update README with custom features.
- Security: All bets server-side; add CSRF, rate-limiting as needed.
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
MIT
## Disclaimer
For entertainment only—no real money. Use responsibly.
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -1,7 +0,0 @@
<VirtualHost *:80>
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>

View File

@@ -1,36 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ChatMessage
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Filament\User\Pages;
use Filament\Pages\Page;
class AboutUs extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-information-circle';
protected static string $view = 'filament.user.pages.about-us';
protected static ?int $navigationSort = 6; // Bottom
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Filament\User\Pages;
use Filament\Pages\Page;
class Deposits extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
protected static string $view = 'filament.user.pages.deposits';
protected static ?int $navigationSort = 2;
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Filament\User\Pages;
use Filament\Pages\Dashboard;
class Lobby extends Dashboard
{
protected static ?string $navigationIcon = 'heroicon-o-home';
protected static string $view = 'filament.user.pages.lobby';
protected static ?int $navigationSort = 1; // Top
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Filament\User\Pages;
use Filament\Pages\Page;
class Profile extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-user-circle';
protected static string $view = 'filament.user.pages.profile';
protected static ?int $navigationSort = 4;
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Filament\User\Pages;
use Filament\Pages\Page;
class Transactions extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.user.pages.transactions';
protected static ?int $navigationSort = 3;
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Filament\User\Pages;
use Filament\Pages\Page;
class VIP extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-star';
protected static string $view = 'filament.user.pages.v-i-p';
protected static ?int $navigationSort = 5;
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Filament\User\Widgets;
use Filament\Widgets\Widget;
class AdminButtonWidget extends Widget
{
protected static ?int $sort = 1;
protected static string $view = 'filament.user.widgets.admin-button-widget';
public function shouldRender(): bool
{
return auth()->user()->role === 'admin';
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Hash;
class SecurityHelper
{
public static function generateBetHash($userId, $amount, $gameId)
{
return Hash::make($userId . $amount . $gameId . now()->timestamp);
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\DB;
use App\Models\Wallet;
use App\Models\Transaction;
use App\Models\AuditLog;
class WalletHelper
{
/**
* Process a wallet transaction (e.g., deposit, bet) with atomic updates and audit logging.
*
* @param int $userId
* @param float $amount
* @param string $type (deposit, bet, win, withdrawal)
* @param string|null $description
* @param string|null $ipAddress
* @return Transaction
* @throws \Exception
*/
public static function processTransaction($userId, $amount, $type, $description = null, $ipAddress = null)
{
return DB::transaction(function () use ($userId, $amount, $type, $description, $ipAddress) {
// Lock wallet for atomic update
$wallet = Wallet::where('user_id', $userId)->lockForUpdate()->firstOrFail();
// Validate balance for bets/withdrawals
if (in_array($type, ['bet', 'withdrawal']) && $wallet->balance < $amount) {
throw new \Exception('Insufficient Skunk Coins');
}
// Update balance
$newBalance = in_array($type, ['bet', 'withdrawal']) ? $wallet->balance - $amount : $wallet->balance + $amount;
$wallet->update(['balance' => $newBalance]);
// Create transaction with SHA-256 hash
$hash = hash('sha256', $userId . $amount . $type . time());
$transaction = Transaction::create([
'wallet_id' => $wallet->id,
'amount' => $amount,
'type' => $type,
'description' => $description,
'hash' => $hash,
]);
// Log action in audit_logs
AuditLog::create([
'user_id' => $userId,
'action' => 'wallet_' . $type,
'details' => json_encode([
'wallet_id' => $wallet->id,
'amount' => $amount,
'new_balance' => $newBalance,
'transaction_id' => $transaction->id,
]),
'ip_address' => $ipAddress,
]);
return $transaction;
});
}
/**
* Redeem a promo code for Skunk Coins.
*
* @param int $userId
* @param string $code
* @param string|null $ipAddress
* @return Transaction
* @throws \Exception
*/
public static function redeemPromoCode($userId, $code, $ipAddress = null)
{
return DB::transaction(function () use ($userId, $code, $ipAddress) {
$promo = \App\Models\PromoCode::where('code', $code)
->where('is_active', true)
->where(function ($query) {
$query->where('uses_remaining', -1)->orWhere('uses_remaining', '>', 0);
})
->where(function ($query) {
$query->whereNull('expires_at')->orWhere('expires_at', '>', now());
})
->firstOrFail();
// Check if already redeemed
if (\App\Models\PromoCodeRedemption::where('user_id', $userId)->where('promo_code_id', $promo->id)->exists()) {
throw new \Exception('Promo code already redeemed');
}
// Process deposit
$transaction = self::processTransaction($userId, $promo->value, 'deposit', "Promo code: $code", $ipAddress);
// Record redemption
\App\Models\PromoCodeRedemption::create([
'promo_code_id' => $promo->id,
'user_id' => $userId,
'redeemed_at' => now(),
]);
// Decrement uses if limited
if ($promo->uses_remaining !== -1) {
$promo->decrement('uses_remaining');
}
return $transaction;
});
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\SecurityHelper;
use App\Http\Controllers\Controller;
use App\Models\Game;
use App\Models\Transaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class BetController extends Controller
{
public function placeBet(Request $request)
{
$validated = $request->validate([
'game_id' => 'required|exists:games,id',
'amount' => 'required|numeric|min:0.01',
]);
DB::beginTransaction();
try {
$user = auth()->user();
if ($user->balance < $validated['amount']) {
abort(400, 'Insufficient balance');
}
$hash = SecurityHelper::generateBetHash($user->id, $validated['amount'], $validated['game_id']);
$game = Game::find($validated['game_id']);
$win = random_int(0, 100) < $game->win_probability;
$payout = $win ? $validated['amount'] * 2 : -$validated['amount'];
$user->balance += $payout;
$user->save();
Transaction::create([
'user_id' => $user->id,
'type' => $win ? 'win' : 'loss',
'amount' => abs($payout),
'description' => "Bet hash: $hash",
]);
DB::commit();
return response()->json([
'success' => true,
'win' => $win,
'balance' => $user->balance,
'hash' => $hash,
]);
} catch (\Exception $e) {
DB::rollback();
abort(500, $e->getMessage());
}
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
use App\Models\Wallet;
use App\Models\Game;
use App\Helpers\WalletHelper;
use Illuminate\Http\Request;
class LobbyController extends Controller
{
public function index()
{
$user = auth()->user();
return Inertia::render('Lobby', [
'wallet' => Wallet::where('user_id', $user->id)->first() ?? ['balance' => 0],
'games' => Game::where('is_active', true)->get(),
'user' => [
'name' => $user->name,
'vip_level' => $user->vip_level,
'is_kyc_verified' => $user->is_kyc_verified,
],
]);
}
public function addCoins(Request $request)
{
$request->validate(['amount' => 'required|numeric|min:1']);
WalletHelper::processTransaction(auth()->id(), $request->amount, 'deposit', 'Manual coin addition', $request->ip());
return Inertia::render('Lobby', [
'wallet' => Wallet::where('user_id', auth()->id())->first(),
'games' => Game::where('is_active', true)->get(),
'user' => [
'name' => auth()->user()->name,
'vip_level' => auth()->user()->vip_level,
'is_kyc_verified' => auth()->user()->is_kyc_verified,
],
'success' => 'Added ' . $request->amount . ' Skunk Coins',
]);
}
public function redeemPromo(Request $request)
{
$request->validate(['code' => 'required|string']);
try {
WalletHelper::redeemPromoCode(auth()->id(), $request->code, $request->ip());
return Inertia::render('Lobby', [
'wallet' => Wallet::where('user_id', auth()->id())->first(),
'games' => Game::where('is_active', true)->get(),
'user' => [
'name' => auth()->user()->name,
'vip_level' => auth()->user()->vip_level,
'is_kyc_verified' => auth()->user()->is_kyc_verified,
],
'success' => 'Promo code redeemed successfully',
]);
} catch (\Exception $e) {
return Inertia::render('Lobby', [
'wallet' => Wallet::where('user_id', auth()->id())->first(),
'games' => Game::where('is_active', true)->get(),
'user' => [
'name' => auth()->user()->name,
'vip_level' => auth()->user()->vip_level,
'is_kyc_verified' => auth()->user()->is_kyc_verified,
],
'error' => $e->getMessage(),
]);
}
}
}

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -1,85 +0,0 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

42
app/Models/AuditLog.php Normal file
View File

@@ -0,0 +1,42 @@
<?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\SoftDeletes;
class AuditLog extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_id',
'action',
'details',
'ip_address',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'details' => 'json',
];
/**
* Get the user that owns the audit log.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

33
app/Models/Game.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Game extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'slug',
'thumbnail_url',
'category',
'is_active',
];
/**
* Get the player stats for the game.
*/
public function playerStats(): HasMany
{
return $this->hasMany(PlayerStats::class);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PlayerStats extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_id',
'game_id',
'total_winnings',
'total_bets',
'play_count',
'daily_play_amount',
'win_streak',
'last_played_at',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'last_played_at' => 'datetime',
];
/**
* Get the user that owns the stats.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the game for the stats (if per-game).
*/
public function game(): BelongsTo
{
return $this->belongsTo(Game::class);
}
/**
* Custom method to update stats after a game play (example).
*
* @param float $betAmount
* @param float $winAmount
*/
public function updateAfterPlay(float $betAmount, float $winAmount): void
{
$this->increment('play_count');
$this->total_bets += $betAmount;
$this->total_winnings += $winAmount;
$this->daily_play_amount += $betAmount; // Simplify; use cron for daily reset
$this->win_streak = $winAmount > 0 ? $this->win_streak + 1 : 0;
$this->last_played_at = now();
$this->save();
}
}

42
app/Models/PromoCode.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class PromoCode extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'code',
'value',
'uses_remaining',
'expires_at',
'is_active',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'expires_at' => 'datetime',
];
/**
* Get the redemptions for the promo code.
*/
public function redemptions(): HasMany
{
return $this->hasMany(PromoCodeRedemption::class);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PromoCodeRedemption extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'promo_code_id',
'user_id',
'redeemed_at',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'redeemed_at' => 'datetime',
];
/**
* Get the promo code that owns the redemption.
*/
public function promoCode(): BelongsTo
{
return $this->belongsTo(PromoCode::class);
}
/**
* Get the user that owns the redemption.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Transaction extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'wallet_id',
'amount',
'type',
'description',
'hash',
];
/**
* Get the wallet that owns the transaction.
*/
public function wallet(): BelongsTo
{
return $this->belongsTo(Wallet::class);
}
}

49
app/Models/Wallet.php Normal file
View File

@@ -0,0 +1,49 @@
<?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 Wallet extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_id',
'balance',
];
/**
* Get the user that owns the wallet.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the transactions for the wallet.
*/
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
/**
* Custom method to get total winnings (derived from transactions).
*
* @return float
*/
public function getTotalWinningsAttribute(): float
{
return $this->transactions()->where('type', 'win')->sum('amount');
}
}

View File

@@ -3,27 +3,22 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\PermissionRegistrar;
class AppServiceProvider extends ServiceProvider
{
public function register()
/**
* Register any application services.
*/
public function register(): void
{
//
}
public function boot()
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Register Spatie permissions
app(PermissionRegistrar::class)->setPermissionClass(\Spatie\Permission\Models\Permission::class);
// Redirect authenticated users to Filament lobby for specific routes
$this->app['router']->middleware('web')->group(function () {
$currentRoute = request()->route()?->getName();
if (Auth::check() && in_array($currentRoute, ['home', 'filament.user.auth.login', 'filament.user.auth.register'])) {
redirect()->route('filament.user.pages.lobby')->send();
}
});
//
}
}
}

View File

@@ -17,26 +17,32 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Spatie\Permission\Middlewares\RoleMiddleware;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->darkMode(true) // Enforce dark theme for casino admin vibe
->colors([
'primary' => Color::Amber,
'primary' => Color::hex('#4CAF50'), // Crypto-green for consistency with lobby
'secondary' => Color::hex('#0D1117'), // Dark background to match Stake.com aesthetic
])
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
// Add custom widgets later for stats (e.g., user activity, Skunk Coins transactions)
])
->middleware([
EncryptCookies::class,
@@ -51,6 +57,10 @@ class AdminPanelProvider extends PanelProvider
])
->authMiddleware([
Authenticate::class,
]);
RoleMiddleware::class . ':admin', // Restrict to admin role via Spatie
])
->brandName('Skunk Lounge Admin')
->brandLogo(asset('images/logo.png')) // Placeholder for custom logo
->favicon(asset('images/favicon.ico')); // Placeholder for favicon
}
}
}

View File

@@ -1,98 +0,0 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Filament\Navigation\NavigationItem;
use Filament\Navigation\NavigationGroup;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class UserPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('user')
->path('lobby')
->login()
->registration()
->colors([
'primary' => Color::Purple,
])
->brandLogo(null)
->discoverResources(in: app_path('Filament/User/Resources'), for: 'App\\Filament\\User\\Resources')
->discoverPages(in: app_path('Filament/User/Pages'), for: 'App\\Filament\\User\\Pages')
->pages([
\App\Filament\User\Pages\Lobby::class,
])
->discoverWidgets(in: app_path('Filament/User/Widgets'), for: 'App\\Filament\\User\\Widgets')
->widgets([
Widgets\AccountWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->navigationGroups([
NavigationGroup::make('Casino')
->items([
NavigationItem::make('Lobby')
->url(fn () => route('filament.user.pages.lobby'))
->icon('heroicon-o-home')
->sort(1),
NavigationItem::make('Deposits')
->url(fn () => route('filament.user.pages.deposits'))
->icon('heroicon-o-currency-dollar')
->sort(2),
NavigationItem::make('Transactions')
->url(fn () => route('filament.user.pages.transactions'))
->icon('heroicon-o-document-text')
->sort(3),
NavigationItem::make('Profile')
->url(fn () => route('filament.user.pages.profile'))
->icon('heroicon-o-user-circle')
->sort(4),
NavigationItem::make('VIP')
->url(fn () => route('filament.user.pages.v-i-p'))
->icon('heroicon-o-star')
->sort(5),
NavigationItem::make('About Us')
->url(fn () => route('filament.user.pages.about-us'))
->icon('heroicon-o-information-circle')
->sort(6),
]),
NavigationGroup::make('Administration')
->items([
NavigationItem::make('Admin Panel')
->url('/admin')
->icon('heroicon-o-shield-check')
->visible(fn () => auth()->user() && auth()->user()->role === 'admin')
->sort(7),
]),
])
->default();
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Lobby extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.lobby');
}
}

0
artisan Executable file → Normal file
View File

View File

@@ -3,6 +3,4 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\Filament\UserPanelProvider::class,
];

View File

@@ -7,13 +7,12 @@
"license": "MIT",
"require": {
"php": "^8.2",
"doctrine/dbal": "^4.3",
"filament/filament": "^3.3",
"laravel/breeze": "^2.3",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10.1",
"livewire/livewire": "^3.6",
"pusher/pusher-php-server": "^7.2",
"pusher/pusher-php-server": "~7.2",
"spatie/laravel-permission": "^6.21"
},
"require-dev": {

271
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b7822b3bd2227e2c0dee864f6ee7be57",
"content-hash": "7c664dab79ad9a7e1e82b353d06abbf1",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -224,25 +224,25 @@
},
{
"name": "brick/math",
"version": "0.13.1",
"version": "0.14.0",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04"
"reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04",
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04",
"url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2",
"reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2",
"shasum": ""
},
"require": {
"php": "^8.1"
"php": "^8.2"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^10.1",
"vimeo/psalm": "6.8.8"
"phpstan/phpstan": "2.1.22",
"phpunit/phpunit": "^11.5"
},
"type": "library",
"autoload": {
@@ -272,7 +272,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.13.1"
"source": "https://github.com/brick/math/tree/0.14.0"
},
"funding": [
{
@@ -280,7 +280,7 @@
"type": "github"
}
],
"time": "2025-03-29T13:50:30+00:00"
"time": "2025-08-29T12:40:03+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -533,16 +533,16 @@
},
{
"name": "doctrine/dbal",
"version": "4.3.2",
"version": "4.3.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "7669f131d43b880de168b2d2df9687d152d6c762"
"reference": "231959669bb2173194c95636eae7f1b41b2a8b19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/7669f131d43b880de168b2d2df9687d152d6c762",
"reference": "7669f131d43b880de168b2d2df9687d152d6c762",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/231959669bb2173194c95636eae7f1b41b2a8b19",
"reference": "231959669bb2173194c95636eae7f1b41b2a8b19",
"shasum": ""
},
"require": {
@@ -552,10 +552,10 @@
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "13.0.0",
"doctrine/coding-standard": "13.0.1",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.2",
"phpstan/phpstan": "2.1.17",
"phpstan/phpstan": "2.1.22",
"phpstan/phpstan-phpunit": "2.0.6",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "11.5.23",
@@ -619,7 +619,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/4.3.2"
"source": "https://github.com/doctrine/dbal/tree/4.3.3"
},
"funding": [
{
@@ -635,7 +635,7 @@
"type": "tidelift"
}
],
"time": "2025-08-05T13:30:38+00:00"
"time": "2025-09-04T23:52:42+00:00"
},
{
"name": "doctrine/deprecations",
@@ -1960,6 +1960,76 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "inertiajs/inertia-laravel",
"version": "v2.0.6",
"source": {
"type": "git",
"url": "https://github.com/inertiajs/inertia-laravel.git",
"reference": "6d0afc3237c370036de4a703927b17a4e7b83298"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/6d0afc3237c370036de4a703927b17a4e7b83298",
"reference": "6d0afc3237c370036de4a703927b17a4e7b83298",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^10.0|^11.0|^12.0",
"php": "^8.1.0",
"symfony/console": "^6.2|^7.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.2",
"larastan/larastan": "^3.0",
"laravel/pint": "^1.16",
"mockery/mockery": "^1.3.3",
"orchestra/testbench": "^8.0|^9.2|^10.0",
"phpunit/phpunit": "^10.4|^11.5",
"roave/security-advisories": "dev-master"
},
"suggest": {
"ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Inertia\\ServiceProvider"
]
}
},
"autoload": {
"files": [
"./helpers.php"
],
"psr-4": {
"Inertia\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"homepage": "https://reinink.ca"
}
],
"description": "The Laravel adapter for Inertia.js.",
"keywords": [
"inertia",
"laravel"
],
"support": {
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
"source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.6"
},
"time": "2025-08-26T06:17:48+00:00"
},
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "4.2.8",
@@ -2023,79 +2093,18 @@
},
"time": "2025-08-14T18:43:05+00:00"
},
{
"name": "laravel/breeze",
"version": "v2.3.8",
"source": {
"type": "git",
"url": "https://github.com/laravel/breeze.git",
"reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/breeze/zipball/1a29c5792818bd4cddf70b5f743a227e02fbcfcd",
"reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd",
"shasum": ""
},
"require": {
"illuminate/console": "^11.0|^12.0",
"illuminate/filesystem": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"illuminate/validation": "^11.0|^12.0",
"php": "^8.2.0",
"symfony/console": "^7.0"
},
"require-dev": {
"laravel/framework": "^11.0|^12.0",
"orchestra/testbench-core": "^9.0|^10.0",
"phpstan/phpstan": "^2.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Breeze\\BreezeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Breeze\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/breeze/issues",
"source": "https://github.com/laravel/breeze"
},
"time": "2025-07-18T18:49:59+00:00"
},
{
"name": "laravel/framework",
"version": "v12.27.1",
"version": "v12.28.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "168844fe531baaa11db00f7902fd0116d46d3bdd"
"reference": "868c1f2d3dba4df6d21e3a8d818479f094cfd942"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/168844fe531baaa11db00f7902fd0116d46d3bdd",
"reference": "168844fe531baaa11db00f7902fd0116d46d3bdd",
"url": "https://api.github.com/repos/laravel/framework/zipball/868c1f2d3dba4df6d21e3a8d818479f094cfd942",
"reference": "868c1f2d3dba4df6d21e3a8d818479f094cfd942",
"shasum": ""
},
"require": {
@@ -2173,6 +2182,7 @@
"illuminate/filesystem": "self.version",
"illuminate/hashing": "self.version",
"illuminate/http": "self.version",
"illuminate/json-schema": "self.version",
"illuminate/log": "self.version",
"illuminate/macroable": "self.version",
"illuminate/mail": "self.version",
@@ -2205,7 +2215,8 @@
"league/flysystem-read-only": "^3.25.1",
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
"orchestra/testbench-core": "^10.6.3",
"opis/json-schema": "^2.4.1",
"orchestra/testbench-core": "^10.6.5",
"pda/pheanstalk": "^5.0.6|^7.0.0",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0",
@@ -2299,7 +2310,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-09-02T23:59:38+00:00"
"time": "2025-09-04T14:58:12+00:00"
},
{
"name": "laravel/prompts",
@@ -2360,70 +2371,6 @@
},
"time": "2025-07-07T14:17:42+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0",
"illuminate/contracts": "^11.0|^12.0",
"illuminate/database": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-07-09T19:45:24+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.4",
@@ -4821,20 +4768,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.0",
"version": "4.9.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0"
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -4893,9 +4840,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.0"
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
},
"time": "2025-06-25T14:20:11+00:00"
"time": "2025-09-04T20:59:21+00:00"
},
{
"name": "ryangjchandler/blade-capture-directive",
@@ -9154,16 +9101,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.35",
"version": "11.5.36",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91"
"reference": "264a87c7ef68b1ab9af7172357740dc266df5957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/264a87c7ef68b1ab9af7172357740dc266df5957",
"reference": "264a87c7ef68b1ab9af7172357740dc266df5957",
"shasum": ""
},
"require": {
@@ -9235,7 +9182,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.35"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.36"
},
"funding": [
{
@@ -9259,7 +9206,7 @@
"type": "tidelift"
}
],
"time": "2025-08-28T05:13:54+00:00"
"time": "2025-09-03T06:24:17+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@@ -13,7 +13,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Skunk Lounge'),
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------

View File

@@ -40,6 +40,7 @@ return [
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
@@ -158,6 +159,10 @@ return [
'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' => [
@@ -167,6 +172,10 @@ return [
'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),
],
],

View File

@@ -1,27 +0,0 @@
<?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::table('users', function (Blueprint $table) {
$table->string('role')->default('user');
$table->decimal('balance', 10, 2)->default(0.00);
$table->enum('account_status', ['active', 'suspended', 'banned'])->default('active');
$table->enum('kyc_status', ['pending', 'verified', 'rejected'])->default('pending');
$table->string('kyc_document_path')->nullable(); // For future KYC file uploads
$table->timestamp('last_login')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['role', 'balance', 'account_status', 'kyc_status', 'kyc_document_path', 'last_login']);
});
}
};

View File

@@ -1,23 +0,0 @@
<?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('site_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->string('value');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('site_settings');
}
};

View File

@@ -1,24 +0,0 @@
<?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('chats', function (Blueprint $table) {
$table->id();
$table->string('room'); // e.g., 'lobby' or 'game_1'
$table->foreignId('user_id')->constrained();
$table->text('message');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('chats');
}
};

View File

@@ -1,27 +0,0 @@
<?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('games', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->boolean('enabled')->default(true);
$table->decimal('win_probability', 5, 2)->default(50.00);
$table->decimal('min_bet', 10, 2)->default(1.00);
$table->decimal('max_bet', 10, 2)->default(100.00);
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('games');
}
};

View File

@@ -0,0 +1,30 @@
<?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::table('users', function (Blueprint $table) {
$table->boolean('is_kyc_verified')->default(false)->comment('KYC-like verification status');
$table->integer('vip_level')->default(0)->comment('VIP status: 0 = standard, 1-5 = tiers');
$table->timestamp('last_login_at')->nullable()->comment('For tracking activity');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['is_kyc_verified', 'vip_level', 'last_login_at']);
});
}
};

View File

@@ -0,0 +1,29 @@
<?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('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete()->comment('Links to user');
$table->decimal('balance', 15, 2)->default(0.00)->comment('Skunk Coins balance');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('wallets');
}
};

View File

@@ -0,0 +1,26 @@
<?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('transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('wallet_id')->constrained()->cascadeOnDelete()->comment('Links to wallet');
$table->decimal('amount', 15, 2)->comment('Amount transacted');
$table->enum('type', ['bet', 'win', 'deposit', 'withdrawal'])->comment('Transaction type');
$table->text('description')->nullable()->comment('Details, e.g., "Bet on Plinko"');
$table->string('hash')->comment('SHA-256 hash for validation');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('transactions');
}
};

View File

@@ -0,0 +1,26 @@
<?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('games', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('Game name, e.g., Plinko');
$table->string('slug')->unique()->comment('URL-friendly slug');
$table->string('thumbnail_url')->nullable()->comment('Vibrant thumbnail for lobby');
$table->enum('category', ['slots', 'plinko', 'wheel', 'cards', 'other'])->default('other')->comment('For filtering');
$table->boolean('is_active')->default(true)->comment('Toggle for maintenance');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('games');
}
};

View File

@@ -10,11 +10,11 @@ return new class extends Migration
{
Schema::create('promo_codes', function (Blueprint $table) {
$table->id();
$table->string('code')->unique();
$table->decimal('value', 10, 2);
$table->boolean('used')->default(false);
$table->foreignId('user_id')->nullable()->constrained();
$table->timestamp('expires_at')->nullable();
$table->string('code')->unique()->comment('Unique promo code, e.g., WELCOME100');
$table->decimal('value', 15, 2)->comment('Skunk Coins awarded');
$table->integer('uses_remaining')->default(-1)->comment('-1 for unlimited');
$table->timestamp('expires_at')->nullable()->comment('Expiration date');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}

View File

@@ -8,18 +8,17 @@ return new class extends Migration
{
public function up(): void
{
Schema::create('transactions', function (Blueprint $table) {
Schema::create('promo_code_redemptions', function (Blueprint $table) {
$table->id();
$table->foreignId('promo_code_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('type', ['win', 'loss', 'deposit', 'withdraw', 'promo', 'adjustment']);
$table->decimal('amount', 10, 2);
$table->string('description')->nullable();
$table->timestamp('redeemed_at');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('transactions');
Schema::dropIfExists('promo_code_redemptions');
}
};

View File

@@ -10,9 +10,10 @@ return new class extends Migration
{
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained();
$table->string('action');
$table->json('details')->nullable();
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete()->comment('Admin or player who performed action');
$table->string('action')->comment('E.g., "updated_user_balance"');
$table->json('details')->nullable()->comment('Before/after changes');
$table->string('ip_address')->nullable();
$table->timestamps();
});
}

View File

@@ -0,0 +1,29 @@
<?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('player_stats', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('game_id')->nullable()->constrained()->cascadeOnDelete()->comment('Null for overall stats');
$table->decimal('total_winnings', 15, 2)->default(0.00);
$table->decimal('total_bets', 15, 2)->default(0.00);
$table->integer('play_count')->default(0);
$table->decimal('daily_play_amount', 15, 2)->default(0.00)->comment('Bets in last 24 hours');
$table->integer('win_streak')->default(0);
$table->timestamp('last_played_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('player_stats');
}
};

View File

@@ -2,48 +2,54 @@
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
use App\Models\Wallet;
use App\Models\Game;
use App\Models\PromoCode;
use Spatie\Permission\Models\Role;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// Seed admin user (for first-time admin)
User::create([
'name' => 'Admin',
// Roles (Spatie)
Role::create(['name' => 'admin']);
Role::create(['name' => 'player']);
Role::create(['name' => 'vip']);
// Test User (Player)
$player = User::factory()->create([
'name' => 'Test Player',
'email' => 'player@skunklounge.com',
'password' => bcrypt('password'),
'is_kyc_verified' => true,
'vip_level' => 1,
]);
$player->assignRole('player', 'vip');
// Test Admin
$admin = User::factory()->create([
'name' => 'Test Admin',
'email' => 'admin@skunklounge.com',
'password' => Hash::make('securepass123'),
'role' => 'admin',
'balance' => 1000.00,
'account_status' => 'active',
'kyc_status' => 'verified',
'password' => bcrypt('password'),
'is_kyc_verified' => true,
'vip_level' => 0,
]);
$admin->assignRole('admin');
// Seed site settings
DB::table('site_settings')->insert([
['key' => 'currency_symbol', 'value' => '$'],
['key' => 'default_balance', 'value' => '100.00'],
['key' => 'maintenance_mode', 'value' => 'false'],
]);
// Wallets
Wallet::create(['user_id' => $player->id, 'balance' => 10000.00]);
Wallet::create(['user_id' => $admin->id, 'balance' => 0.00]);
// Seed promo code
DB::table('promo_codes')->insert([
'code' => 'START100',
'value' => 100.00,
]);
// Games
Game::create(['name' => 'Plinko', 'slug' => 'plinko', 'thumbnail_url' => 'https://placehold.co/300x200', 'category' => 'plinko']);
Game::create(['name' => 'Slots', 'slug' => 'slots', 'thumbnail_url' => 'https://placehold.co/300x200', 'category' => 'slots']);
// Seed sample game
DB::table('games')->insert([
'name' => 'Slots',
'enabled' => true,
'win_probability' => 50.00,
'min_bet' => 1.00,
'max_bet' => 100.00,
'description' => 'Pragmatic Play-style slots',
]);
// Promo Codes
PromoCode::create(['code' => 'WELCOME100', 'value' => 100.00, 'uses_remaining' => -1, 'is_active' => true]);
// Player Stats (initial)
\App\Models\PlayerStats::create(['user_id' => $player->id, 'total_winnings' => 0.00, 'total_bets' => 0.00, 'play_count' => 0]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class PlayerStatsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,23 +1,58 @@
services:
app:
build: .
web:
build:
context: .
dockerfile: Dockerfile
container_name: skunk-lounge-web
ports:
- "8000:80"
# volumes:
# - .:/var/www/html
depends_on:
- postgres
env_file:
- .env
postgres:
image: postgres:16
environment:
POSTGRES_DB: skunk_lounge_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secretpassword
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- .:/var/www/html
depends_on:
- db
environment:
DB_CONNECTION: pgsql
DB_HOST: db
DB_PORT: 5432
DB_DATABASE: skunk_lounge
DB_USERNAME: skunk_user
DB_PASSWORD: skunk_pass
APP_KEY: ${APP_KEY}
networks:
- skunk-net
db:
image: postgres:16
container_name: skunk-lounge-db
restart: always
ports:
- "5433:5432"
environment:
POSTGRES_DB: skunk_lounge
POSTGRES_USER: skunk_user
POSTGRES_PASSWORD: skunk_pass
volumes:
- db-data:/var/lib/postgresql/data
networks:
- skunk-net
vite:
image: node:20
container_name: skunk-lounge-vite
working_dir: /var/www/html
volumes:
- .:/var/www/html
command: sh -c "sleep 5 && npm install && npm run dev"
ports:
- "5173:5173"
depends_on:
- web
networks:
- skunk-net
volumes:
pgdata:
db-data:
networks:
skunk-net:
driver: bridge

1610
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,22 @@
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.0",
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"@inertiajs/vue3": "^1.2.0",
"@vitejs/plugin-vue": "^5.1.4",
"autoprefixer": "^10.4.20",
"axios": "^1.6.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.1.0",
"vite": "^7.0.4"
"laravel-echo": "^2.2.0",
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.47",
"pusher-js": "^8.4.0",
"tailwindcss": "^3.4.10",
"vite": "^5.4.8"
},
"dependencies": {
"vue": "^3.4.0",
"laravel-echo": "^1.15.0",
"pusher-js": "^8.0.0"
}
}

View File

@@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,19 +0,0 @@
.bg-dark {
background-color: #111;
}
.glow {
text-shadow: 0 0 10px #00ff00;
}
.shadow-neon {
box-shadow: 0 0 15px #ff00de;
}
.btn-neon {
background-color: #00ff00;
color: #111;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
box-shadow: 0 0 10px #00ff00;
}
.btn-neon:hover {
box-shadow: 0 0 15px #00ff00;
}

View File

@@ -2,23 +2,16 @@
@tailwind components;
@tailwind utilities;
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
.bg-dark {
background-color: #111;
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
.glow {
text-shadow: 0 0 10px #00ff00;
}
.shadow-neon {
box-shadow: 0 0 15px #ff00de;
}
.btn-neon {
background-color: #00ff00;
color: #111;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
box-shadow: 0 0 10px #00ff00;
}
.btn-neon:hover {
box-shadow: 0 0 15px #00ff00;
.game-card:hover {
@apply shadow-lg shadow-crypto-green/50;
}

View File

@@ -0,0 +1,82 @@
<template>
<div class="min-h-screen">
<nav class="bg-gray-800 p-4 flex justify-between">
<div class="text-2xl font-bold">Skunk Lounge</div>
<div class="space-x-4">
<span class="text-crypto-green">{{ user.name }} (VIP Level: {{ user.vip_level }})</span>
<span v-if="user.is_kyc_verified" class="text-green-400">KYC Verified</span>
<a href="/lobby" class="hover:text-crypto-green">Games</a>
<a href="/profile" class="hover:text-crypto-green">Profile</a>
<a href="/logout" class="hover:text-crypto-green">Logout</a>
</div>
</nav>
<div class="p-6">
<div v-if="success" class="bg-green-500 p-2 rounded mb-4">{{ success }}</div>
<div v-if="error" class="bg-red-500 p-2 rounded mb-4">{{ error }}</div>
<div class="bg-gray-800 rounded-lg p-4 mb-6 flex justify-between items-center">
<span>Skunk Coins: {{ wallet.balance }}</span>
<div class="flex space-x-4">
<form @submit.prevent="addCoins">
<input v-model="amount" type="number" class="bg-gray-700 p-2 rounded text-white" placeholder="Enter amount">
<button type="submit" class="bg-crypto-green hover:bg-green-600 text-white px-4 py-2 rounded ml-2">Add Coins</button>
</form>
<form @submit.prevent="redeemPromo">
<input v-model="promoCode" type="text" class="bg-gray-700 p-2 rounded text-white" placeholder="Promo code">
<button type="submit" class="bg-crypto-green hover:bg-green-600 text-white px-4 py-2 rounded ml-2">Redeem</button>
</form>
</div>
</div>
<div class="bg-gray-800 rounded-lg p-4 mb-6">
<h3 class="text-lg mb-2">Player Stats</h3>
<p>Total Winnings: {{ stats.total_winnings }} Skunk Coins</p>
<p>Total Bets: {{ stats.total_bets }} Skunk Coins</p>
<p>Play Count: {{ stats.play_count }}</p>
<p>Win Streak: {{ stats.win_streak }}</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="game in games" :key="game.id" class="bg-gray-800 rounded-lg overflow-hidden transform hover:scale-105 transition game-card">
<img :src="game.thumbnail_url" :alt="game.name" class="w-full h-48 object-cover">
<div class="p-4">
<h3 class="text-lg">{{ game.name }}</h3>
<button class="bg-crypto-green hover:bg-green-600 text-white px-4 py-2 rounded mt-2">Play Now</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3'; // Use useForm for Inertia POST requests
defineProps({
wallet: Object,
games: Array,
user: Object,
stats: Object,
success: String,
error: String,
});
const depositForm = useForm({ amount: '' });
const promoForm = useForm({ code: '' });
const addCoins = () => {
depositForm.post('/lobby/add-coins', {
onSuccess: () => depositForm.reset(),
});
};
const redeemPromo = () => {
promoForm.post('/lobby/redeem-promo', {
onSuccess: () => promoForm.reset(),
});
};
</script>
<style scoped>
.game-card {
@apply transition-all duration-300 hover:shadow-lg hover:shadow-crypto-green/50;
}
</style>

View File

@@ -1,7 +1,13 @@
import './bootstrap';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true });
return pages[`./Pages/${name}.vue`];
},
setup({ el, App, props }) {
createApp({ render: () => h(App, props) }).mount(el);
},
});

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Skunk Lounge</title>
@vite(['resources/js/app.js', 'resources/css/app.css'])
@inertiaHead
</head>
<body class="bg-dark-bg text-white">
@inertia
</body>
</html>

View File

@@ -1,27 +0,0 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -1,25 +0,0 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
</div>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -1,47 +0,0 @@
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -1,52 +0,0 @@
<x-guest-layout>
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-primary-button class="ms-4">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -1,39 +0,0 @@
<x-guest-layout>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -1,31 +0,0 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@@ -1,3 +0,0 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,7 +0,0 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif

View File

@@ -1,3 +0,0 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -1 +0,0 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -1,35 +0,0 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
@php
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
</div>

View File

@@ -1,9 +0,0 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif

View File

@@ -1,5 +0,0 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
{{ $value ?? $slot }}
</label>

View File

@@ -1,61 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'Skunk Lounge' }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Arial&display=swap" rel="stylesheet">
<!-- Bootstrap (for grid/responsiveness) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<!-- Tailwind CSS -->
@vite('resources/css/app.css')
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
</head>
<body class="bg-dark text-white font-sans antialiased">
<!-- Top Bar -->
<header class="bg-gray_900 shadow-neon p-4 fixed top-0 left-0 right-0 z-50">
<div class="container mx-auto flex justify-between items-center">
<a href="/lobby" class="text-2xl font-bold text-neon_green text-glow">Skunk Lounge</a>
<div class="flex items-center">
<span class="mr-4 text-lg">Balance: ${{ auth()->user()->balance ?? 0 }}</span>
<a href="/deposits" class="btn btn-neon">Deposit</a>
</div>
</div>
</header>
<!-- Left Sidebar Menu -->
<nav class="fixed top-16 left-0 bottom-0 w-64 bg-gray_900 p-4 shadow-neon z-40 overflow-y-auto">
<div class="mb-4">
<!-- Logo -->
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="w-full mb-4"> <!-- Add logo to public/images if needed -->
</div>
<ul>
<li><a href="/lobby" class="block py-2 px-4 hover:bg-gray_800 rounded text-white hover:text-neon_green text-glow">Lobby</a></li>
<li><a href="/profile" class="block py-2 px-4 hover:bg-gray_800 rounded text-white hover:text-neon_green text-glow">Profile</a></li>
<li><a href="/deposits" class="block py-2 px-4 hover:bg-gray_800 rounded text-white hover:text-neon_green text-glow">Deposits</a></li>
<li><a href="/transactions" class="block py-2 px-4 hover:bg-gray_800 rounded text-white hover:text-neon_green text-glow">Transactions</a></li>
<li><a href="/vip" class="block py-2 px-4 hover:bg-gray_800 rounded text-white hover:text-neon_green text-glow">VIP</a></li>
<li><a href="/about" class="block py-2 px-4 hover:bg-gray_800 rounded text-white hover:text-neon_green text-glow">About Us</a></li>
</ul>
</nav>
<!-- Main Content -->
<main class="ml-64 pt-16 p-4 min-h-screen">
{{ $slot }}
</main>
<!-- Footer -->
<footer class="bg-gray_900 p-4 text-center text-gray-500">
Fake casino for entertainment only. No real money.
</footer>
</body>
</html>

View File

@@ -1,78 +0,0 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div
x-show="show"
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{{ $slot }}
</div>
</div>

View File

@@ -1,11 +0,0 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -1,3 +0,0 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -1,11 +0,0 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -1,3 +0,0 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -1,3 +0,0 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>

View File

@@ -1,17 +0,0 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{ __("You're logged in!") }}
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -1,3 +0,0 @@
<x-filament-panels::page>
</x-filament-panels::page>

View File

@@ -1,4 +0,0 @@
<x-filament-panels::page>
<h1 class="text-2xl font-bold mb-4">Deposits</h1>
<p>Claim promo codes for fake currency. Claim history below (coming soon).</p>
</x-filament-panels::page>

View File

@@ -1,20 +0,0 @@
<x-filament-panels::page>
<h1 class="text-2xl font-bold mb-4">Casino Lobby</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="card bg-gray-800 p-4 rounded">
<h3 class="text-lg mb-2">Slots</h3>
<p>Pragmatic-style reels and animations.</p>
<a href="/games/slots" class="filament-button filament-button-size-sm">Play</a>
</div>
<div class="card bg-gray-800 p-4 rounded">
<h3 class="text-lg mb-2">Plinko</h3>
<p>Physics-based drop with multipliers.</p>
<a href="/games/plinko" class="filament-button filament-button-size-sm">Play</a>
</div>
<div class="card bg-gray-800 p-4 rounded">
<h3 class="text-lg mb-2">Wheel Spin</h3>
<p>Fortune wheel with prizes.</p>
<a href="/games/wheel" class="filament-button filament-button-size-sm">Play</a>
</div>
</div>
</x-filament-panels::page>

View File

@@ -1,4 +0,0 @@
<x-filament-panels::page>
<h1 class="text-2xl font-bold mb-4">Profile</h1>
<p>Change password, profile picture, email, add 2FA (coming soon).</p>
</x-filament-panels::page>

View File

@@ -1,3 +0,0 @@
<x-filament-panels::page>
<h1 class="text-2xl font-bold mb-4">Transactions</h1><p>View deposits, withdrawals, wins/losses (coming soon).</p>
</x-filament-panels::page>

View File

@@ -1,3 +0,0 @@
<x-filament-panels::page>
<h1 class="text-2xl font-bold mb-4">About Us</h1><p>Skunk Lounge is a fake casino for entertainment only. No real money.</p>
</x-filament-panels::page>

View File

@@ -1,4 +0,0 @@
<x-filament-widgets::widget>
<x-filament::section>
<a href="/admin" class="btn-neon">Admin Panel</a>
</x-filament-widgets::widget>

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Skunk Lounge - Fake Casino</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<style>
body { background-color: #111; color: #fff; font-family: Arial, sans-serif; }
.glow { text-shadow: 0 0 10px #ff00de; }
.shadow-neon { box-shadow: 0 0 15px #ff00de; }
.btn-neon { background-color: #ff00de; color: #111; padding: 0.5rem 1rem; border-radius: 0.25rem; box-shadow: 0 0 10px #ff00de; }
.btn-neon:hover { box-shadow: 0 0 15px #ff00de; }
</style>
</head>
<body>
<div class="container mx-auto mt-4 bg-gray-900 text-white p-4 rounded shadow-neon">
<h1 class="text-3xl font-bold mb-4 glow">Welcome to Skunk Lounge</h1>
<p class="mb-4">Self-hosted fake casino for entertainment only. No real money—only fake currency for dopamine rushes. Mimicking Stake.com and Jetmyst.com with sleek navigation and vibrant games.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div class="card bg-gray-800 p-4 rounded shadow-neon">
<h3 class="text-lg mb-2">Games Available</h3>
<p class="text-2xl font-bold glow">{{ $gamesCount }}</p>
</div>
<div class="card bg-gray-800 p-4 rounded shadow-neon">
<h3 class="text-lg mb-2">Registered Users</h3>
<p class="text-2xl font-bold glow">{{ $usersCount }}</p>
</div>
<div class="card bg-gray-800 p-4 rounded shadow-neon">
<h3 class="text-lg mb-2">Biggest Jackpot Won</h3>
<p class="text-2xl font-bold glow">${{ $biggestJackpot }}</p>
</div>
</div>
<div class="flex justify-center">
<a href="{{ route('filament.user.auth.login') }}" class="btn-neon mr-4">Login</a>
<a href="{{ route('filament.user.auth.register') }}" class="btn-neon">Register</a>
</div>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More