🆙 Delete not used things 🆙
@@ -1,221 +0,0 @@
|
||||
# Arcturus Morningstar Extended
|
||||
|
||||
A high-performance, open-source Habbo hotel emulator built with Java 21 and Netty.
|
||||
|
||||
## Features
|
||||
|
||||
- **Modern Architecture** - Built on Netty for high-performance networking
|
||||
- **Database Optimization** - HikariCP connection pooling with prepared statement caching
|
||||
- **Efficient Collections** - Trove collections (THashMap, THashSet) for faster operations
|
||||
- **Async Pathfinding** - A* pathfinding with CompletableFuture for non-blocking operations
|
||||
- **Thread Pooling** - Optimized thread management for concurrent tasks
|
||||
- **In-Memory Caching** - Frequently accessed data cached at startup
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 21 or higher
|
||||
- MySQL 8.0+
|
||||
- Maven 3.8+
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/your-repo/arcturus-morningstar-extended.git
|
||||
cd arcturus-morningstar-extended/Emulator
|
||||
```
|
||||
|
||||
2. **Configure the database**
|
||||
- Create a MySQL database
|
||||
- Import the SQL schema (see `database` folder)
|
||||
- Edit `config.ini` with your database credentials
|
||||
|
||||
3. **Build the project**
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
4. **Run the emulator**
|
||||
```bash
|
||||
java -jar target/Habbo-4.0.5-jar-with-dependencies.jar
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.ini` to configure:
|
||||
|
||||
```ini
|
||||
# Database
|
||||
db.hostname=localhost
|
||||
db.port=3306
|
||||
db.database=habbo
|
||||
db.username=root
|
||||
db.password=yourpassword
|
||||
db.pool.maxsize=50
|
||||
db.pool.minsize=10
|
||||
|
||||
# Game Server
|
||||
game.port=30000
|
||||
|
||||
# RCON
|
||||
rcon.port=30001
|
||||
rcon.allowed=127.0.0.1
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Database Pool Settings
|
||||
|
||||
The emulator uses HikariCP with optimized settings:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `db.pool.maxsize` | 50 | Maximum connections |
|
||||
| `db.pool.minsize` | 10 | Minimum idle connections |
|
||||
| `cachePrepStmts` | true | Prepared statement caching |
|
||||
| `useServerPrepStmts` | true | Server-side prepared statements |
|
||||
| `rewriteBatchedStatements` | true | Batch query optimization |
|
||||
|
||||
### Thread Pool Settings
|
||||
|
||||
Configure in `config.ini`:
|
||||
```ini
|
||||
runtime.threads=16
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **Database** - HikariCP connection pooling with query optimization
|
||||
- **Network** - Netty-based TCP/UDP server
|
||||
- **Game Engine** - Room management, user handling, items
|
||||
- **Pathfinding** - A* algorithm with height support
|
||||
- **Plugin System** - Event-driven architecture
|
||||
|
||||
### Key Classes
|
||||
|
||||
- `Emulator.java` - Main entry point
|
||||
- `DatabasePool.java` - Connection pool management
|
||||
- `RoomManager.java` - Room lifecycle management
|
||||
- `PathfinderImpl.java` - A* pathfinding implementation
|
||||
- `CatalogManager.java` - Catalog and shop management
|
||||
- `ItemManager.java` - Furniture/items management
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Development build
|
||||
mvn compile
|
||||
|
||||
# Production build
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# Run tests
|
||||
mvn test
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/main/java/com/eu/habbo/
|
||||
├── database/ # Database connections and pooling
|
||||
├── habbohotel/ # Core game logic
|
||||
│ ├── bots/ # Bot AI
|
||||
│ ├── catalog/ # Shop system
|
||||
│ ├── commands/ # Console commands
|
||||
│ ├── guilds/ # Guild system
|
||||
│ ├── items/ # Furniture/items
|
||||
│ ├── pets/ # Pet system
|
||||
│ ├── rooms/ # Room management & pathfinding
|
||||
│ └── users/ # User management
|
||||
├── messages/ # Network messages (incoming/outgoing)
|
||||
├── networking/ # Network handlers
|
||||
├── threading/ # Thread management
|
||||
└── util/ # Utilities
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Implemented Optimizations
|
||||
|
||||
1. **Database Layer**
|
||||
- HikariCP connection pooling
|
||||
- Prepared statement caching
|
||||
- Batch statement rewriting
|
||||
- Server-side prepared statements
|
||||
|
||||
2. **Memory & Collections**
|
||||
- Trove collections (faster than standard Java)
|
||||
- In-memory caching for static data
|
||||
- Object reuse where possible
|
||||
|
||||
3. **Networking**
|
||||
- Netty ByteBuf for zero-copy operations
|
||||
- Efficient buffer handling
|
||||
- Async I/O operations
|
||||
|
||||
4. **Pathfinding**
|
||||
- A* algorithm with optimization
|
||||
- Timeout protection for long paths
|
||||
- Tile state caching
|
||||
|
||||
### Benchmark Expectations
|
||||
|
||||
With default settings:
|
||||
- Database queries: 20-40% faster with connection pooling
|
||||
- Memory usage: Optimized with Trove collections
|
||||
- Network throughput: High-performance with Netty
|
||||
- Pathfinding: Protected with execution timeouts
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Database Connection Failed**
|
||||
- Check MySQL is running
|
||||
- Verify credentials in config.ini
|
||||
- Ensure database exists
|
||||
|
||||
**OutOfMemory Errors**
|
||||
- Increase heap size: `java -Xmx4g -jar ...`
|
||||
- Reduce pool size in config.ini
|
||||
|
||||
**Lag Issues**
|
||||
- Increase `runtime.threads` in config.ini
|
||||
- Optimize MySQL with indexes
|
||||
- Use SSD for database storage
|
||||
|
||||
### Logging
|
||||
|
||||
Logs are written to:
|
||||
- Console (stdout)
|
||||
- `logs/` folder (file output)
|
||||
|
||||
Configure logging in `src/main/resources/logback.xml`
|
||||
|
||||
## License
|
||||
|
||||
This project is for educational purposes. This is a fork of the original Arcturus emulator.
|
||||
|
||||
## Credits
|
||||
|
||||
- Original Arcturus team
|
||||
- Habbo Hotel (Sulake)
|
||||
- Netty Project
|
||||
- Trove Collections
|
||||
- HikariCP
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- GitHub Issues: Report bugs and feature requests
|
||||
- Discord: Join the community
|
||||
|
||||
---
|
||||
|
||||
**Version:** 4.0.5
|
||||
**Java:** 21+
|
||||
**Build:** Maven
|
||||
|
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 283 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 408 B |
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 381 B After Width: | Height: | Size: 379 B |
|
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 280 B |
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 524 B |
|
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 521 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 670 B After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 464 B |
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 439 B |
|
Before Width: | Height: | Size: 432 B After Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 622 B |
|
Before Width: | Height: | Size: 246 B After Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 453 B |
|
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 331 B |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v10.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v10.0": {
|
||||
"EPICWrap/1.0.0": {
|
||||
"runtime": {
|
||||
"EPICWrap.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"EPICWrap/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net10.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "10.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
EPICWrap V61.2.0
|
||||
Project: Epicnabbo
|
||||
Emulator: MORNINGSTAR
|
||||
By Remco
|
||||
@@ -1,34 +0,0 @@
|
||||
#######################################################
|
||||
# EPICWrap - Release Notes #
|
||||
# Version: 61.2.0 (2026) #
|
||||
# Developer: Remco #
|
||||
#######################################################
|
||||
|
||||
[MAJOR UPDATE: USER EXPERIENCE]
|
||||
- Added Welcome Wizard: A new introduction screen that
|
||||
explains the tool's features to new users.
|
||||
- Enhanced Menu Clarity: Every selection step now includes
|
||||
detailed "HELP" and "INFO" text to guide the user.
|
||||
- Improved Navigation: Full "Back" button (B) logic
|
||||
integrated into all project configuration steps.
|
||||
|
||||
[ASSET ENGINE & LOGIC]
|
||||
- Added Clothing Support: Process and organize clothing
|
||||
assets and figuredata XMLs seamlessly.
|
||||
- Added Badge Support: Dedicated downloader and
|
||||
organization engine for hotel badge collections.
|
||||
- Multi-Folder Logic: Automatically creates subdirectories
|
||||
(/furni, /clothing, /badges) based on your needs.
|
||||
|
||||
[TECHNICAL & STABILITY]
|
||||
- State-Machine Architecture: Rewritten menu logic for
|
||||
100% stability and zero crashes during navigation.
|
||||
- Auto-Provisioning: Enhanced directory creation with
|
||||
improved Windows 11 permission handling.
|
||||
- Security: Forced TLS 1.3 protocols for safe GitLab
|
||||
communication and silent updates.
|
||||
|
||||
#######################################################
|
||||
# Official Repo: gitlab.epicnabbo.nl #
|
||||
# Developed by Remco - Master Edition 2026 #
|
||||
#######################################################
|
||||
@@ -1 +0,0 @@
|
||||
61.2.0
|
||||
@@ -1,18 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
@@ -1,130 +0,0 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Change those to match your database settings
|
||||
# For Docker: use 'mariadb' as DB_HOST
|
||||
# For local: use '127.0.0.1' as DB_HOST
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=atomcms
|
||||
DB_USERNAME=atomcms
|
||||
DB_PASSWORD=password
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
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}"
|
||||
|
||||
## Custom variables
|
||||
RCON_HOST=127.0.0.1
|
||||
RCON_PORT=3001
|
||||
|
||||
# Find retros settings
|
||||
FINDRETROS_NAME=
|
||||
FINDRETROS_ENABLED=false
|
||||
|
||||
# Recaptcha settings
|
||||
GOOGLE_RECAPTCHA_SITE_KEY=
|
||||
GOOGLE_RECAPTCHA_SECRET_KEY=
|
||||
|
||||
# Turnstile
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
|
||||
# If set to true, Atom will rename any colliding table names when migration the first time around
|
||||
RENAME_COLLIDING_TABLES=false
|
||||
|
||||
# Flash client settings
|
||||
FLASH_CLIENT_ENABLED=false
|
||||
EMULATOR_IP=127.0.0.1
|
||||
EMULATOR_PORT=3000
|
||||
SWF_BASE_PATH=client/flash
|
||||
HABBO_SWF=Habbo.swf
|
||||
PRODUCTION_FOLDER=gordon/PRODUCTION
|
||||
EXTERNAL_FURNIDATA=gamedata/furnidata.xml
|
||||
EXTERNAL_FIGUREMAP=gamedata/figuremap.xml
|
||||
EXTERNAL_FIGUREDATA=gamedata/figuredata.xml
|
||||
EXTERNAL_PRODUCTDATA=gamedata/productdata.txt
|
||||
EXTERNAL_TEXTS=gamedata/external_flash_texts.txt
|
||||
EXTERNAL_VARIABLES=gamedata/external_variables.txt
|
||||
EXTERNAL_OVERRIDE_TEXTS=gamedata/override/external_flash_override_texts.txt
|
||||
EXTERNAL_OVERRIDE_VARIABLES=gamedata/override/external_override_variables.txt
|
||||
|
||||
# Only enable if you come from a CMS like RevCMS
|
||||
# This will only work if your password is hashed using md5
|
||||
# By default Atom CMS uses bcrypt, this is purely used to ease the process, when switching from a CMS using md5
|
||||
CONVERT_PASSWORDS=false
|
||||
|
||||
# Enable this if your site is running through https, but you're experiencing issues with requests being made to "http"
|
||||
FORCE_HTTPS=false
|
||||
|
||||
# Default language for the site
|
||||
APP_LOCALE=en
|
||||
|
||||
# Enter the time in minutes that a password reset token can be valid
|
||||
PASSWORD_RESET_TOKEN_TIME=15
|
||||
|
||||
# General paypal options
|
||||
PAYPAL_MODE='sandbox'
|
||||
PAYPAL_PAYMENT_ACTION='Order'
|
||||
PAYPAL_CURRENCY='USD'
|
||||
PAYPAL_NOTIFY_URL=
|
||||
PAYPAL_LOCALE='en_US'
|
||||
PAYPAL_VALIDATE_SSL=true
|
||||
|
||||
#PayPal Setting & API Credentials - sandbox
|
||||
PAYPAL_SANDBOX_CLIENT_ID=
|
||||
PAYPAL_SANDBOX_CLIENT_SECRET=
|
||||
PAYPAL_SANDBOX_APP_ID=
|
||||
|
||||
#PayPal Setting & API Credentials - live
|
||||
PAYPAL_LIVE_CLIENT_ID=
|
||||
PAYPAL_LIVE_CLIENT_SECRET=
|
||||
PAYPAL_LIVE_APP_ID=
|
||||
|
||||
FORTIFY_PREFIX=
|
||||
@@ -1,41 +0,0 @@
|
||||
APP_NAME=Atom
|
||||
APP_ENV=testing
|
||||
APP_KEY=base64:GENERATE_NEW_KEY_HERE
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mariadb
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=testing
|
||||
DB_USERNAME=docker
|
||||
DB_PASSWORD=password
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=array
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=array
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MAIL_MAILER=array
|
||||
|
||||
# Testing specific settings
|
||||
BCRYPT_ROUNDS=4
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
# Disable CAPTCHA for testing
|
||||
GOOGLE_RECAPTCHA_ENABLED=0
|
||||
CLOUDFLARE_TURNSTILE_ENABLED=0
|
||||
|
||||
# Cloudflare Turnstile (test keys)
|
||||
TURNSTILE_SITE_KEY=1x00000000000000000000AA
|
||||
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
|
||||
|
||||
# Google reCAPTCHA (dummy values for testing)
|
||||
GOOGLE_RECAPTCHA_SITE_KEY=dummy_site_key
|
||||
GOOGLE_RECAPTCHA_SECRET_KEY=dummy_secret_key
|
||||
@@ -1,11 +0,0 @@
|
||||
* text=auto
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
@@ -1,39 +0,0 @@
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/client
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/app/public
|
||||
/storage/framework/*
|
||||
/vendor
|
||||
/public/static
|
||||
.env
|
||||
.env.backup
|
||||
.env.testing
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
/.idea
|
||||
/.vscode
|
||||
/public/assets/images/generated-logos
|
||||
CLAUDE.md
|
||||
.phpunit.cache
|
||||
.yarn
|
||||
public/js
|
||||
public/css
|
||||
public/fonts
|
||||
public/vendor
|
||||
.junie
|
||||
.claude
|
||||
.github
|
||||
.ai
|
||||
.codex
|
||||
.cursor
|
||||
opencode.json
|
||||
.mcp.json
|
||||
boost.json
|
||||
@@ -1,36 +0,0 @@
|
||||
# Quick change
|
||||
|
||||
## Configuration
|
||||
- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}`
|
||||
|
||||
---
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
This is a quick change workflow for small or straightforward tasks where all requirements are clear from the task description.
|
||||
|
||||
### Your Approach
|
||||
|
||||
1. Proceed directly with implementation
|
||||
2. Make reasonable assumptions when details are unclear
|
||||
3. Do not ask clarifying questions unless absolutely blocked
|
||||
4. Focus on getting the task done efficiently
|
||||
|
||||
This workflow also works for experiments when the feature is bigger but you don't care about implementation details.
|
||||
|
||||
If blocked or uncertain on a critical decision, ask the user for direction.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### [ ] Step: Implementation
|
||||
|
||||
Implement the task directly based on the task description.
|
||||
|
||||
1. Make reasonable assumptions for any unclear details
|
||||
2. Implement the required changes in the codebase
|
||||
3. Add and run relevant tests and linters if applicable
|
||||
4. Perform basic manual verification if applicable
|
||||
|
||||
Save a brief summary of what was done to `{@artifacts_path}/report.md` if significant changes were made.
|
||||
@@ -1,260 +0,0 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
Atom CMS runs on Laravel 12. Keep application logic in `app/` (controllers, events, jobs) and shareable domain services under `app/Services`. Blade templates, JS/TS, and theme assets live in `resources/`, with theme-specific Vite setup inside `resources/themes/atom` and `resources/themes/dusk`. API and web routes belong in `routes/` (`web.php`, `api.php`, `console.php`). Database migrations, factories, and seeders remain in `database/`. Static assets served publicly should be placed in `public/`. Feature and unit tests live in `tests/Feature` and `tests/Unit`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Install dependencies with `composer install` and `npm install`. Spin up the Laravel dev server with `php artisan serve` or, when using Sail, `./vendor/bin/sail up`. Use `npm run dev:atom` or `npm run dev:dusk` for hot module reloads; the matching `npm run build:atom` or `npm run build:dusk` commands generate production assets in `public/build`. Prepare the database via `php artisan migrate --seed`. Run the test suite with `./vendor/bin/pest` (or `./vendor/bin/sail pest` inside Sail). To inspect queues, caches, or config, prefer standard Artisan tools (`php artisan queue:work`, `php artisan config:clear`).
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
Follow the Laravel Pint preset configured in `pint.json`; run `./vendor/bin/pint` before committing to enforce PSR-12-compatible formatting and ordered imports. Keep PHP indentation at four spaces, and favor descriptive class names (`HotelRoomController`, `SyncUserRanksJob`). Frontend code is formatted with Prettier (`npm run format`), which also handles Blade components; avoid manual alignment tweaks that Prettier will rewrite. Use snake_case for config keys, kebab-case for asset filenames, and PascalCase for Vue components.
|
||||
|
||||
## Testing Guidelines
|
||||
We rely on Pest v4 (`tests/Pest.php`) as the primary test runner. Execute `./vendor/bin/pest` locally, or `./vendor/bin/sail pest` when using Sail; append flags like `--coverage` as needed. Organize feature tests under `tests/Feature` and focused units under `tests/Unit`, maintaining the `*Test.php` suffix so Pest discovers them. When introducing new behavior, cover the happy path and dominant failure modes, and refresh seed data if fixtures change. Keep suites idempotent—prefer migrations and factories over manual SQL setup.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Commits in this repo use imperative, lowercase subjects with optional scopes, e.g. `refactor(profile): improve dashboard grid` or `style: run laravel pint`. Group related changes together and avoid WIP commits. Pull requests should describe the intent, link to tracked issues, and include screenshots or terminal output when UI or console behavior changes. Confirm tests (`./vendor/bin/pest`) and Pint/Prettier checks pass before requesting review.
|
||||
|
||||
===
|
||||
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4.8
|
||||
- filament/filament (FILAMENT) - v4
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- livewire/livewire (LIVEWIRE) - v3
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- pestphp/pest (PEST) - v3
|
||||
- phpunit/phpunit (PHPUNIT) - v11
|
||||
- rector/rector (RECTOR) - v2
|
||||
- alpinejs (ALPINEJS) - v3
|
||||
- tailwindcss (TAILWINDCSS) - v4
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pest-testing` — Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
- `tailwindcss-development` — Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan
|
||||
|
||||
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
|
||||
|
||||
## URLs
|
||||
|
||||
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
|
||||
|
||||
## Tinker / Debugging
|
||||
|
||||
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
|
||||
## Reading Browser Logs With the `browser-logs` Tool
|
||||
|
||||
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||
- Only recent browser logs will be useful - ignore old logs.
|
||||
|
||||
## Searching Documentation (Critically Important)
|
||||
|
||||
- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Available Search Syntax
|
||||
|
||||
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
|
||||
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
|
||||
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
|
||||
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
|
||||
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
## Database
|
||||
|
||||
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||
- Use Eloquent models and relationships before suggesting raw database queries.
|
||||
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||
- Generate code that prevents N+1 query problems by using eager loading.
|
||||
- Use Laravel's query builder for very complex database operations.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
|
||||
|
||||
### APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## Controllers & Validation
|
||||
|
||||
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure.
|
||||
- This is perfectly fine and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it.
|
||||
|
||||
## Laravel 10 Structure
|
||||
|
||||
- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`.
|
||||
- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
|
||||
- Middleware registration happens in `app/Http/Kernel.php`
|
||||
- Exception handling is in `app/Exceptions/Handler.php`
|
||||
- Console commands and schedule register in `app/Console/Kernel.php`
|
||||
- Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
|
||||
|
||||
## Database
|
||||
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
# Tailwind CSS
|
||||
|
||||
- Always use existing Tailwind conventions; check project patterns before adding new ones.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data.
|
||||
- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task.
|
||||
</laravel-boost-guidelines>
|
||||
@@ -1,4 +0,0 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, the owners will be requested for
|
||||
# review when someone opens a pull request.
|
||||
* @ObjectRetros
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright 2023 ObjectRetros
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,196 +0,0 @@
|
||||
# AtomCMS Remco Epicnabbo Edition
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/9ePNdJ4.png" alt="Atom CMS"/>
|
||||
|
||||
A modern, community-driven Retro CMS built with Laravel 12.x - **PHP 8.5 Ready**
|
||||
|
||||
[](https://discord.gg/pP6HyZedAj)
|
||||
[](https://laravel.com)
|
||||
[](https://php.net)
|
||||
[](https://phpstan.org)
|
||||
|
||||
</div>
|
||||
|
||||
> Customized version of Atom CMS by Remco (Epicnabbo) with PHP 8.5 compatibility and full PHPStan level 9 compliance.
|
||||
|
||||
## 📋 What Changed
|
||||
|
||||
### PHP 8.5 Upgrade
|
||||
- **Upgraded to PHP 8.5** using Rector UP_TO_PHP_85
|
||||
- Updated `rector.php` configuration for PHP 8.5
|
||||
- Applied modern PHP 8.5 features and best practices
|
||||
|
||||
### PHPStan Level 9 Compliance
|
||||
- **Fixed all PHPStan level 9 errors** (0 errors achieved)
|
||||
- Fixed return types in all controllers
|
||||
- Fixed middleware nullsafe operators
|
||||
- Added proper type declarations
|
||||
- Updated `phpstan.neon` configuration
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed class naming issues in `WebsiteTeam.php`
|
||||
- Fixed `PasswordValidationRules` trait imports
|
||||
- Fixed duplicate imports in Filament resources
|
||||
- Fixed `Form::fill()` usage in `ManageBadgeUploads.php`
|
||||
- Fixed `array_chunk` parameter types in `InstallationController.php`
|
||||
- Fixed `isset`/logic issues in `PaypalController.php`
|
||||
- Fixed `GoogleRecaptchaRule` return type
|
||||
- Added missing `TurnstileCheck` class implementation
|
||||
|
||||
### New Features
|
||||
- Added `ExternalTextsParser.php` stub with required methods
|
||||
- Added `getPredominantImageColor()` helper function
|
||||
- Fixed `getPredominantImageColor()` in `ArticleResource.php`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 About
|
||||
|
||||
Atom CMS is a modern, community-driven CMS designed to provide a flexible and user-friendly platform for retro hotel management. This version is optimized for PHP 8.5 with strict type checking.
|
||||
|
||||
### Built With
|
||||
|
||||
- **[Laravel 12.x](https://laravel.com/docs/12.x)** - Elegant PHP framework powering the backend
|
||||
- **[Vite](https://vitejs.dev/)** - Next-generation frontend tooling for blazing-fast builds
|
||||
- **[TailwindCSS](https://tailwindcss.com/)** - Utility-first CSS framework for responsive design
|
||||
- **PHP 8.5** - Latest PHP version with modern features
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🎨 **Built-in Theme System** - Use any CSS framework or create custom themes
|
||||
- 🔐 **Secure Authentication** - Laravel-powered authentication and authorization
|
||||
- 🌍 **Multi-language Support** - Built-in localization for global audiences
|
||||
- 📊 **Integrated Housekeeping** - Comprehensive admin panel
|
||||
- 🔄 **Rcon System** - Real-time server communication
|
||||
- 📱 **Responsive Design** - Mobile-first approach with TailwindCSS
|
||||
- 🚀 **PHP 8.5 Ready** - Latest PHP features with full type safety
|
||||
|
||||
---
|
||||
|
||||
## 👀 Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|------------|---------|
|
||||
| PHP | **8.5** or higher |
|
||||
| MySQL | 8.x or higher |
|
||||
| MariaDB | 10.x or higher |
|
||||
| Composer | v2 |
|
||||
| Node.js | LTS |
|
||||
|
||||
### Required PHP Extensions
|
||||
|
||||
```ini
|
||||
extension=curl
|
||||
extension=fileinfo
|
||||
extension=gd
|
||||
extension=mbstring
|
||||
extension=openssl
|
||||
extension=pdo_mysql
|
||||
extension=sockets
|
||||
extension=intl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd atomcms
|
||||
|
||||
# Configure environment
|
||||
copy .env.example .env
|
||||
# Edit .env and update your database credentials
|
||||
|
||||
# Install dependencies
|
||||
composer install
|
||||
npm install
|
||||
|
||||
# Set up database and generate key
|
||||
php artisan migrate --seed
|
||||
php artisan key:generate
|
||||
|
||||
# Build assets
|
||||
npm run build:atom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Verification
|
||||
|
||||
Run PHPStan to verify level 9 compliance:
|
||||
|
||||
```bash
|
||||
php -d memory_limit=2G vendor/bin/phpstan analyse --no-progress --error-format=table
|
||||
```
|
||||
|
||||
Expected output: **0 errors**
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Modified
|
||||
|
||||
### Core Upgrades
|
||||
- `rector.php` - PHP 8.5 configuration
|
||||
- `phpstan.neon` - Level 9 configuration
|
||||
|
||||
### Controllers Fixed
|
||||
- `app/Http/Controllers/Community/LeaderboardController.php`
|
||||
- `app/Http/Controllers/Community/Staff/StaffApplicationsController.php`
|
||||
- `app/Http/Controllers/Community/Staff/WebsiteTeamApplicationsController.php`
|
||||
- `app/Http/Controllers/Community/WebsiteRareValuesController.php`
|
||||
- `app/Http/Controllers/Miscellaneous/InstallationController.php`
|
||||
- `app/Http/Controllers/Shop/PaypalController.php`
|
||||
|
||||
### Middleware Fixed
|
||||
- `app/Http/Middleware/InstallationMiddleware.php`
|
||||
- `app/Http/Middleware/MaintenanceMiddleware.php`
|
||||
- `app/Http/Middleware/SetThemeMiddleware.php`
|
||||
|
||||
### Filament Resources Fixed
|
||||
- `app/Filament/Resources/Atom/Articles/ArticleResource.php`
|
||||
- `app/Filament/Resources/Hotel/BadgeUploads/Pages/ManageBadgeUploads.php`
|
||||
- `app/Filament/Resources/Atom/Permissions/PermissionResource.php`
|
||||
|
||||
### Services & Rules
|
||||
- `app/Services/Articles/ReactionService.php`
|
||||
- `app/Services/Parsers/ExternalTextsParser.php` (new)
|
||||
- `app/Http/Rules/GoogleRecaptchaRule.php`
|
||||
- `app/Http/Rules/TurnstileCheck.php`
|
||||
|
||||
### Helpers
|
||||
- `app/Helpers/helper.php` - Added `getPredominantImageColor()`
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For original Atom CMS documentation, visit the [official wiki](https://github.com/atom-retros/atomcms/wiki).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Credits
|
||||
|
||||
### Original Atom CMS Team
|
||||
- **Kasja** - Design direction, Dusk theme, ideas & graphics
|
||||
- **INicollas** - Dark mode, Turbolinks, article reactions, user sessions
|
||||
- **Kani** - Rcon system, FindRetros API, Atom CMS v2 creator/maintainer
|
||||
- **DuckieTM** - Badge drawer, bugfixes, housekeeping features
|
||||
- **EntenKoeniq** - Auto language registration, color scheme selection
|
||||
|
||||
### This Edition
|
||||
- **Remco (Epicnabbo)** - PHP 8.5 upgrade, PHPStan level 9 compliance
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**AtomCMS Remco Epicnabbo Edition** - PHP 8.5 Compatible
|
||||
|
||||
*Made with ❤️ by the Epicnabbo Community*
|
||||
|
||||
</div>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Events\RecoveryCodeReplaced;
|
||||
use Laravel\Fortify\Http\Requests\TwoFactorLoginRequest;
|
||||
use Laravel\Fortify\Http\Responses\TwoFactorLoginResponse;
|
||||
|
||||
class TwoFactorAuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* The guard implementation.
|
||||
*
|
||||
* @var StatefulGuard
|
||||
*/
|
||||
protected $guard;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(StatefulGuard $guard)
|
||||
{
|
||||
$this->guard = $guard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate a new session using the two factor authentication code.
|
||||
*/
|
||||
public function store(TwoFactorLoginRequest $request): TwoFactorLoginResponse
|
||||
{
|
||||
$user = $request->challengedUser();
|
||||
|
||||
if ($code = $request->validRecoveryCode()) {
|
||||
if ($user !== null) {
|
||||
$user->replaceRecoveryCode($code);
|
||||
}
|
||||
|
||||
event(new RecoveryCodeReplaced($user, $code));
|
||||
} elseif (! $request->hasValidCode()) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('Invalid Two Factor Authentication code'),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->guard->login($user, $request->remember());
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
if ($user !== null) {
|
||||
$user->update([
|
||||
'ip_current' => $request->ip(),
|
||||
]);
|
||||
}
|
||||
|
||||
return app(TwoFactorLoginResponse::class);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Actions\Fortify\Rules\PasswordValidationRules;
|
||||
use App\Models\Miscellaneous\WebsiteBetaCode;
|
||||
use App\Models\User;
|
||||
use App\Rules\BetaCodeRule;
|
||||
use App\Rules\GoogleRecaptchaRule;
|
||||
use App\Rules\WebsiteWordfilterRule;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use RyanChandler\LaravelCloudflareTurnstile\Rules\Turnstile;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
if ((setting('disable_registration') ?: '0') == '1') {
|
||||
throw ValidationException::withMessages([
|
||||
'registration' => __('Registration is disabled.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$ip = request()->ip();
|
||||
if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
|
||||
throw ValidationException::withMessages([
|
||||
'registration' => __('Your IP address seems to be invalid'),
|
||||
]);
|
||||
}
|
||||
|
||||
$matchingIpCount = User::query()
|
||||
->where('ip_current', '=', $ip)
|
||||
->orWhere('ip_register', '=', $ip)
|
||||
->count();
|
||||
|
||||
if ($matchingIpCount >= (int) (setting('max_accounts_per_ip') ?: '99')) {
|
||||
throw ValidationException::withMessages([
|
||||
'registration' => __('You have reached the max amount of allowed account'),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->validate($input);
|
||||
|
||||
$user = User::create([
|
||||
'username' => $input['username'],
|
||||
'mail' => $input['mail'],
|
||||
'password' => Hash::make($input['password']),
|
||||
'account_created' => time(),
|
||||
'last_login' => time(),
|
||||
'motto' => setting('start_motto') ?: 'Welcome to the hotel!',
|
||||
'look' => setting('start_look') ?: 'hr-100-61.hd-180-1.ch-210-66.lg-270-110.sh-305-62',
|
||||
'credits' => setting('start_credits') ?: 1000,
|
||||
'ip_register' => $ip,
|
||||
'ip_current' => $ip,
|
||||
'auth_ticket' => '',
|
||||
'home_room' => (int) (setting('hotel_home_room') ?: '0'),
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'referral_code' => sprintf('%s%s', $user->id, Str::random(8)),
|
||||
]);
|
||||
|
||||
if (setting('requires_beta_code')) {
|
||||
WebsiteBetaCode::where('code', '=', $input['beta_code'])->update([
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Referral
|
||||
if (isset($input['referral_code'])) {
|
||||
$referralUser = User::query()
|
||||
->where('referral_code', '=', $input['referral_code'])
|
||||
->first();
|
||||
|
||||
if ($referralUser !== null) {
|
||||
// Only process referral if users have different IPs
|
||||
if ($referralUser->ip_current != $user->ip_current && $referralUser->ip_register != $user->ip_register) {
|
||||
$referralUser->referrals()->updateOrCreate(['user_id' => $referralUser->id], [
|
||||
'referrals_total' => $referralUser->referrals != null ? $referralUser->referrals->referrals_total += 1 : 1,
|
||||
]);
|
||||
|
||||
$referralUser->userReferrals()->create([
|
||||
'referred_user_id' => $user->id,
|
||||
'referred_user_ip' => $ip,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (setting('enable_discord_webhook') === '1') {
|
||||
$this->sendDiscordWebhook($user->username, $user->ip_register, $user->mail);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function validate(array $inputs): array
|
||||
{
|
||||
$rules = [
|
||||
'username' => ['required', 'string', sprintf('regex:%s', setting('username_regex') ?: '/^[a-zA-Z0-9_.-]+$/'), 'max:25', Rule::unique('users'), new WebsiteWordfilterRule],
|
||||
'mail' => ['required', 'string', 'email', 'max:255', Rule::unique('users')],
|
||||
'password' => $this->passwordRules(),
|
||||
'beta_code' => ['sometimes', 'string', new BetaCodeRule],
|
||||
'terms' => ['required', 'accepted'],
|
||||
'g-recaptcha-response' => ['sometimes', 'string', new GoogleRecaptchaRule],
|
||||
'cf-turnstile-response' => [app(Turnstile::class)],
|
||||
];
|
||||
|
||||
$messages = [
|
||||
'g-recaptcha-response.required' => __('The Google recaptcha must be completed'),
|
||||
'g-recaptcha-response.string' => __('The google recaptcha was submitted with an invalid type'),
|
||||
];
|
||||
|
||||
return Validator::make($inputs, $rules, $messages)->validate();
|
||||
}
|
||||
|
||||
private function sendDiscordWebhook(string $username, string $ip, string $email): void
|
||||
{
|
||||
if (setting('discord_webhook_url') === '') {
|
||||
Log::error('Discord webhook url not provided', ['Please provide a discord webhook url before being able to send any webhook requests.']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$request = Http::asJson()->post(setting('discord_webhook_url'), [
|
||||
'username' => sprintf('%s Bot', setting('hotel_name')),
|
||||
'content' => "User: {$username} has just registered, with the IP: {$ip} and E-mail: {$email}",
|
||||
]);
|
||||
|
||||
// Log the error in-case webhook wasn't sent
|
||||
if (! $request->successful()) {
|
||||
Log::error('Failed to send Discord webhook notification', [
|
||||
'username' => $username,
|
||||
'ip' => $ip,
|
||||
'email' => $email,
|
||||
'response_status' => $request->status(),
|
||||
'response_body' => $request->body(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class DisableTwoFactorAuthentication extends \Laravel\Fortify\Actions\DisableTwoFactorAuthentication
|
||||
{
|
||||
public function __invoke($user): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed' => false,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Rules\GoogleRecaptchaRule;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Events\TwoFactorAuthenticationChallenged;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Laravel\Fortify\LoginRateLimiter;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use RyanChandler\LaravelCloudflareTurnstile\Rules\Turnstile;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RedirectIfTwoFactorAuthenticatable
|
||||
{
|
||||
/**
|
||||
* The guard implementation.
|
||||
*
|
||||
* @var StatefulGuard
|
||||
*/
|
||||
protected $guard;
|
||||
|
||||
/**
|
||||
* The login rate limiter instance.
|
||||
*
|
||||
* @var LoginRateLimiter
|
||||
*/
|
||||
protected $limiter;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter)
|
||||
{
|
||||
$this->guard = $guard;
|
||||
$this->limiter = $limiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, callable $next)
|
||||
{
|
||||
$user = $this->validateCredentials($request);
|
||||
|
||||
if (Fortify::confirmsTwoFactorAuthentication()) {
|
||||
if ($user instanceof User && $user->two_factor_secret &&
|
||||
! is_null($user->two_factor_confirmed_at) &&
|
||||
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user))) {
|
||||
return $this->twoFactorChallengeResponse($request, $user);
|
||||
} else {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
if ($user instanceof User && $user->two_factor_secret &&
|
||||
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user))) {
|
||||
return $this->twoFactorChallengeResponse($request, $user);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to validate the incoming credentials.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function validateCredentials(Request $request)
|
||||
{
|
||||
if (Fortify::$authenticateUsingCallback) {
|
||||
return tap(call_user_func(Fortify::$authenticateUsingCallback, $request), function ($user) use ($request) {
|
||||
if (! $user) {
|
||||
$this->fireFailedEvent($request);
|
||||
|
||||
$this->throwFailedAuthenticationException($request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$model = $this->guard->getProvider()->getModel();
|
||||
|
||||
return tap($model::where(Fortify::username(), $request->{Fortify::username()})->first(), function ($user) use ($request): void {
|
||||
// Update the users password to bcrypt, if they previously used md5
|
||||
if ($user && config('habbo.site.convert_passwords')) {
|
||||
$this->convertUserPassword($user, $request->input('password'));
|
||||
}
|
||||
|
||||
if (! $user || ! $this->guard->getProvider()->validateCredentials($user, ['password' => $request->password])) {
|
||||
$this->fireFailedEvent($request, $user);
|
||||
|
||||
$this->throwFailedAuthenticationException($request);
|
||||
}
|
||||
|
||||
$this->validate($request);
|
||||
|
||||
$user = User::select('id', 'password', 'rank')
|
||||
->where('username', '=', $request->input('username'))
|
||||
->first();
|
||||
|
||||
if (setting('maintenance_enabled') === '1' && setting('min_maintenance_login_rank') > $user->rank) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => __('Only staff can login during maintenance!'),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a failed authentication validation exception.
|
||||
*
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function throwFailedAuthenticationException(Request $request): void
|
||||
{
|
||||
$this->limiter->increment($request);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
Fortify::username() => [trans('auth.failed')],
|
||||
])->errorBag('login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the failed authentication attempt event with the given arguments.
|
||||
*/
|
||||
protected function fireFailedEvent(Request $request, ?Authenticatable $user = null): void
|
||||
{
|
||||
event(new Failed(config('fortify.guard'), $user, [
|
||||
Fortify::username() => $request->{Fortify::username()},
|
||||
'password' => $request->password,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the two factor authentication enabled response.
|
||||
*
|
||||
* @param mixed $user
|
||||
*/
|
||||
protected function twoFactorChallengeResponse(Request $request, $user): Response
|
||||
{
|
||||
$request->session()->put([
|
||||
'login.id' => $user instanceof User ? $user->getKey() : null,
|
||||
'login.remember' => $request->filled('remember'),
|
||||
]);
|
||||
|
||||
TwoFactorAuthenticationChallenged::dispatch($user);
|
||||
|
||||
return $request->wantsJson()
|
||||
? response()->json(['two_factor' => true])
|
||||
: redirect()->route('two-factor.login');
|
||||
}
|
||||
|
||||
private function convertUserPassword(User $user, string $password): void
|
||||
{
|
||||
if ($user->password == md5($password)) {
|
||||
$user->update([
|
||||
'password' => Hash::make($password),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function validate(Request $request): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if (setting('google_recaptcha_enabled')) {
|
||||
$rules['g-recaptcha-response'] = ['required', 'string', new GoogleRecaptchaRule];
|
||||
}
|
||||
|
||||
if (setting('cloudflare_turnstile_enabled')) {
|
||||
$rules['cf-turnstile-response'] = ['required', app(Turnstile::class)];
|
||||
}
|
||||
|
||||
$messages = [
|
||||
'g-recaptcha-response.required' => __('The Google reCAPTCHA must be completed'),
|
||||
'g-recaptcha-response.string' => __('The Google reCAPTCHA was submitted with an invalid type'),
|
||||
'cf-turnstile-response.required' => __('The Cloudflare Turnstile response is required'),
|
||||
];
|
||||
|
||||
return Validator::make($request->all(), $rules, $messages)->validate();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
|
||||
|
||||
class RedirectIfTwoFactorConfirmed extends RedirectIfTwoFactorAuthenticatable
|
||||
{
|
||||
// This class can use the default behavior from the parent class
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Actions\Fortify\Rules\PasswordValidationRules;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify\Rules;
|
||||
|
||||
use App\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', new Password, 'confirmed'];
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Actions\Fortify\Rules\PasswordValidationRules;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current_password:web'],
|
||||
'password' => $this->passwordRules(),
|
||||
], [
|
||||
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if ($input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\CurrencyTypes;
|
||||
use App\Models\User;
|
||||
use App\Services\RconService;
|
||||
|
||||
class SendCurrency
|
||||
{
|
||||
public function __construct(protected RconService $rcon) {}
|
||||
|
||||
public function execute(User $user, string $type, ?int $amount): bool
|
||||
{
|
||||
if (! $amount || $amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->rcon->isConnected) {
|
||||
match ($type) {
|
||||
'credits' => $this->rcon->giveCredits($user, $amount),
|
||||
'duckets' => $this->rcon->giveDuckets($user, $amount),
|
||||
'diamonds' => $this->rcon->giveDiamonds($user, $amount),
|
||||
'points' => $this->rcon->giveGotw($user, $amount),
|
||||
default => false,
|
||||
};
|
||||
} else {
|
||||
match ($type) {
|
||||
'credits' => $user->increment('credits', $amount),
|
||||
'duckets' => $user->currencies()->where('type', CurrencyTypes::Duckets)->increment('amount', $amount),
|
||||
'diamonds' => $user->currencies()->where('type', CurrencyTypes::Diamonds)->increment('amount', $amount),
|
||||
'points' => $user->currencies()->where('type', CurrencyTypes::Points)->increment('amount', $amount),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\RconService;
|
||||
|
||||
class SendFurniture
|
||||
{
|
||||
public function __construct(private readonly RconService $rcon) {}
|
||||
|
||||
public function execute(User $user, array $furniture): void
|
||||
{
|
||||
foreach ($furniture as $furni) {
|
||||
if ($this->rcon->isConnected) {
|
||||
for ($i = 0; $i < $furni['amount']; $i++) {
|
||||
$this->rcon->sendGift($user, $furni['item_id'], 'Thank you for supporting ' . setting('hotel_name'));
|
||||
}
|
||||
} else {
|
||||
for ($i = 0; $i < $furni['amount']; $i++) {
|
||||
$user->items()->create([
|
||||
'item_id' => $furni['item_id'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserActions
|
||||
{
|
||||
public function updateUsername(User $user, string $username): void
|
||||
{
|
||||
$user->update([
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateEmail(User $user, string $email): void
|
||||
{
|
||||
$user->update([
|
||||
'mail' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateMotto(User $user, string $motto): void
|
||||
{
|
||||
$user->update([
|
||||
'motto' => $motto,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateField(User $user, string $field, ?string $value): void
|
||||
{
|
||||
$user->update([
|
||||
$field => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AtomSetupCommand extends Command
|
||||
{
|
||||
protected $signature = 'atom:setup {--auto=false}';
|
||||
|
||||
protected $description = 'Takes you through a basic setup, allowing you to define general settings';
|
||||
|
||||
private function progressInfo(int $step): void
|
||||
{
|
||||
$this->info(sprintf('Step %s/13', $step));
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
Artisan::call('db:seed --class=WebsiteSettingsSeeder');
|
||||
|
||||
if ($this->option('auto') === 'false') {
|
||||
$step = 1;
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$hotelName = $this->ask('Enter your hotel name');
|
||||
WebsiteSetting::where('key', '=', 'hotel_name')->update([
|
||||
'value' => empty($hotelName) ? 'Hotel' : $hotelName,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$colorMode = $this->choice('Enter your preferred CMS color mode', ['light', 'dark'], 0);
|
||||
WebsiteSetting::where('key', '=', 'cms_color_mode')->update([
|
||||
'value' => $colorMode,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startCredits = $this->ask('Enter the amount of credits new users should start with: (default is 5000)');
|
||||
WebsiteSetting::where('key', '=', 'start_credits')->update([
|
||||
'value' => empty($startCredits) ? '5000' : $startCredits,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startDuckets = $this->ask('Enter the amount of credits new users should start with: (default is 5000)');
|
||||
WebsiteSetting::where('key', '=', 'start_duckets')->update([
|
||||
'value' => empty($startDuckets) ? '5000' : $startDuckets,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startDiamonds = $this->ask('Enter the amount of diamonds new users should start with: (default is 100)');
|
||||
WebsiteSetting::where('key', '=', 'start_diamonds')->update([
|
||||
'value' => empty($startDiamonds) ? '100' : $startDiamonds,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startPoints = $this->ask('Enter the amount of points new users should start with (default is 0)');
|
||||
WebsiteSetting::where('key', '=', 'start_points')->update([
|
||||
'value' => empty($startPoints) ? '0' : $startPoints,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$maxAccountsPerIP = $this->ask('Enter the amount of accounts a user can register per IP address (default is 2)');
|
||||
WebsiteSetting::where('key', '=', 'max_accounts_per_ip')->update([
|
||||
'value' => empty($maxAccountsPerIP) ? '2' : $maxAccountsPerIP,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$recaptchaEnabled = $this->choice('Google ReCaptcha enabled: (Do not forget to add your keys to your .env file in-case you set this to 1)', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'google_recaptcha_enabled')->update([
|
||||
'value' => $recaptchaEnabled,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$wordfilterEnabled = $this->choice('CMS wordfilter enabled', ['0', '1'], 1);
|
||||
WebsiteSetting::where('key', '=', 'website_wordfilter_enabled')->update([
|
||||
'value' => $wordfilterEnabled,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$requiredBetaCode = $this->choice('Requires beta code to register', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'requires_beta_code')->update([
|
||||
'value' => $requiredBetaCode,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$registrationDisabled = $this->choice('Disable registration (Can be re-enabled later inside website_settings table if set to 1)', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'disable_registration')->update([
|
||||
'value' => $registrationDisabled,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$giveHC = $this->choice('Give all new users HC automatically', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'give_hc_on_register')->update([
|
||||
'value' => $giveHC,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
|
||||
$maxCommentArticles = $this->ask('Enter the amount of comments each user can post per article (default is 2)');
|
||||
WebsiteSetting::where('key', '=', 'max_comment_per_article')->update([
|
||||
'value' => empty($maxCommentArticles) ? '2' : $maxCommentArticles,
|
||||
]);
|
||||
}
|
||||
|
||||
$seeders = [
|
||||
'WebsiteLanguageSeeder',
|
||||
'WebsiteArticleSeeder',
|
||||
'WebsitePermissionSeeder',
|
||||
'WebsiteWordfilterSeeder',
|
||||
'WebsiteTeamSeeder',
|
||||
'WebsiteRuleCategorySeeder',
|
||||
'WebsiteRuleSeeder',
|
||||
];
|
||||
|
||||
foreach ($seeders as $seeder) {
|
||||
Artisan::call(sprintf('db:seed --class=%s', $seeder));
|
||||
}
|
||||
|
||||
$this->info('The setup was successful!');
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class BuildTheme extends Command
|
||||
{
|
||||
protected $signature = 'build:theme';
|
||||
|
||||
protected $description = 'Build a selected theme assets';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$themes = $this->getAvailableThemes();
|
||||
|
||||
if ($themes->isEmpty()) {
|
||||
$this->error('No themes found in resources/themes/');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$selectedTheme = $this->choice(
|
||||
'Which theme would you like to build?',
|
||||
$themes->toArray(),
|
||||
0,
|
||||
);
|
||||
|
||||
$this->info("Building {$selectedTheme} theme...");
|
||||
|
||||
if (is_array($selectedTheme)) {
|
||||
$selectedTheme = $selectedTheme[0] ?? '';
|
||||
}
|
||||
|
||||
$this->runBuildCommand((string) $selectedTheme);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getAvailableThemes(): \Illuminate\Support\Collection
|
||||
{
|
||||
$themesPath = resource_path('themes');
|
||||
|
||||
if (! File::exists($themesPath)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return collect(File::directories($themesPath))
|
||||
->map(fn ($path) => basename((string) $path))
|
||||
->sort();
|
||||
}
|
||||
|
||||
private function runBuildCommand(string $theme): void
|
||||
{
|
||||
$command = escapeshellcmd("npm run build:{$theme}");
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
|
||||
exec($command, $output, $returnCode);
|
||||
|
||||
foreach ($output as $line) {
|
||||
$this->line($line);
|
||||
}
|
||||
|
||||
if ($returnCode === 0) {
|
||||
$this->info("Theme {$theme} built successfully!");
|
||||
} else {
|
||||
$this->error("Failed to build theme {$theme}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebsiteAd;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ImportAdsData extends Command
|
||||
{
|
||||
protected $signature = 'import:ads-data';
|
||||
|
||||
protected $description = 'Import ads data from the filesystem';
|
||||
|
||||
private const CHUNK_SIZE = 100;
|
||||
|
||||
private const ALLOWED_EXTENSIONS = ['jpeg', 'jpg', 'png', 'gif'];
|
||||
|
||||
public function handle(SettingsService $settingsService): void
|
||||
{
|
||||
$adsPath = $settingsService->getOrDefault('ads_path_filesystem');
|
||||
|
||||
if (! $this->validatePath($adsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = $this->getImageFiles($adsPath);
|
||||
|
||||
if (empty($files)) {
|
||||
$this->warn('No valid image files found in the ads directory.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processFiles($files);
|
||||
|
||||
$this->info('Ads data import completed successfully.');
|
||||
}
|
||||
|
||||
private function validatePath(?string $adsPath): bool
|
||||
{
|
||||
if (empty($adsPath)) {
|
||||
$this->error('Ads path is not configured in website_settings.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_dir($adsPath)) {
|
||||
$this->error("The ads path '{$adsPath}' does not exist in the filesystem.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getImageFiles(string $adsPath): array
|
||||
{
|
||||
return array_filter(scandir($adsPath), function ($file) use ($adsPath) {
|
||||
$filePath = $adsPath . DIRECTORY_SEPARATOR . $file;
|
||||
|
||||
return is_file($filePath) &&
|
||||
in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), self::ALLOWED_EXTENSIONS);
|
||||
});
|
||||
}
|
||||
|
||||
private function processFiles(array $files): void
|
||||
{
|
||||
// Get existing images to avoid duplicates
|
||||
$existingImages = WebsiteAd::pluck('image')->toArray();
|
||||
|
||||
$newFiles = Collection::make($files)
|
||||
->filter(fn ($file) => ! in_array($file, $existingImages))
|
||||
->map(fn ($file) => ['image' => $file])
|
||||
->values();
|
||||
|
||||
$skippedCount = count($files) - $newFiles->count();
|
||||
if ($skippedCount > 0) {
|
||||
$this->warn("Skipped {$skippedCount} existing files.");
|
||||
}
|
||||
|
||||
$newFiles->chunk(self::CHUNK_SIZE)->each(function ($chunk) {
|
||||
WebsiteAd::insert($chunk->toArray());
|
||||
$this->info('Processed ' . $chunk->count() . ' files.');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebsiteBadge;
|
||||
use App\Services\SettingsService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ImportBadgeData extends Command
|
||||
{
|
||||
protected $signature = 'import:badge-data';
|
||||
|
||||
protected $description = 'Import badge data from JSON file';
|
||||
|
||||
private const CHUNK_SIZE = 100;
|
||||
|
||||
private const BADGE_PREFIX = 'badge_desc_';
|
||||
|
||||
public function __construct(
|
||||
private readonly SettingsService $settingsService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$jsonPath = $this->settingsService->getOrDefault('nitro_external_texts_file');
|
||||
|
||||
if (! $this->validateJsonFile($jsonPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->processBadgeData($jsonPath);
|
||||
$this->info('Badge data imported successfully.');
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to import badge data: ' . $e->getMessage());
|
||||
$this->error('Failed to import badge data. Check the logs for details.');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateJsonFile(?string $jsonPath): bool
|
||||
{
|
||||
if (empty($jsonPath)) {
|
||||
$this->error('The JSON file path is not configured in the website settings.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! file_exists($jsonPath)) {
|
||||
$this->error('The JSON file does not exist at the specified path: ' . $jsonPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function processBadgeData(string $jsonPath): void
|
||||
{
|
||||
$jsonData = File::json($jsonPath);
|
||||
|
||||
// Extract badge names and descriptions
|
||||
$badgeNames = Collection::make($jsonData)
|
||||
->filter(fn ($value, $key) => str_starts_with((string) $key, 'badge_name_'))
|
||||
->mapWithKeys(fn ($value, $key) => [str_replace('badge_name_', '', $key) => $value]);
|
||||
|
||||
$badgeDescriptions = Collection::make($jsonData)
|
||||
->filter(fn ($value, $key) => str_starts_with((string) $key, self::BADGE_PREFIX))
|
||||
->mapWithKeys(fn ($value, $key) => [str_replace(self::BADGE_PREFIX, '', $key) => $value]);
|
||||
|
||||
// Combine badge names and descriptions
|
||||
$badgeData = $badgeNames->map(fn ($name, $key) => [
|
||||
'badge_key' => $key, // Use only the badge name (e.g., 14X12, 14XR1)
|
||||
'badge_name' => $name,
|
||||
'badge_description' => $badgeDescriptions->get($key, 'No description available'),
|
||||
])->values();
|
||||
|
||||
// Upsert the combined data in chunks
|
||||
$badgeData->chunk(self::CHUNK_SIZE)->each(function ($chunk) {
|
||||
WebsiteBadge::upsert(
|
||||
$chunk->toArray(),
|
||||
['badge_key'],
|
||||
['badge_name', 'badge_description'],
|
||||
);
|
||||
|
||||
$this->info('Processed ' . $chunk->count() . ' badges.');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum AchievementCategory: string
|
||||
{
|
||||
case Identity = 'identity';
|
||||
case Explore = 'explore';
|
||||
case Music = 'music';
|
||||
case Social = 'social';
|
||||
case Games = 'games';
|
||||
case RoomBuilder = 'room_builder';
|
||||
case Pets = 'pets';
|
||||
case Tools = 'tools';
|
||||
case Events = 'events';
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function toInput(): array
|
||||
{
|
||||
$allCurrencies = self::cases();
|
||||
|
||||
return array_combine(
|
||||
array_column($allCurrencies, 'value'),
|
||||
array_column($allCurrencies, 'name'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum CurrencyTypes: int
|
||||
{
|
||||
case Credits = -1;
|
||||
case Duckets = 0;
|
||||
case Diamonds = 5;
|
||||
case Points = 101;
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function fromCurrencyName(string $currencyName): ?self
|
||||
{
|
||||
$currencyName = strtolower($currencyName);
|
||||
|
||||
$currency = match ($currencyName) {
|
||||
'credits' => self::Credits,
|
||||
'duckets' => self::Duckets,
|
||||
'diamonds' => self::Diamonds,
|
||||
'points' => self::Points,
|
||||
default => null,
|
||||
};
|
||||
|
||||
return $currency;
|
||||
}
|
||||
|
||||
public function getImage(): string
|
||||
{
|
||||
return match ($this->value) {
|
||||
CurrencyTypes::Credits->value => asset('assets/images/currencies/credits.gif'),
|
||||
CurrencyTypes::Duckets->value => asset('assets/images/currencies/duckets.png'),
|
||||
CurrencyTypes::Diamonds->value => asset('assets/images/currencies/diamonds.png'),
|
||||
CurrencyTypes::Points->value => asset('assets/images/currencies/points.png'),
|
||||
};
|
||||
}
|
||||
|
||||
public static function toInput(): array
|
||||
{
|
||||
$allCurrencies = self::cases();
|
||||
|
||||
return array_combine(
|
||||
array_column($allCurrencies, 'value'),
|
||||
array_column($allCurrencies, 'name'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum NotificationType: string
|
||||
{
|
||||
case ArticlePosted = 'article_posted';
|
||||
case HousekeepingCustomMessage = 'housekeeping_custom_message';
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of exception types with their corresponding custom log levels.
|
||||
*
|
||||
* @var array<class-string<Throwable>, \Psr\Log\LogLevel::*>
|
||||
*/
|
||||
protected $levels = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array<int, class-string<Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class MigrationFailedException extends Exception {}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class RconConnectionException extends Exception {}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Filters;
|
||||
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DateRangeFilter extends Filter
|
||||
{
|
||||
public static function make(?string $name = null): static
|
||||
{
|
||||
return parent::make($name)
|
||||
->schema([
|
||||
DatePicker::make("{$name}_from"),
|
||||
DatePicker::make("{$name}_until"),
|
||||
])
|
||||
->query(function (Builder $query, array $data) use (&$name): Builder {
|
||||
return $query
|
||||
->when(
|
||||
$data["{$name}_from"],
|
||||
fn (Builder $query, ?string $date) => $query->whereDate($name !== null ? $name : '', '>=', $date),
|
||||
)
|
||||
->when(
|
||||
$data["{$name}_until"],
|
||||
fn (Builder $query, ?string $date) => $query->whereDate($name !== null ? $name : '', '<=', $date),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Services\Parsers\ExternalTextsParser;
|
||||
use Filament\Actions\Action as PageAction;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @property-read \Filament\Schemas\Components\Form $form
|
||||
*/
|
||||
class BadgePage extends Page
|
||||
{
|
||||
use InteractsWithForms, TranslatableResource;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Hotel';
|
||||
|
||||
protected string $view = 'filament.pages.badge-page';
|
||||
|
||||
protected static string $translateIdentifier = 'badge-resource';
|
||||
|
||||
public bool $badgeWasPreviouslyCreated = false;
|
||||
|
||||
public array $data = [];
|
||||
|
||||
public static string $roleName = 'badge_page';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view::admin::' . static::$roleName);
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
$translated = __(
|
||||
sprintf('filament::resources.resources.%s.navigation_label', static::$translateIdentifier),
|
||||
);
|
||||
|
||||
return is_array($translated) ? (string) ($translated[0] ?? '') : (string) $translated;
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make(__('filament::resources.tabs.Main'))
|
||||
->schema([
|
||||
TextInput::make('code')
|
||||
->label(__('filament::resources.inputs.badge_code'))
|
||||
->helperText(__('filament::resources.helpers.badge_code_helper'))
|
||||
->afterStateUpdated(function (?string $state, Set $set) {
|
||||
$set('code', strtoupper((string) $state));
|
||||
})
|
||||
->suffixAction(fn (): PageAction => PageAction::make('search')->icon('heroicon-o-magnifying-glass')->action(fn () => $this->searchBadgesByCode()),
|
||||
),
|
||||
|
||||
TextInput::make('image')
|
||||
->label(__('filament::resources.inputs.badge_image'))
|
||||
->placeholder('...')
|
||||
->autocomplete()
|
||||
->visible(fn (Get $get) => isset($this->data['image']))
|
||||
->prefixAction(
|
||||
fn (?string $state): PageAction => PageAction::make('visit')
|
||||
->icon('heroicon-s-arrow-top-right-on-square')
|
||||
->tooltip(__('filament::resources.common.Open link'))
|
||||
->url($state)
|
||||
->visible(fn () => ! empty($state))
|
||||
->openUrlInNewTab(),
|
||||
),
|
||||
]),
|
||||
|
||||
Section::make('Nitro Texts')
|
||||
->collapsible()
|
||||
->visible(fn () => isset($this->data['nitro']) && ! empty($this->data['nitro']))
|
||||
->schema([
|
||||
TextInput::make('nitro.title')
|
||||
->label(__('filament::resources.inputs.badge_title'))
|
||||
->placeholder('...')
|
||||
->visible(fn () => isset($this->data['nitro']['title'])),
|
||||
|
||||
TextInput::make('nitro.description')
|
||||
->label(__('filament::resources.inputs.badge_description'))
|
||||
->placeholder('...')
|
||||
->visible(fn () => isset($this->data['nitro']['description'])),
|
||||
]),
|
||||
|
||||
Section::make('Flash Texts')
|
||||
->collapsible()
|
||||
->visible(fn () => isset($this->data['flash']) && ! empty($this->data['flash']))
|
||||
->schema([
|
||||
TextInput::make('flash.title')
|
||||
->label(__('filament::resources.inputs.badge_title'))
|
||||
->placeholder('...')
|
||||
->visible(fn () => isset($this->data['flash']['title'])),
|
||||
|
||||
TextInput::make('flash.description')
|
||||
->label(__('filament::resources.inputs.badge_description'))
|
||||
->placeholder('...')
|
||||
->visible(fn () => isset($this->data['flash']['description'])),
|
||||
]),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
private function searchBadgesByCode(): void
|
||||
{
|
||||
$badgeCode = $this->form->getState()['code'] ?? null;
|
||||
|
||||
if (empty($badgeCode)) {
|
||||
Notification::make()
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->iconColor('danger')
|
||||
->color('danger')
|
||||
->title(__('filament::resources.notifications.badge_code_required'))
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$badgeData = app(ExternalTextsParser::class)->getBadgeData($badgeCode);
|
||||
$this->badgeWasPreviouslyCreated = is_array($badgeData['nitro'] ?? null) || is_array($badgeData['flash'] ?? null);
|
||||
|
||||
if ($this->badgeWasPreviouslyCreated) {
|
||||
Notification::make()
|
||||
->icon('heroicon-o-check-circle')
|
||||
->iconColor('success')
|
||||
->color('success')
|
||||
->title(__('filament::resources.notifications.badge_found'))
|
||||
->send();
|
||||
|
||||
$this->data = [
|
||||
'code' => $badgeCode,
|
||||
...$this->getDefaultDataBehavior(
|
||||
$badgeData['image'] ?? null,
|
||||
$badgeData['nitro']['title'] ?? null,
|
||||
$badgeData['nitro']['description'] ?? null,
|
||||
$badgeData['flash']['title'] ?? null,
|
||||
$badgeData['flash']['description'] ?? null,
|
||||
),
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->color('success')
|
||||
->icon('heroicon-o-check-circle')
|
||||
->iconColor('success')
|
||||
->title(__('filament::resources.notifications.create_badge'))
|
||||
->send();
|
||||
|
||||
$this->data = [
|
||||
'code' => $badgeCode,
|
||||
...$this->getDefaultDataBehavior(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getDefaultDataBehavior(
|
||||
?string $badgeImageUrl = null,
|
||||
?string $nitroTitle = null,
|
||||
?string $nitroDesc = null,
|
||||
?string $flashTitle = null,
|
||||
?string $flashDesc = null,
|
||||
): array {
|
||||
return [
|
||||
'image' => $badgeImageUrl ?? '',
|
||||
'nitro' => [
|
||||
'title' => $nitroTitle ?? '',
|
||||
'description' => $nitroDesc ?? '',
|
||||
],
|
||||
'flash' => [
|
||||
'title' => $flashTitle ?? '',
|
||||
'description' => $flashDesc ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$nitroEnabled = config('hotel.client.nitro.enabled');
|
||||
$flashEnabled = config('hotel.client.flash.enabled');
|
||||
|
||||
// image and code fields are required when creating a new badge
|
||||
if (! $this->badgeWasPreviouslyCreated && (empty($this->data['image']) || empty($this->data['code']))) {
|
||||
$notificationTitle = empty($this->data['image']) ?
|
||||
__('filament::resources.notifications.badge_image_required') :
|
||||
__('filament::resources.notifications.badge_code_required');
|
||||
|
||||
Notification::make()
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->iconColor('danger')
|
||||
->color('danger')
|
||||
->title($notificationTitle)
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$externalTextsParser = app(ExternalTextsParser::class);
|
||||
|
||||
if ((empty($this->data['nitro']) && $nitroEnabled) || (empty($this->data['flash']) && $flashEnabled)) {
|
||||
Notification::make()
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->iconColor('danger')
|
||||
->color('danger')
|
||||
->title(__('filament::resources.notifications.badge_texts_required'))
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->uploadBadgeImage($externalTextsParser);
|
||||
|
||||
if (! empty($this->data['nitro']) && $nitroEnabled) {
|
||||
$externalTextsParser->updateNitroBadgeTexts($this->data['code'], ...$this->data['nitro']);
|
||||
}
|
||||
if (! empty($this->data['flash']) && $flashEnabled) {
|
||||
$externalTextsParser->updateFlashBadgeTexts($this->data['code'], ...$this->data['flash']);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
Log::channel('badge')->error('[ORION BADGE RESOURCE] - ERROR: ' . $exception->getMessage());
|
||||
|
||||
Notification::make()
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->iconColor('danger')
|
||||
->color('danger')
|
||||
->title(__('filament::resources.notifications.badge_update_failed'))
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->data['image'] = $externalTextsParser->getBadgeImageUrl($this->data['code']);
|
||||
$this->badgeWasPreviouslyCreated = true;
|
||||
|
||||
Notification::make()
|
||||
->icon('heroicon-o-check-circle')
|
||||
->iconColor('success')
|
||||
->color('success')
|
||||
->title(__('filament::resources.notifications.badge_updated'))
|
||||
->send();
|
||||
}
|
||||
|
||||
protected function uploadBadgeImage(ExternalTextsParser $parser): void
|
||||
{
|
||||
if (empty($this->data['image']) || ! filter_var($this->data['image'], FILTER_VALIDATE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->data['image'] == $parser->getBadgeImageUrl($this->data['code'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image = Http::get($this->data['image']);
|
||||
|
||||
if (! $image->successful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contentType = $image->header('content-type');
|
||||
|
||||
$gdImage = match ($contentType) {
|
||||
'image/png' => imagecreatefrompng($this->data['image']),
|
||||
'image/gif' => imagecreatefromgif($this->data['image']),
|
||||
'image/jpeg' => imagecreatefromjpeg($this->data['image']),
|
||||
default => false
|
||||
};
|
||||
|
||||
if ($gdImage === false) {
|
||||
Notification::make()
|
||||
->icon('heroicon-o-exclamation-triangle')
|
||||
->iconColor('danger')
|
||||
->color('danger')
|
||||
->title(__('filament::resources.notifications.badge_image_upload_failed'))
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$uploadPath = public_path(sprintf('%s%s%s.gif',
|
||||
rtrim((string) config('hotel.client.flash.relative_files_path'), '\//'),
|
||||
'/c_images/album1584/',
|
||||
$this->data['code'],
|
||||
));
|
||||
|
||||
imagegif($gdImage, $uploadPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<\Filament\Actions\Action|ActionGroup>
|
||||
*/
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
PageAction::make('save')
|
||||
->label(__('filament::resources.common.Update'))
|
||||
->action(fn () => $this->create())
|
||||
->color('primary')
|
||||
->visible(fn () => isset($this->data['code']) && $this->badgeWasPreviouslyCreated),
|
||||
|
||||
PageAction::make('create')
|
||||
->label(__('filament::resources.common.Create'))
|
||||
->action(fn () => $this->create())
|
||||
->color('success')
|
||||
->visible(fn () => isset($this->data['code']) && ! $this->badgeWasPreviouslyCreated),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use Filament\Pages\Dashboard as FilamentDashboard;
|
||||
|
||||
class Dashboard extends FilamentDashboard
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Dashboard';
|
||||
|
||||
protected static ?string $navigationLabel = 'Homepage';
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-home';
|
||||
|
||||
public static string $translateIdentifier = 'dashboard';
|
||||
|
||||
public static string $roleName = 'dashboard';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view::admin::' . static::$roleName);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class Login extends \Filament\Auth\Pages\Login
|
||||
{
|
||||
public string $username = '';
|
||||
|
||||
public function authenticate(): ?LoginResponse
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(5);
|
||||
} catch (TooManyRequestsException $exception) {
|
||||
Notification::make()
|
||||
->title(__('filament-panels::pages/auth/login.notifications.throttled.title', [
|
||||
'seconds' => $exception->secondsUntilAvailable,
|
||||
'minutes' => ceil($exception->secondsUntilAvailable / 60),
|
||||
]))
|
||||
->body(array_key_exists('body', (array) __('filament-panels::pages/auth/login.notifications.throttled') ?: []) ? __('filament-panels::pages/auth/login.notifications.throttled.body', [
|
||||
'seconds' => $exception->secondsUntilAvailable,
|
||||
'minutes' => ceil($exception->secondsUntilAvailable / 60),
|
||||
]) : null)
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $this->form->getState();
|
||||
|
||||
if (! Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'] ?? false)) {
|
||||
$this->throwFailureValidationException();
|
||||
}
|
||||
|
||||
$user = Filament::auth()->user();
|
||||
|
||||
if (
|
||||
($user instanceof FilamentUser) &&
|
||||
(! $user->canAccessPanel(Filament::getCurrentOrDefaultPanel()))
|
||||
) {
|
||||
Filament::auth()->logout();
|
||||
|
||||
$this->throwFailureValidationException();
|
||||
}
|
||||
|
||||
session()->regenerate();
|
||||
|
||||
return app(LoginResponse::class);
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
{
|
||||
throw ValidationException::withMessages([
|
||||
'data.username' => __('filament-panels::pages/auth/login.messages.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('username')
|
||||
->label(__('filament::login.fields.username.label'))
|
||||
->required()
|
||||
->autocomplete(),
|
||||
TextInput::make('password')
|
||||
->label(__('filament::login.fields.password.label'))
|
||||
->password()
|
||||
->required(),
|
||||
Checkbox::make('remember')
|
||||
->label(__('filament::login.fields.remember.label')),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getEmailFormComponent(): Component
|
||||
{
|
||||
return TextInput::make('username')
|
||||
->label(__('filament::login.fields.username.label'))
|
||||
->required()
|
||||
->autocomplete()
|
||||
->autofocus()
|
||||
->extraInputAttributes(['tabindex' => 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getCredentialsFromFormData(array $data): array
|
||||
{
|
||||
return [
|
||||
'username' => $data['username'],
|
||||
'password' => $data['password'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @phpstan-type AuthUser = \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
|
||||
namespace App\Filament\Resources\Atom\Articles;
|
||||
|
||||
use App\Filament\Resources\Atom\Articles\Pages\CreateArticle;
|
||||
use App\Filament\Resources\Atom\Articles\Pages\EditArticle;
|
||||
use App\Filament\Resources\Atom\Articles\Pages\ListArticles;
|
||||
use App\Filament\Resources\Atom\Articles\Pages\ViewArticle;
|
||||
use App\Filament\Resources\Atom\Articles\RelationManagers\TagsRelationManager;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Articles\WebsiteArticle;
|
||||
use Exception;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ArticleResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteArticle::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-newspaper';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/articles';
|
||||
|
||||
public static string $translateIdentifier = 'articles';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components(static::getForm());
|
||||
}
|
||||
|
||||
public static function getForm(): array
|
||||
{
|
||||
return [
|
||||
Tabs::make('Main')
|
||||
->tabs([
|
||||
Tab::make(__('filament::resources.tabs.Home'))
|
||||
->icon('heroicon-o-home')
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label(__('filament::resources.inputs.title'))
|
||||
->required()
|
||||
->autocomplete()
|
||||
->maxLength(255)
|
||||
->columnSpan('full'),
|
||||
|
||||
TextInput::make('short_story')
|
||||
->label(__('filament::resources.inputs.description'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autocomplete()
|
||||
->columnSpan('full'),
|
||||
|
||||
FileUpload::make('image')
|
||||
->label(__('filament::resources.inputs.image'))
|
||||
->directory('website_news_images')
|
||||
->visibility('public'),
|
||||
|
||||
RichEditor::make('full_story')
|
||||
->label(__('filament::resources.inputs.content'))
|
||||
->required()
|
||||
->columnSpan('full'),
|
||||
|
||||
Hidden::make('user_id')
|
||||
->default(Auth::id() ?? 0),
|
||||
]),
|
||||
|
||||
Tab::make(__('filament::resources.tabs.Configurations'))
|
||||
->icon('heroicon-o-cog')
|
||||
->schema([
|
||||
Toggle::make('is_visible')
|
||||
->label(__('filament::resources.inputs.visible'))
|
||||
->onIcon('heroicon-s-check')
|
||||
->offIcon('heroicon-s-x-mark')
|
||||
->default(true)
|
||||
->live()
|
||||
->afterStateUpdated(function (string $operation, $state, $record) {
|
||||
/** @var \App\Models\Articles\WebsiteArticle $record */
|
||||
if ($operation !== 'edit' || ! $record instanceof \App\Models\Articles\WebsiteArticle) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($state) {
|
||||
$record->restore();
|
||||
} else {
|
||||
$record->delete();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
})
|
||||
->formatStateUsing(function ($record) {
|
||||
if (is_null($record)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return is_null($record->deleted_at);
|
||||
}),
|
||||
|
||||
Toggle::make('can_comment')
|
||||
->onIcon('heroicon-s-check')
|
||||
->label(__('filament::resources.inputs.allow_comments'))
|
||||
->default(true)
|
||||
->offIcon('heroicon-s-x-mark'),
|
||||
]),
|
||||
])->columnSpanFull(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->poll('60s')
|
||||
->columns(static::getTable())
|
||||
->filters([
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DeleteBulkAction::make(),
|
||||
RestoreBulkAction::make(),
|
||||
ForceDeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getTable(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->label(__('filament::resources.columns.id')),
|
||||
|
||||
ImageColumn::make('image')
|
||||
->circular()
|
||||
->extraAttributes(['style' => 'image-rendering: pixelated'])
|
||||
->size(50)
|
||||
->label(__('filament::resources.columns.image')),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label(__('filament::resources.columns.title'))
|
||||
->searchable()
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('user.username')
|
||||
->searchable()
|
||||
->label(__('filament::resources.columns.by')),
|
||||
|
||||
ToggleColumn::make('is_visible')
|
||||
->label(__('filament::resources.columns.visible'))
|
||||
->onIcon('heroicon-s-check')
|
||||
->toggleable()
|
||||
->state(fn ($record) => is_null($record->deleted_at))
|
||||
->disabled(),
|
||||
|
||||
ToggleColumn::make('allow_comments')
|
||||
->label(__('filament::resources.columns.allow_comments'))
|
||||
->onIcon('heroicon-s-check')
|
||||
->toggleable()
|
||||
->disabled(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
TagsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListArticles::route('/'),
|
||||
'create' => CreateArticle::route('/create'),
|
||||
'view' => ViewArticle::route('/{record}'),
|
||||
'edit' => EditArticle::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGlobalSearchEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getGlobalSearchEloquentQuery()->withTrashed();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Articles\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Articles\ArticleResource;
|
||||
use App\Models\Article;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateArticle extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ArticleResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
/** @var null|Article $articleCreated */
|
||||
$articleCreated = $this->getRecord();
|
||||
|
||||
if (! $articleCreated || ! $articleCreated->visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
$articleCreated->createFollowersNotification();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Articles\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Articles\ArticleResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditArticle extends EditRecord
|
||||
{
|
||||
protected static string $resource = ArticleResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Articles\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Articles\ArticleResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListArticles extends ListRecords
|
||||
{
|
||||
protected static string $resource = ArticleResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Articles\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Articles\ArticleResource;
|
||||
use App\Models\Article;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ViewArticle extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ArticleResource::class;
|
||||
|
||||
public function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('Send Notification')
|
||||
->label(__('Send notifications'))
|
||||
->color('gray')
|
||||
->visible(fn (Article $record) => $record->user_id === Auth::id())
|
||||
->requiresConfirmation()
|
||||
->action(function (Article $record) {
|
||||
$record->createFollowersNotification();
|
||||
}),
|
||||
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Articles\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\Atom\Tags\TagResource;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use Filament\Actions\AttachAction;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DetachAction;
|
||||
use Filament\Actions\DetachBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TagsRelationManager extends RelationManager
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static string $relationship = 'tags';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static string $translateIdentifier = 'tags';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns(TagResource::getTable())
|
||||
->modifyQueryUsing(fn ($query) => $query->latest())
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()
|
||||
->schema(TagResource::getForm()),
|
||||
|
||||
AttachAction::make()->preloadRecordSelect(),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
DetachAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DetachBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\CameraWebs;
|
||||
|
||||
use App\Filament\Resources\Atom\CameraWebs\Pages\EditCameraWeb;
|
||||
use App\Filament\Resources\Atom\CameraWebs\Pages\ListCameraWeb;
|
||||
use App\Models\Miscellaneous\CameraWeb;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CameraWebResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CameraWeb::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-photo';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'camera-web';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'photos';
|
||||
|
||||
protected static ?string $navigationLabel = 'Web Camera';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Toggle::make('visible')
|
||||
->label(__('Visible'))
|
||||
->default(true),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label(__('filament::resources.columns.id'))
|
||||
->sortable(),
|
||||
TextColumn::make('user_id')
|
||||
->label(__('filament::resources.columns.user_id')),
|
||||
TextColumn::make('room_id')
|
||||
->label(__('filament::resources.columns.room_id')),
|
||||
TextColumn::make('timestamp')
|
||||
->label(__('filament::resources.columns.created_at'))
|
||||
->dateTime(),
|
||||
ImageColumn::make('url')
|
||||
->label(__('filament::resources.columns.image'))
|
||||
->extraAttributes(['style' => 'image-rendering: pixelated'])
|
||||
->size(125),
|
||||
ToggleColumn::make('visible')
|
||||
->label(__('Visible')),
|
||||
])
|
||||
->recordActions([
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCameraWeb::route('/'),
|
||||
'edit' => EditCameraWeb::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\CameraWebs\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\CameraWebs\CameraWebResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCameraWeb extends EditRecord
|
||||
{
|
||||
protected static string $resource = CameraWebResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\CameraWebs\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\CameraWebs\CameraWebResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCameraWeb extends ListRecords
|
||||
{
|
||||
protected static string $resource = CameraWebResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\CmsSettings;
|
||||
|
||||
use App\Filament\Resources\Atom\CmsSettings\Pages\ManageCmsSettings;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CmsSettingResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteSetting::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-cpu-chip';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/cms-settings';
|
||||
|
||||
public static string $translateIdentifier = 'cms-settings';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->label(__('filament::resources.inputs.key'))
|
||||
->maxLength(50)
|
||||
->autocomplete()
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
|
||||
TextInput::make('value')
|
||||
->label(__('filament::resources.inputs.value'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autocomplete(),
|
||||
|
||||
TextInput::make('comment')
|
||||
->label(__('filament::resources.inputs.comment'))
|
||||
->nullable()
|
||||
->maxLength(255)
|
||||
->autocomplete()
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns([
|
||||
'sm' => 2,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('key')
|
||||
->label(__('filament::resources.columns.key'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('value')
|
||||
->label(__('filament::resources.columns.value'))
|
||||
->searchable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('comment')
|
||||
->label(__('filament::resources.columns.comment'))
|
||||
->toggleable()
|
||||
->searchable()
|
||||
->tooltip(function (TextColumn $column): ?string {
|
||||
$state = $column->getState();
|
||||
|
||||
if (! is_string($state) || strlen($state) <= $column->getCharacterLimit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $state;
|
||||
})
|
||||
->limit(60),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
// ...
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ManageCmsSettings::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\CmsSettings\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\CmsSettings\CmsSettingResource;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ManageCmsSettings extends ManageRecords
|
||||
{
|
||||
protected static string $resource = CmsSettingResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('reload_cache')
|
||||
->label('Reload Cache')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Reload Settings Cache')
|
||||
->modalDescription('This will clear and reload the website settings cache. The cache will be automatically rebuilt on the next request.')
|
||||
->modalSubmitActionLabel('Reload Cache')
|
||||
->action(function () {
|
||||
Cache::forget('website_settings');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Cache Cleared')
|
||||
->body('Settings cache has been cleared successfully.')
|
||||
->send();
|
||||
}),
|
||||
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableRecordsPerPageSelectOptions(): array
|
||||
{
|
||||
return [25, 50, 100];
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages\CreateHelpQuestionCategory;
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages\EditHelpQuestionCategory;
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages\ListHelpQuestionCategories;
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages\ViewHelpQuestionCategory;
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource\RelationManagers\QuestionsRelationManager;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Help\WebsiteHelpCenterCategory;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class HelpQuestionCategoryResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteHelpCenterCategory::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-folder';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Help Center';
|
||||
|
||||
protected static ?string $slug = 'help/categories';
|
||||
|
||||
public static string $translateIdentifier = 'help-categories';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components(static::getForm());
|
||||
}
|
||||
|
||||
public static function getForm(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->label(__('Name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('description')
|
||||
->label(__('Description'))
|
||||
->maxLength(255),
|
||||
];
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns(static::getTable());
|
||||
}
|
||||
|
||||
public static function getTable(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->label(__('ID'))
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label(__('Name'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('description')
|
||||
->label(__('Description')),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
QuestionsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListHelpQuestionCategories::route('/'),
|
||||
'create' => CreateHelpQuestionCategory::route('/create'),
|
||||
'view' => ViewHelpQuestionCategory::route('/{record}'),
|
||||
'edit' => EditHelpQuestionCategory::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateHelpQuestionCategory extends CreateRecord
|
||||
{
|
||||
protected static string $resource = HelpQuestionCategoryResource::class;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditHelpQuestionCategory extends EditRecord
|
||||
{
|
||||
protected static string $resource = HelpQuestionCategoryResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListHelpQuestionCategories extends ListRecords
|
||||
{
|
||||
protected static string $resource = HelpQuestionCategoryResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableReorderColumn(): ?string
|
||||
{
|
||||
return 'order';
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionCategoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewHelpQuestionCategory extends ViewRecord
|
||||
{
|
||||
protected static string $resource = HelpQuestionCategoryResource::class;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionCategoryResource\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use Filament\Actions\AttachAction;
|
||||
use Filament\Actions\DetachAction;
|
||||
use Filament\Actions\DetachBulkAction;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class QuestionsRelationManager extends RelationManager
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static string $relationship = 'questions';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
public static string $translateIdentifier = 'help-questions';
|
||||
|
||||
protected static ?string $inverseRelationship = 'categories';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components(HelpQuestionResource::getForm(true));
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table->columns(HelpQuestionResource::getTable())
|
||||
->modifyQueryUsing(fn ($query) => $query->latest())
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
AttachAction::make(),
|
||||
])
|
||||
->recordActions([
|
||||
DetachAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DetachBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource\Pages\CreateHelpQuestion;
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource\Pages\EditHelpQuestion;
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource\Pages\ListHelpQuestions;
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource\Pages\ViewHelpQuestion;
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource\RelationManagers\CategoriesRelationManager;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Help\WebsiteHelpCenterTicket;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class HelpQuestionResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteHelpCenterTicket::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-question-mark-circle';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Help Center';
|
||||
|
||||
protected static ?string $slug = 'help/questions';
|
||||
|
||||
public static string $translateIdentifier = 'help-questions';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components(static::getForm());
|
||||
}
|
||||
|
||||
public static function getForm(bool $forRelationManager = false): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('title')
|
||||
->label(__('Title'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('content')
|
||||
->label(__('Content'))
|
||||
->required(),
|
||||
|
||||
Select::make('category_id')
|
||||
->label(__('Category'))
|
||||
->relationship('category', 'name')
|
||||
->required()
|
||||
->visible(! $forRelationManager),
|
||||
|
||||
Select::make('user_id')
|
||||
->label(__('User'))
|
||||
->relationship('user', 'username')
|
||||
->required(),
|
||||
|
||||
Toggle::make('open')
|
||||
->label(__('Open'))
|
||||
->default(true),
|
||||
];
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns(static::getTable());
|
||||
}
|
||||
|
||||
public static function getTable(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->label(__('ID'))
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label(__('Title'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('user.username')
|
||||
->label(__('User'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('category.name')
|
||||
->label(__('Category')),
|
||||
|
||||
ToggleColumn::make('open')
|
||||
->label(__('Open')),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('Created'))
|
||||
->dateTime(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
CategoriesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListHelpQuestions::route('/'),
|
||||
'create' => CreateHelpQuestion::route('/create'),
|
||||
'view' => ViewHelpQuestion::route('/{record}'),
|
||||
'edit' => EditHelpQuestion::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateHelpQuestion extends CreateRecord
|
||||
{
|
||||
protected static string $resource = HelpQuestionResource::class;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditHelpQuestion extends EditRecord
|
||||
{
|
||||
protected static string $resource = HelpQuestionResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListHelpQuestions extends ListRecords
|
||||
{
|
||||
protected static string $resource = HelpQuestionResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewHelpQuestion extends ViewRecord
|
||||
{
|
||||
protected static string $resource = HelpQuestionResource::class;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HelpQuestionResource\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\Atom\HelpQuestionCategoryResource;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use Filament\Actions\AttachAction;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DetachAction;
|
||||
use Filament\Actions\DetachBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CategoriesRelationManager extends RelationManager
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static string $relationship = 'categories';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static string $translateIdentifier = 'help-question-categories';
|
||||
|
||||
protected static ?string $inverseRelationship = 'questions';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components(HelpQuestionCategoryResource::getForm());
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table->columns(HelpQuestionCategoryResource::getTable())
|
||||
->modifyQueryUsing(fn ($query) => $query->latest('id'))
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make(),
|
||||
AttachAction::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DetachAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DetachBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HousekeepingPermissions;
|
||||
|
||||
use App\Filament\Resources\Atom\HousekeepingPermissions\Pages\ListHousekeepingPermissions;
|
||||
use App\Models\WebsiteHousekeepingPermission;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class HousekeepingPermissionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = WebsiteHousekeepingPermission::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/housekeeping-permissions';
|
||||
|
||||
protected static ?string $navigationLabel = 'Housekeeping permissions';
|
||||
|
||||
public static string $translateIdentifier = 'housekeeping-permissions';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('permission')
|
||||
->label(__('filament::resources.inputs.permission'))
|
||||
->maxLength(50)
|
||||
->autocomplete()
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
|
||||
TextInput::make('min_rank')
|
||||
->label(__('filament::resources.inputs.min_rank'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autocomplete(),
|
||||
|
||||
TextInput::make('description')
|
||||
->label(__('filament::resources.inputs.description'))
|
||||
->nullable()
|
||||
->maxLength(255)
|
||||
->autocomplete()
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns([
|
||||
'sm' => 2,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'asc')
|
||||
->columns([
|
||||
TextColumn::make('permission')
|
||||
->label(__('filament::resources.columns.permission'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('min_rank')
|
||||
->label(__('filament::resources.columns.min_rank'))
|
||||
->searchable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('description')
|
||||
->label(__('filament::resources.columns.description'))
|
||||
->toggleable()
|
||||
->searchable()
|
||||
->tooltip(function (TextColumn $column): ?string {
|
||||
$state = $column->getState();
|
||||
|
||||
if (! is_string($state) || strlen($state) <= $column->getCharacterLimit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $state;
|
||||
})
|
||||
->limit(60),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
//
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListHousekeepingPermissions::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\HousekeepingPermissions\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\HousekeepingPermissions\HousekeepingPermissionResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListHousekeepingPermissions extends ListRecords
|
||||
{
|
||||
protected static string $resource = HousekeepingPermissionResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom;
|
||||
|
||||
use App\Filament\Resources\Atom\NavigationResource\Pages\CreateNavigation;
|
||||
use App\Filament\Resources\Atom\NavigationResource\Pages\EditNavigation;
|
||||
use App\Filament\Resources\Atom\NavigationResource\Pages\ListNavigations;
|
||||
use App\Filament\Resources\Atom\NavigationResource\RelationManagers\SubNavigationsRelationManager;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\WebsiteNavigation;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class NavigationResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteNavigation::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bars-3';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/navigations';
|
||||
|
||||
public static string $translateIdentifier = 'navigations';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components(static::getForm());
|
||||
}
|
||||
|
||||
public static function getForm(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->label(__('Name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('url')
|
||||
->label(__('URL'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('icon')
|
||||
->label(__('Icon'))
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('order')
|
||||
->label(__('Order'))
|
||||
->numeric()
|
||||
->default(0),
|
||||
|
||||
Select::make('parent_id')
|
||||
->label(__('Parent Navigation'))
|
||||
->relationship('parent', 'name')
|
||||
->nullable(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns(static::getTable());
|
||||
}
|
||||
|
||||
public static function getTable(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->label(__('ID'))
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label(__('Name'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('url')
|
||||
->label(__('URL')),
|
||||
|
||||
TextColumn::make('order')
|
||||
->label(__('Order'))
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('parent.name')
|
||||
->label(__('Parent')),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
SubNavigationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListNavigations::route('/'),
|
||||
'create' => CreateNavigation::route('/create'),
|
||||
'edit' => EditNavigation::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\NavigationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\NavigationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateNavigation extends CreateRecord
|
||||
{
|
||||
protected static string $resource = NavigationResource::class;
|
||||
}
|
||||