🆙 Delete not used things 🆙

This commit is contained in:
Remco
2026-03-01 20:47:30 +01:00
parent 60ebabf974
commit 1cbf069e4a
4021 changed files with 0 additions and 193653 deletions
Binary file not shown.
-221
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

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": ""
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
-18
View File
@@ -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
-130
View File
@@ -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=
-41
View File
@@ -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
-11
View File
@@ -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
-39
View File
@@ -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.
-260
View File
@@ -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>
-4
View File
@@ -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
-21
View File
@@ -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.
-196
View File
@@ -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**
[![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/pP6HyZedAj)
[![Laravel](https://img.shields.io/badge/Laravel-12.x-FF2D20?style=flat&logo=laravel&logoColor=white)](https://laravel.com)
[![PHP](https://img.shields.io/badge/PHP-8.5-777BB4?style=flat&logo=php&logoColor=white)](https://php.net)
[![PHPStan](https://img.shields.io/badge/PHPStan-Level%209-green?style=flat)](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();
}
}
-39
View File
@@ -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;
}
}
-28
View File
@@ -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'],
]);
}
}
}
}
}
-36
View File
@@ -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.');
});
}
}
-27
View File
@@ -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'),
);
}
}
-51
View File
@@ -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';
}
-48
View File
@@ -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),
);
});
}
}
-324
View File
@@ -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);
}
}
-106
View File
@@ -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;
}

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