You've already forked Epicnabbo-Catalogus-Updated-Daily
🆙 Add fixed cms 🆙
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
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
|
||||
@@ -0,0 +1,130 @@
|
||||
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=
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
* 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
|
||||
@@ -0,0 +1,39 @@
|
||||
/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
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,260 @@
|
||||
# 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>
|
||||
@@ -0,0 +1,4 @@
|
||||
# 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
|
||||
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
@@ -0,0 +1,299 @@
|
||||
<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
|
||||
|
||||
[](https://discord.gg/pP6HyZedAj)
|
||||
[](https://laravel.com)
|
||||
[](https://php.net)
|
||||
|
||||
[Live Demos](#-live-preview) • [Installation](#-installation) • [Documentation](https://github.com/atom-retros/atomcms/wiki) • [Contributing](#-contributing)
|
||||
|
||||
</div>
|
||||
|
||||
>[!NOTE]
|
||||
>Disclaimer: Educational Use Only
|
||||
>
|
||||
> Atom CMS is provided as an educational resource for learning purposes only. The creators and contributors are not responsible for any misuse or unintended consequences arising from its use. By using Atom CMS, you agree to take full responsibility for your actions and ensure compliance with all applicable laws and regulations in your jurisdiction.
|
||||
|
||||
|
||||
## 🎯 About
|
||||
|
||||
Atom CMS is a modern, community-driven CMS designed to provide a flexible and user-friendly platform for retro hotel management. Built on Laravel 12.x with a focus on extensibility and ease of use, Atom CMS features a built-in theme system that allows you to use any CSS framework or create fully customized vanilla designs.
|
||||
|
||||
### Built With
|
||||
|
||||
- **[Laravel 12.x](https://laravel.com/docs/11.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
|
||||
|
||||
---
|
||||
|
||||
## ✨ 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
|
||||
- 🚀 **Modern Stack** - Latest PHP 8.2+ features
|
||||
|
||||
---
|
||||
|
||||
## 👀 Live Preview
|
||||
|
||||
Experience Atom CMS with our official themes:
|
||||
|
||||
- **Dusk Theme**: [https://dusk.atomcms.dev](https://dusk.atomcms.dev)
|
||||
- **Atom Theme**: [https://atom.atomcms.dev](https://atom.atomcms.dev)
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|------------|---------|
|
||||
| PHP | 8.2 or higher |
|
||||
| MySQL | 8.x or higher |
|
||||
| MariaDB | 10.x or higher |
|
||||
| Composer | v2 |
|
||||
| Node.js | LTS |
|
||||
| Database | [Arcturus Morningstar](https://github.com/ObjectRetros/retro-hotel-files/blob/main/arcturus-3-5-3-database.sql) |
|
||||
|
||||
### Required PHP Extensions
|
||||
|
||||
Ensure the following extensions are enabled in your `php.ini`:
|
||||
|
||||
```ini
|
||||
extension=curl
|
||||
extension=fileinfo
|
||||
extension=gd
|
||||
extension=mbstring
|
||||
extension=openssl
|
||||
extension=pdo_mysql
|
||||
extension=sockets
|
||||
extension=intl
|
||||
```
|
||||
|
||||
**Note:** Remove the semicolon (`;`) prefix if the extension is commented out.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Windows Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ObjectRetros/atomcms.git
|
||||
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
|
||||
# For development: npm run dev:atom
|
||||
```
|
||||
|
||||
#### IIS Configuration
|
||||
|
||||
Point your IIS site to the `public` folder inside the `atomcms` directory.
|
||||
|
||||
#### Required Permissions
|
||||
|
||||
Grant "Full control" to both `IUSR` and `IIS_IUSRS` for the atomcms folder.
|
||||
|
||||
**Visual guide:** [Permission setup tutorial](https://gyazo.com/7d5f38525a762c1b26bbd7552ca93478)
|
||||
|
||||
#### Troubleshooting cURL SSL Errors
|
||||
|
||||
If you encounter cURL 60 errors:
|
||||
|
||||
1. Download the latest [cacert.pem](https://curl.se/docs/caextract.html)
|
||||
2. Place it in `C:/`
|
||||
3. Edit `php.ini` and update:
|
||||
```ini
|
||||
curl.cainfo = "C:/cacert-2025-09-09.pem"
|
||||
```
|
||||
4. Restart your web server
|
||||
|
||||
#### Complete Windows Tutorial
|
||||
|
||||
New to retro hotel setup? Follow our comprehensive three-part series:
|
||||
|
||||
- [Part 1: Basic Setup](https://devbest.com/threads/how-to-set-up-a-retro-in-2022-iis-nitro-html5-part-1.92532/)
|
||||
- [Part 2: Configuration](https://devbest.com/threads/how-to-set-up-a-retro-in-2022-iis-nitro-html5-part-2.92533/)
|
||||
- [Part 3: Finalization](https://devbest.com/threads/how-to-set-up-a-retro-in-2022-iis-nitro-html5-part-3.92543/)
|
||||
|
||||
---
|
||||
|
||||
### Linux Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ObjectRetros/atomcms.git
|
||||
cd atomcms
|
||||
|
||||
# Configure environment
|
||||
cp .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
|
||||
# For development: npm run dev:atom
|
||||
```
|
||||
|
||||
#### Set Permissions
|
||||
|
||||
```bash
|
||||
sudo chown -R $USER:www-data storage
|
||||
sudo chown -R $USER:www-data bootstrap/cache
|
||||
chmod -R 775 storage
|
||||
chmod -R 775 bootstrap/cache
|
||||
```
|
||||
|
||||
#### NGINX Configuration
|
||||
|
||||
For NGINX setup, refer to Laravel's [deployment documentation](https://laravel.com/docs/12.x/deployment#nginx).
|
||||
|
||||
#### Complete Linux Tutorial
|
||||
|
||||
Need help setting up your retro hotel on Linux? Follow our comprehensive Ubuntu tutorial:
|
||||
|
||||
- [Complete Ubuntu Setup Guide](https://git.krews.org/duckietm/ubuntu-tutorial) - Step-by-step instructions for Ubuntu
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Production Environment
|
||||
|
||||
Update these variables in your `.env` file for production:
|
||||
|
||||
```dotenv
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
FORCE_HTTPS=true # If using Cloudflare's "Always use HTTPS"
|
||||
```
|
||||
|
||||
### Cloudflare Turnstile Captcha
|
||||
|
||||
Protect your site from bots:
|
||||
|
||||
1. Visit [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/)
|
||||
2. Sign in and select your site
|
||||
3. Copy the site and secret keys to your `.env` file
|
||||
|
||||
### Important: Disable Rocket Loader
|
||||
|
||||
Atom CMS uses JavaScript that conflicts with Cloudflare's Rocket Loader. To disable:
|
||||
|
||||
1. Go to your Cloudflare dashboard
|
||||
2. Navigate to **Speed** → **Optimization**
|
||||
3. Find **Rocket Loader™** and disable it
|
||||
|
||||
### Migrating from Another CMS
|
||||
|
||||
If migrating from Cosmic CMS or similar platforms:
|
||||
|
||||
Set `RENAME_COLLIDING_TABLES=true` in your `.env` file. Atom CMS will automatically handle table conflicts.
|
||||
|
||||
**Note:** We recommend proper manual cleanup, but this feature helps avoid common migration issues.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Atom CMS includes a growing test suite using Pest.
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
# Using Pest
|
||||
vendor/bin/pest
|
||||
|
||||
# Using Artisan
|
||||
php artisan test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For detailed documentation, addons, tips, and tricks, visit our [official wiki](https://github.com/atom-retros/atomcms/wiki).
|
||||
|
||||
### Learning Laravel
|
||||
|
||||
New to Laravel? These free resources will help:
|
||||
- [Official bootcamp & course](https://learn.laravel.com) - Official Laravel bootcamp & course
|
||||
- [Laravel 11 from Scratch](https://laracasts.com/series/30-days-to-learn-laravel-11) - Official Laracasts course
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! To maintain code quality and streamline reviews, please read our [contribution guidelines](https://github.com/ObjectRetros/atomcms/wiki/0.-Contribution-guidelines) before submitting a pull request.
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Credits
|
||||
|
||||
Atom CMS is made possible by our amazing community:
|
||||
|
||||
### Core Contributors
|
||||
|
||||
- **Kasja** - Design direction, Dusk theme, ideas & graphics
|
||||
- **INicollas** - Dark mode, Turbolinks, article reactions, user sessions, PT-BR translations, Orion Housekeeping
|
||||
- **Kani** - Rcon system, FindRetros API, Atom CMS v2 creator/maintainer
|
||||
- **DuckieTM** - Badge drawer, bugfixes, housekeeping features
|
||||
- **EntenKoeniq** - Auto language registration, color scheme selection, various page fixes
|
||||
|
||||
### Contributors
|
||||
|
||||
- **Dominic** - Performance improvements, user sessions
|
||||
- **Beny** - FindRetros API fixes, Cloudflare fixes
|
||||
- **Live** - French translations, bugfixes
|
||||
- **MisterDeen** - Custom Discord widget
|
||||
- **DamienJolly**, **Danbo**, **Diddy/Josh** - Various bugfixes and improvements
|
||||
- **Sonay** - Material theme inspiration
|
||||
- **Raizer** - Circinus
|
||||
|
||||
### Translations
|
||||
|
||||
- **Oliver** - Finnish
|
||||
- **Damue & EntenKoeniq** - German
|
||||
- **Talion** - Turkish
|
||||
- **CentralCee, Rille & Tuborgs** - Swedish
|
||||
- **Yannick** - Dutch
|
||||
- **Gedomi** - Spanish
|
||||
- **Lorenzune** - Italian
|
||||
- **Twana** - Norwegian
|
||||
- **Plow** - French
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⬆ Back to Top](#atom-cms)**
|
||||
|
||||
Made with ❤️ by the Atom CMS Community
|
||||
|
||||
</div>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?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()) {
|
||||
$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();
|
||||
|
||||
$user->update([
|
||||
'ip_current' => $request->ip(),
|
||||
]);
|
||||
|
||||
return app(TwoFactorLoginResponse::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?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)
|
||||
{
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?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 && $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 && $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) {
|
||||
// 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->getKey(),
|
||||
'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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Actions\Fortify\Rules\PasswordValidationRules;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param mixed $user
|
||||
*/
|
||||
public function reset($user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Actions\Fortify\Rules\PasswordValidationRules;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param mixed $user
|
||||
*/
|
||||
public function update($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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param mixed $user
|
||||
*/
|
||||
public function update($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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param mixed $user
|
||||
*/
|
||||
protected function updateVerifiedUser($user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\CurrencyTypes;
|
||||
use App\Services\RconService;
|
||||
|
||||
class SendCurrency
|
||||
{
|
||||
public function __construct(protected RconService $rcon) {}
|
||||
|
||||
public function execute($user, string $type, ?int $amount)
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
class UserActions
|
||||
{
|
||||
public function updateUsername($user, $username): void
|
||||
{
|
||||
$user->update([
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateEmail($user, $email): void
|
||||
{
|
||||
$user->update([
|
||||
'mail' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateMotto($user, $motto): void
|
||||
{
|
||||
$user->update([
|
||||
'motto' => $motto,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateField($user, string $field, ?string $value): void
|
||||
{
|
||||
$user->update([
|
||||
$field => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?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!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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()
|
||||
{
|
||||
$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...");
|
||||
|
||||
$this->runBuildCommand($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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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.');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?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.');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum NotificationType: string
|
||||
{
|
||||
case ArticlePosted = 'article_posted';
|
||||
case HousekeepingCustomMessage = 'housekeeping_custom_message';
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class MigrationFailedException extends Exception {}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class RconConnectionException extends Exception {}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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, $date) => $query->whereDate($name, '>=', $date),
|
||||
)
|
||||
->when(
|
||||
$data["{$name}_until"],
|
||||
fn (Builder $query, $date) => $query->whereDate($name, '<=', $date),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?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 $badgeWasPreviouslyCreated;
|
||||
|
||||
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
|
||||
{
|
||||
return __(
|
||||
sprintf('filament::resources.resources.%s.navigation_label', static::$translateIdentifier),
|
||||
);
|
||||
}
|
||||
|
||||
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($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']) || is_array($badgeData['flash']);
|
||||
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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 $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', __('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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?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\TextInput;
|
||||
use Illuminate\Support\Str;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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 (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('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
<?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 (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('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\NavigationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\NavigationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditNavigation extends EditRecord
|
||||
{
|
||||
protected static string $resource = NavigationResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\NavigationResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\NavigationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListNavigations extends ListRecords
|
||||
{
|
||||
protected static string $resource = NavigationResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\NavigationResource\RelationManagers;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\DissociateAction;
|
||||
use Filament\Actions\DissociateBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class SubNavigationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'subNavigations';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'label';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('label')
|
||||
->label(__('filament::resources.inputs.label'))
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('slug')
|
||||
->label(__('filament::resources.inputs.slug')),
|
||||
|
||||
TextInput::make('order')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->label(__('filament::resources.columns.order')),
|
||||
|
||||
Toggle::make('visible')
|
||||
->label(__('filament::resources.columns.visible')),
|
||||
|
||||
Toggle::make('new_tab')
|
||||
->label(__('filament::resources.columns.new_tab')),
|
||||
])
|
||||
->columns([
|
||||
'sm' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('label'),
|
||||
|
||||
TextColumn::make('slug')
|
||||
->label(__('filament::resources.columns.slug')),
|
||||
|
||||
ToggleColumn::make('visible')
|
||||
->label(__('filament::resources.columns.visible')),
|
||||
|
||||
ToggleColumn::make('new_tab')
|
||||
->label(__('filament::resources.columns.new_tab')),
|
||||
|
||||
TextColumn::make('order')
|
||||
->label(__('filament::resources.columns.order')),
|
||||
])
|
||||
->reorderable('order')
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make(),
|
||||
// Tables\Actions\AssociateAction::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DissociateAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DissociateBulkAction::make(),
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Permissions\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Permissions\PermissionResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePermission extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PermissionResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Permissions\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Permissions\PermissionResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPermission extends EditRecord
|
||||
{
|
||||
protected static string $resource = PermissionResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Permissions\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Permissions\PermissionResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPermissions extends ListRecords
|
||||
{
|
||||
protected static string $resource = PermissionResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Permissions\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Permissions\PermissionResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewPermission extends ViewRecord
|
||||
{
|
||||
protected static string $resource = PermissionResource::class;
|
||||
|
||||
public function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Permissions;
|
||||
|
||||
use App\Filament\Resources\Atom\Permissions\Pages\CreatePermission;
|
||||
use App\Filament\Resources\Atom\Permissions\Pages\EditPermission;
|
||||
use App\Filament\Resources\Atom\Permissions\Pages\ListPermissions;
|
||||
use App\Filament\Resources\Atom\Permissions\Pages\ViewPermission;
|
||||
use App\Filament\Tables\Columns\HabboBadgeColumn;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Game\Permission;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Pages\Enums\SubNavigationPosition;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Support\Str;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\HtmlString;
|
||||
// ensure Str is imported once
|
||||
|
||||
class PermissionResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = Permission::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/permissions';
|
||||
|
||||
public static string $translateIdentifier = 'permissions';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'rank_name';
|
||||
|
||||
protected static ?\Filament\Pages\Enums\SubNavigationPosition $subNavigationPosition = SubNavigationPosition::Top;
|
||||
|
||||
public static function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema
|
||||
{
|
||||
/**
|
||||
* @param string $name
|
||||
* @param bool $needsSecondOption = false
|
||||
*/
|
||||
$groupedToggleButton = fn (string $name, bool $needsSecondOption = false): ToggleButtons => ToggleButtons::make($name)
|
||||
->label(function () use ($name) {
|
||||
$translationKey = "filament::resources.permissions.{$name}";
|
||||
$translation = __($translationKey);
|
||||
|
||||
if ($translationKey == $translation) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return $translation;
|
||||
})
|
||||
->options(function () use ($needsSecondOption) {
|
||||
$options = [
|
||||
'0' => __('filament::resources.options.no'),
|
||||
'1' => __('filament::resources.options.yes'),
|
||||
];
|
||||
|
||||
if ($needsSecondOption) {
|
||||
$options['2'] = __('filament::resources.options.rights');
|
||||
}
|
||||
|
||||
return $options;
|
||||
})
|
||||
->icons(['0' => 'heroicon-o-check', '1' => 'heroicon-o-x-mark', '2' => 'heroicon-o-sparkles'])
|
||||
->colors(['0' => 'danger', '1' => 'success'])
|
||||
->grouped();
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make('Main')
|
||||
->tabs([
|
||||
Tab::make(__('filament::resources.tabs.General Information'))
|
||||
->schema([
|
||||
TextInput::make('rank_name')
|
||||
->label(__('filament::resources.inputs.name'))
|
||||
->maxLength(25)
|
||||
->required(),
|
||||
|
||||
TextInput::make('badge')
|
||||
->label(__('filament::resources.inputs.badge_code'))
|
||||
->maxLength(12)
|
||||
->required(),
|
||||
|
||||
TextInput::make('level')
|
||||
->label(__('filament::resources.inputs.level'))
|
||||
->required(),
|
||||
|
||||
TextInput::make('room_effect')
|
||||
->label(__('filament::resources.inputs.room_effect'))
|
||||
->required(),
|
||||
]),
|
||||
|
||||
Tab::make(__('filament::resources.tabs.In-game Permissions'))
|
||||
->schema([
|
||||
Section::make(__('filament::resources.sections.permissions.title'))
|
||||
->description(new HtmlString(__('filament::resources.sections.permissions.description')))
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns([
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema(function () use ($groupedToggleButton) {
|
||||
$columns = Schema::getColumns('permissions');
|
||||
|
||||
$arcturusPermissions = collect($columns)->filter(function (array $column) {
|
||||
$columnName = $column['name'] ?? null;
|
||||
|
||||
if (! $columnName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_starts_with($columnName, 'cmd')
|
||||
|| str_starts_with($columnName, 'acc')
|
||||
|| str_ends_with($columnName, 'cmd');
|
||||
})->values();
|
||||
|
||||
return $arcturusPermissions->map(function (array $column) use ($groupedToggleButton) {
|
||||
$columnName = $column['name'];
|
||||
$needsSecondOption = $column['type_name'] == 'enum' && str_ends_with((string) $column['type'], "'2')");
|
||||
|
||||
return $groupedToggleButton($columnName, $needsSecondOption);
|
||||
})->toArray();
|
||||
}),
|
||||
]),
|
||||
|
||||
]),
|
||||
|
||||
Tab::make(__('filament::resources.tabs.Configurations'))
|
||||
->schema([
|
||||
Grid::make(['default' => 2])
|
||||
->schema([
|
||||
Select::make('log_commands')
|
||||
->label(__('filament::resources.inputs.log_commands'))
|
||||
->columnSpanFull()
|
||||
->options([
|
||||
'0' => __('filament::resources.options.no'),
|
||||
'1' => __('filament::resources.options.yes'),
|
||||
]),
|
||||
|
||||
TextInput::make('prefix')
|
||||
->label(__('filament::resources.inputs.prefix'))
|
||||
->maxLength(5)
|
||||
->required(),
|
||||
|
||||
ColorPicker::make('prefix_color')
|
||||
->label(__('filament::resources.inputs.prefix_color'))
|
||||
->required(),
|
||||
|
||||
Toggle::make('hidden_rank')
|
||||
->label(__('filament::resources.inputs.is_hidden'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make()
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns([
|
||||
'md' => 2,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('auto_credits_amount')
|
||||
->columnSpan(1)
|
||||
->label(__('filament::resources.inputs.auto_credits_amount'))
|
||||
->required(),
|
||||
|
||||
TextInput::make('auto_pixels_amount')
|
||||
->label(__('filament::resources.inputs.auto_pixels_amount'))
|
||||
->required(),
|
||||
|
||||
TextInput::make('auto_gotw_amount')
|
||||
->label(__('filament::resources.inputs.auto_gotw_amount'))
|
||||
->required(),
|
||||
|
||||
TextInput::make('auto_points_amount')
|
||||
->label(__('filament::resources.inputs.auto_points_amount'))
|
||||
->required(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label(__('filament::resources.columns.id')),
|
||||
|
||||
HabboBadgeColumn::make('badge')
|
||||
->alignCenter()
|
||||
->label(__('filament::resources.columns.image')),
|
||||
|
||||
TextColumn::make('rank_name')
|
||||
->label(__('filament::resources.columns.name'))
|
||||
->description(fn (Permission $record) => Str::limit($record->description, 40))
|
||||
->tooltip(function (Permission $record): ?string {
|
||||
$description = $record->description;
|
||||
|
||||
if (strlen($description) <= 40) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $description;
|
||||
})
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('prefix')
|
||||
->label(__('filament::resources.columns.prefix'))
|
||||
->description(fn (Permission $record) => $record->prefix_color)
|
||||
->searchable(),
|
||||
|
||||
ToggleColumn::make('hidden_rank')
|
||||
->label(__('filament::resources.columns.is_hidden')),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListPermissions::route('/'),
|
||||
'create' => CreatePermission::route('/create'),
|
||||
'view' => ViewPermission::route('/{record}'),
|
||||
'edit' => EditPermission::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Tags\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Tags\TagResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateTag extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TagResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Tags\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Tags\TagResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTag extends EditRecord
|
||||
{
|
||||
protected static string $resource = TagResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Tags\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Tags\TagResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTags extends ListRecords
|
||||
{
|
||||
protected static string $resource = TagResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Tags\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Tags\TagResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewTag extends ViewRecord
|
||||
{
|
||||
protected static string $resource = TagResource::class;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Tags\RelationManagers;
|
||||
|
||||
use App\Filament\Resources\Atom\Articles\ArticleResource;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use Filament\Actions\AttachAction;
|
||||
use Filament\Actions\DetachAction;
|
||||
use Filament\Actions\DetachBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ArticlesRelationManager extends RelationManager
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
// Use camelCase to match the method in the Tag model
|
||||
protected static string $relationship = 'websiteArticles';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'title';
|
||||
|
||||
public static string $translateIdentifier = 'article';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components(ArticleResource::getForm());
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns(ArticleResource::getTable())
|
||||
->modifyQueryUsing(fn ($query) => $query->latest())
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
AttachAction::make()
|
||||
->preloadRecordSelect(),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
DetachAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DetachBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Tags;
|
||||
|
||||
use App\Filament\Resources\Atom\Tags\Pages\CreateTag;
|
||||
use App\Filament\Resources\Atom\Tags\Pages\EditTag;
|
||||
use App\Filament\Resources\Atom\Tags\Pages\ListTags;
|
||||
use App\Filament\Resources\Atom\Tags\Pages\ViewTag;
|
||||
use App\Filament\Resources\Atom\Tags\RelationManagers\ArticlesRelationManager;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Articles\Tag;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\ColorColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TagResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = Tag::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-tag';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/tags';
|
||||
|
||||
public static string $translateIdentifier = 'tags';
|
||||
|
||||
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('name')
|
||||
->label(__('filament::resources.inputs.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autocomplete()
|
||||
->columnSpan('full'),
|
||||
|
||||
ColorPicker::make('background_color')
|
||||
->label(__('filament::resources.inputs.background_color'))
|
||||
->required()
|
||||
->columnSpan('full'),
|
||||
]),
|
||||
])->columnSpanFull(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns(static::getTable())
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getTable(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('id')
|
||||
->label(__('filament::resources.columns.id')),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label(__('filament::resources.columns.name'))
|
||||
->searchable()
|
||||
->limit(50),
|
||||
|
||||
ColorColumn::make('background_color')
|
||||
->label(__('filament::resources.columns.background_color'))
|
||||
->searchable()
|
||||
->copyable()
|
||||
->copyMessage(__('filament::resources.common.Sucessfull'))
|
||||
->copyMessageDuration(1500),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
ArticlesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListTags::route('/'),
|
||||
'create' => CreateTag::route('/create'),
|
||||
'view' => ViewTag::route('/{record}'),
|
||||
'edit' => EditTag::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Teams\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Teams\TeamResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateTeam extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TeamResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Teams\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Teams\TeamResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTeam extends EditRecord
|
||||
{
|
||||
protected static string $resource = TeamResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Teams\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\Teams\TeamResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTeams extends ListRecords
|
||||
{
|
||||
protected static string $resource = TeamResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\Teams;
|
||||
|
||||
use App\Filament\Resources\Atom\Teams\Pages\CreateTeam;
|
||||
use App\Filament\Resources\Atom\Teams\Pages\EditTeam;
|
||||
use App\Filament\Resources\Atom\Teams\Pages\ListTeams;
|
||||
use App\Filament\Tables\Columns\HabboBadgeColumn;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Community\Staff\WebsiteTeam;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TeamResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteTeam::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-user-group';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/teams';
|
||||
|
||||
public static string $translateIdentifier = 'teams';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make()
|
||||
->schema([
|
||||
TextInput::make('rank_name')
|
||||
->autofocus()
|
||||
->maxLength(255)
|
||||
->required()
|
||||
->label(__('filament::resources.inputs.name')),
|
||||
|
||||
TextInput::make('job_description')
|
||||
->maxLength(255)
|
||||
->label(__('filament::resources.inputs.description')),
|
||||
|
||||
TextInput::make('badge')
|
||||
->maxLength(255)
|
||||
->label(__('filament::resources.inputs.badge_code'))
|
||||
->required(),
|
||||
|
||||
Toggle::make('hidden_rank')
|
||||
->label(__('filament::resources.inputs.is_hidden')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label(__('filament::resources.columns.id')),
|
||||
|
||||
HabboBadgeColumn::make('badge')
|
||||
->label(__('filament::resources.columns.badge')),
|
||||
|
||||
TextColumn::make('rank_name')
|
||||
->label(__('filament::resources.columns.name')),
|
||||
|
||||
TextColumn::make('job_description')
|
||||
->label(__('filament::resources.inputs.description')),
|
||||
|
||||
IconColumn::make('hidden_rank')
|
||||
->label(__('filament::resources.columns.is_hidden'))
|
||||
->icon(fn (WebsiteTeam $record) => $record->hidden_rank ? 'heroicon-o-check-circle' : 'heroicon-o-x-circle')
|
||||
->colors([
|
||||
'danger' => false,
|
||||
'success' => true,
|
||||
]),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListTeams::route('/'),
|
||||
'create' => CreateTeam::route('/create'),
|
||||
'edit' => EditTeam::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\WebsiteDrawBadges\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\WebsiteDrawBadges\WebsiteDrawBadgeResource;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditWebsiteDrawBadge extends EditRecord
|
||||
{
|
||||
protected static string $resource = WebsiteDrawBadgeResource::class;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\WebsiteDrawBadges\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\WebsiteDrawBadges\WebsiteDrawBadgeResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListWebsiteDrawBadge extends ListRecords
|
||||
{
|
||||
protected static string $resource = WebsiteDrawBadgeResource::class;
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\WebsiteDrawBadges;
|
||||
|
||||
use App\Filament\Resources\Atom\WebsiteDrawBadges\Pages\EditWebsiteDrawBadge;
|
||||
use App\Filament\Resources\Atom\WebsiteDrawBadges\Pages\ListWebsiteDrawBadge;
|
||||
use App\Models\WebsiteDrawBadge;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
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;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class WebsiteDrawBadgeResource extends Resource
|
||||
{
|
||||
protected static ?string $model = WebsiteDrawBadge::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-trophy';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'draw-badges';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'draw badges';
|
||||
|
||||
protected static ?string $navigationLabel = 'Draw Badges';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('badge_name')
|
||||
->label(__('Badge Name'))
|
||||
->nullable()
|
||||
->maxLength(24)
|
||||
->autocomplete(false),
|
||||
TextInput::make('badge_desc')
|
||||
->label(__('Badge Description'))
|
||||
->nullable()
|
||||
->maxLength(255)
|
||||
->autocomplete(false)
|
||||
->columnSpanFull(),
|
||||
Toggle::make('published')
|
||||
->label(__('Published'))
|
||||
->default(false),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label(__('ID'))
|
||||
->sortable(),
|
||||
TextColumn::make('user_id')
|
||||
->label(__('User ID')),
|
||||
TextColumn::make('user.username')
|
||||
->label(__('Username'))
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('badge_name')
|
||||
->limit(8)
|
||||
->label(__('Badge Name')),
|
||||
TextColumn::make('badge_desc')
|
||||
->label(__('Badge description'))
|
||||
->limit(35)
|
||||
->tooltip(function (TextColumn $column): ?string {
|
||||
$state = $column->getState();
|
||||
if (strlen($state) <= $column->getCharacterLimit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $state;
|
||||
}),
|
||||
TextColumn::make('created_at')
|
||||
->label(__('Created At'))
|
||||
->dateTime(),
|
||||
ImageColumn::make('badge_url')
|
||||
->label(__('Badge'))
|
||||
->getStateUsing(fn ($record) => config('app.url') . $record->badge_url)
|
||||
->extraAttributes(['style' => 'image-rendering: pixelated'])
|
||||
->size(40),
|
||||
ToggleColumn::make('published')
|
||||
->label(__('Published')),
|
||||
])
|
||||
->recordActions([
|
||||
DeleteAction::make()
|
||||
->before(function (DeleteAction $action, WebsiteDrawBadge $record) {
|
||||
$badgeCode = pathinfo($record->badge_path, PATHINFO_FILENAME);
|
||||
|
||||
// Remove the badge from any user before deleting it.
|
||||
if ($record->published) {
|
||||
DB::table('users_badges')
|
||||
->where('user_id', $record->user_id)
|
||||
->where('badge_code', $badgeCode)
|
||||
->delete();
|
||||
}
|
||||
|
||||
// Remove from JSON
|
||||
$filePath = DB::table('website_settings')->where('key', 'nitro_external_texts_file')->value('value');
|
||||
|
||||
if ($filePath && file_exists($filePath) && is_writable($filePath)) {
|
||||
$json = json_decode(file_get_contents($filePath), true);
|
||||
unset($json["badge_name_{$badgeCode}"]);
|
||||
unset($json["badge_desc_{$badgeCode}"]);
|
||||
file_put_contents($filePath, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
// Delete the badge file from the filesystem
|
||||
$badgePath = $record->badge_path;
|
||||
if ($badgePath && file_exists($badgePath)) {
|
||||
unlink($badgePath);
|
||||
}
|
||||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
DeleteBulkAction::make()
|
||||
->before(function (DeleteBulkAction $action, $records) {
|
||||
foreach ($records as $record) {
|
||||
$badgeCode = pathinfo((string) $record->badge_path, PATHINFO_FILENAME);
|
||||
|
||||
// Remove the badge from any user before deleting it.
|
||||
if ($record->published) {
|
||||
DB::table('users_badges')
|
||||
->where('user_id', $record->user_id)
|
||||
->where('badge_code', $badgeCode)
|
||||
->delete();
|
||||
}
|
||||
|
||||
$filePath = DB::table('website_settings')->where('key', 'nitro_external_texts_file')->value('value');
|
||||
|
||||
if ($filePath && file_exists($filePath) && is_writable($filePath)) {
|
||||
$json = json_decode(file_get_contents($filePath), true);
|
||||
unset($json["badge_name_{$badgeCode}"]);
|
||||
unset($json["badge_desc_{$badgeCode}"]);
|
||||
file_put_contents($filePath, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
$badgePath = $record->badge_path;
|
||||
if ($badgePath && file_exists($badgePath)) {
|
||||
unlink($badgePath);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListWebsiteDrawBadge::route('/'),
|
||||
'edit' => EditWebsiteDrawBadge::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom;
|
||||
|
||||
use App\Filament\Resources\Atom\WriteableBoxResource\Pages\ManageWriteableBoxes;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\WebsiteWriteableBox;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class WriteableBoxResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = WebsiteWriteableBox::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-pencil-square';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Website';
|
||||
|
||||
protected static ?string $slug = 'website/writeable-boxes';
|
||||
|
||||
public static string $translateIdentifier = 'writeable-boxes';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components(static::getForm());
|
||||
}
|
||||
|
||||
public static function getForm(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('title')
|
||||
->label(__('Title'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('content')
|
||||
->label(__('Content'))
|
||||
->required(),
|
||||
|
||||
TextInput::make('order')
|
||||
->label(__('Order'))
|
||||
->numeric()
|
||||
->default(0),
|
||||
];
|
||||
}
|
||||
|
||||
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('order')
|
||||
->label(__('Order'))
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('Created'))
|
||||
->dateTime(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ManageWriteableBoxes::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Atom\WriteableBoxResource\Pages;
|
||||
|
||||
use App\Filament\Resources\Atom\WriteableBoxResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ManageRecords;
|
||||
|
||||
class ManageWriteableBoxes extends ManageRecords
|
||||
{
|
||||
protected static string $resource = WriteableBoxResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DashboardResource\Widgets;
|
||||
|
||||
use App\Filament\Resources\Shop\ShopOrderResource;
|
||||
use App\Models\User\UserOrder;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as BaseWidget;
|
||||
|
||||
class LatestOrders extends BaseWidget
|
||||
{
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(UserOrder::latest())
|
||||
->paginated([3, 5, 8])
|
||||
->columns(ShopOrderResource::getTable())
|
||||
->recordActions([
|
||||
ViewAction::make()->schema(ShopOrderResource::getForm()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DashboardResource\Widgets;
|
||||
|
||||
use App\Models\User\UserOrder;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Flowframe\Trend\Trend;
|
||||
use Flowframe\Trend\TrendValue;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
class OrdersAggregateChart extends ChartWidget
|
||||
{
|
||||
protected ?string $maxHeight = '300px';
|
||||
|
||||
protected string $color = 'secondary';
|
||||
|
||||
public function getHeading(): string|Htmlable|null
|
||||
{
|
||||
return __('filament::resources.stats.orders_chart.title');
|
||||
}
|
||||
|
||||
public function getDescription(): string|Htmlable|null
|
||||
{
|
||||
return __('filament::resources.stats.orders_chart.description');
|
||||
}
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$pendingOrder = Trend::query(UserOrder::pending())
|
||||
->between(start: now()->startOfMonth(), end: now()->endOfMonth())
|
||||
->perDay()
|
||||
->count();
|
||||
|
||||
$cancelledOrder = Trend::query(UserOrder::cancelled())
|
||||
->between(start: now()->startOfMonth(), end: now()->endOfMonth())
|
||||
->perDay()
|
||||
->count();
|
||||
|
||||
$completedOrder = Trend::query(UserOrder::completed())
|
||||
->between(start: now()->startOfMonth(), end: now()->endOfMonth())
|
||||
->perDay()
|
||||
->count();
|
||||
|
||||
$datasets = [
|
||||
$this->getDataset($pendingOrder, __('filament::resources.stats.orders_chart.pending'), '#fbbf24', '#f59e0b'),
|
||||
$this->getDataset($cancelledOrder, __('filament::resources.stats.orders_chart.cancelled'), '#dc2626', '#b91c1c'),
|
||||
$this->getDataset($completedOrder, __('filament::resources.stats.orders_chart.completed'), '#10b981', '#059669'),
|
||||
];
|
||||
|
||||
$data = $pendingOrder->map(fn (TrendValue $value) => $value->date)->merge(
|
||||
$cancelledOrder->map(fn (TrendValue $value) => $value->date),
|
||||
)->merge(
|
||||
$completedOrder->map(fn (TrendValue $value) => $value->date),
|
||||
)->unique()->sort()->flatten();
|
||||
|
||||
return [
|
||||
'datasets' => $datasets,
|
||||
'labels' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getDataset($data, $label, string $backgroundColor, string $borderColor): array
|
||||
{
|
||||
return [
|
||||
'label' => $label,
|
||||
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
|
||||
'backgroundColor' => $backgroundColor,
|
||||
'borderColor' => $borderColor,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\Achievements;
|
||||
|
||||
use App\Enums\AchievementCategory;
|
||||
use App\Enums\CurrencyTypes;
|
||||
use App\Filament\Resources\Hotel\Achievements\Pages\CreateAchievement;
|
||||
use App\Filament\Resources\Hotel\Achievements\Pages\EditAchievement;
|
||||
use App\Filament\Resources\Hotel\Achievements\Pages\ListAchievements;
|
||||
use App\Filament\Resources\Hotel\Achievements\Pages\ViewAchievement;
|
||||
use App\Filament\Tables\Columns\HabboBadgeColumn;
|
||||
use App\Filament\Traits\TranslatableResource;
|
||||
use App\Models\Achievement;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AchievementResource extends Resource
|
||||
{
|
||||
use TranslatableResource;
|
||||
|
||||
protected static ?string $model = Achievement::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-academic-cap';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Hotel';
|
||||
|
||||
public static string $translateIdentifier = 'achievements';
|
||||
|
||||
protected static ?string $slug = 'hotel/achievements';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make('Main')
|
||||
->tabs([
|
||||
Tab::make(__('filament::resources.tabs.Home'))
|
||||
->icon('heroicon-o-home')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('filament::resources.inputs.name'))
|
||||
->required()
|
||||
->maxLength(64)
|
||||
->autocomplete()
|
||||
->columnSpan('full'),
|
||||
|
||||
TextInput::make('level')
|
||||
->label(__('filament::resources.inputs.level'))
|
||||
->numeric()
|
||||
->required()
|
||||
->autocomplete()
|
||||
->columnSpan('full'),
|
||||
|
||||
Select::make('category')
|
||||
->native(false)
|
||||
->label(__('filament::resources.inputs.category'))
|
||||
->options(AchievementCategory::toInput()),
|
||||
]),
|
||||
|
||||
Tab::make(__('filament::resources.tabs.Configurations'))
|
||||
->icon('heroicon-o-cog')
|
||||
->schema([
|
||||
Select::make('visible')
|
||||
->native(false)
|
||||
->label(__('filament::resources.inputs.visible'))
|
||||
->options([
|
||||
'1' => __('filament::resources.common.Yes'),
|
||||
'0' => __('filament::resources.common.No'),
|
||||
]),
|
||||
|
||||
Select::make('reward_type')
|
||||
->native(false)
|
||||
->label(__('filament::resources.inputs.reward_type'))
|
||||
->options(CurrencyTypes::toInput()),
|
||||
|
||||
TextInput::make('reward_amount')
|
||||
->label(__('filament::resources.inputs.reward_amount'))
|
||||
->numeric()
|
||||
->required(),
|
||||
|
||||
TextInput::make('points')
|
||||
->label(__('filament::resources.inputs.points'))
|
||||
->helperText(__('filament::resources.helpers.achievement_points'))
|
||||
->numeric()
|
||||
->required(),
|
||||
|
||||
TextInput::make('progress_needed')
|
||||
->label(__('filament::resources.inputs.progress_needed'))
|
||||
->helperText(__('filament::resources.helpers.achievement_progress_needed'))
|
||||
->numeric()
|
||||
->required(),
|
||||
]),
|
||||
])->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label(__('filament::resources.columns.id')),
|
||||
|
||||
HabboBadgeColumn::make('badge')
|
||||
->label(__('filament::resources.columns.badge')),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label(__('filament::resources.columns.name'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('level')
|
||||
->label(__('filament::resources.columns.level')),
|
||||
|
||||
TextColumn::make('category')
|
||||
->badge()
|
||||
->searchable()
|
||||
->label(__('filament::resources.columns.category'))
|
||||
->toggleable(),
|
||||
|
||||
ToggleColumn::make('visible')
|
||||
->label(__('filament::resources.columns.visible'))
|
||||
->disabled()
|
||||
->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('visible')
|
||||
->options([
|
||||
'1' => __('filament::resources.common.Yes'),
|
||||
'0' => __('filament::resources.common.No'),
|
||||
])
|
||||
->label(__('filament::resources.columns.visible'))
|
||||
->placeholder(__('filament::resources.common.All')),
|
||||
|
||||
SelectFilter::make('category')
|
||||
->options(AchievementCategory::toInput())
|
||||
->label(__('filament::resources.columns.category'))
|
||||
->placeholder(__('filament::resources.common.All')),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAchievements::route('/'),
|
||||
'create' => CreateAchievement::route('/create'),
|
||||
'view' => ViewAchievement::route('/{record}'),
|
||||
'edit' => EditAchievement::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\Achievements\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\Achievements\AchievementResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAchievement extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AchievementResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\Achievements\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\Achievements\AchievementResource;
|
||||
use Filament\Pages\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditAchievement extends EditRecord
|
||||
{
|
||||
protected static string $resource = AchievementResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
// Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\Achievements\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\Achievements\AchievementResource;
|
||||
use Filament\Pages\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAchievements extends ListRecords
|
||||
{
|
||||
protected static string $resource = AchievementResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
// Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\Achievements\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\Achievements\AchievementResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewAchievement extends ViewRecord
|
||||
{
|
||||
protected static string $resource = AchievementResource::class;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\BadgeTextEditors;
|
||||
|
||||
use App\Filament\Resources\Hotel\BadgeTextEditors\Pages\CreateBadgeTextEditor;
|
||||
use App\Filament\Resources\Hotel\BadgeTextEditors\Pages\EditBadgeTextEditor;
|
||||
use App\Filament\Resources\Hotel\BadgeTextEditors\Pages\ListBadgeTextEditors;
|
||||
use App\Models\WebsiteBadge;
|
||||
use App\Services\SettingsService;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BadgeTextEditorResource extends Resource
|
||||
{
|
||||
protected static ?string $model = WebsiteBadge::class;
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Hotel';
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-pencil-square';
|
||||
|
||||
protected static ?string $navigationLabel = 'Badge Editor';
|
||||
|
||||
protected static ?string $modelLabel = 'Badge Text';
|
||||
|
||||
protected static ?string $slug = 'hotel/badge-text-editor';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('badge_key')
|
||||
->required()
|
||||
->label('Badge Key - Expl. ATOM101')
|
||||
->placeholder('This is the badge code'),
|
||||
TextInput::make('badge_name')
|
||||
->required()
|
||||
->label('Badge Name')
|
||||
->placeholder('This is the name of the badge: Expl. The ATOM Badge'),
|
||||
Textarea::make('badge_description')
|
||||
->required()
|
||||
->label('Badge Description')
|
||||
->placeholder('Please add a description for the badge.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
$settingsService = app(SettingsService::class);
|
||||
$badgesPath = $settingsService->getOrDefault('badges_path', '/gamedata/c_images/album1584/');
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
ImageColumn::make('badge_key')
|
||||
->label('Badge Image')
|
||||
->getStateUsing(function ($record) use ($badgesPath) {
|
||||
$badgeName = str_replace('badge_desc_', '', $record->badge_key);
|
||||
$imageUrl = asset($badgesPath . $badgeName . '.gif');
|
||||
|
||||
return $imageUrl;
|
||||
})
|
||||
->width(50)
|
||||
->height(50),
|
||||
TextColumn::make('badge_name')
|
||||
->label('Badge Code & Name')
|
||||
->formatStateUsing(fn ($record) => $record->badge_key . ' : ' . $record->badge_name)
|
||||
->searchable(query: function ($query, $search) {
|
||||
$query->where('badge_key', 'like', "%{$search}%")
|
||||
->orWhere('badge_name', 'like', "%{$search}%");
|
||||
})
|
||||
->sortable(),
|
||||
TextColumn::make('badge_description')
|
||||
->label('Badge Description')
|
||||
->getStateUsing(fn ($record) => Str::limit($record->badge_description, 65))
|
||||
->searchable(),
|
||||
])
|
||||
->filters([])
|
||||
->defaultSort('badge_key', 'asc')
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListBadgeTextEditors::route('/'),
|
||||
'create' => CreateBadgeTextEditor::route('/create'),
|
||||
'edit' => EditBadgeTextEditor::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\BadgeTextEditors\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\BadgeTextEditors\BadgeTextEditorResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateBadgeTextEditor extends CreateRecord
|
||||
{
|
||||
protected static string $resource = BadgeTextEditorResource::class;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\BadgeTextEditors\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\BadgeTextEditors\BadgeTextEditorResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use PDOException;
|
||||
|
||||
class EditBadgeTextEditor extends EditRecord
|
||||
{
|
||||
protected static string $resource = BadgeTextEditorResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [DeleteAction::make()];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void {}
|
||||
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
try {
|
||||
return parent::handleRecordUpdate($record, $data);
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() === '23000') {
|
||||
Log::error('Duplicate badge key error: ' . $e->getMessage());
|
||||
|
||||
Notification::make()
|
||||
->title('Duplicate Badge Key')
|
||||
->body('The badge key already exists. Please use a unique badge key.')
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
return $record;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\BadgeTextEditors\Pages;
|
||||
|
||||
use App\Filament\Resources\Hotel\BadgeTextEditors\BadgeTextEditorResource;
|
||||
use App\Models\WebsiteBadge;
|
||||
use App\Services\SettingsService;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ListBadgeTextEditors extends ListRecords
|
||||
{
|
||||
protected static string $resource = BadgeTextEditorResource::class;
|
||||
|
||||
protected function getActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Add Badge')
|
||||
->color('info')
|
||||
->modalHeading('Add a New Badge')
|
||||
->modalButton('Create Badge')
|
||||
->after(function () {
|
||||
Notification::make()
|
||||
->title('Badge Created')
|
||||
->body('The badge was successfully created.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('export')
|
||||
->label('Export to ExternalTexts')
|
||||
->action('exportToJson'),
|
||||
Action::make('backup')
|
||||
->label('Create Backup of ExternalTexts')
|
||||
->color('success')
|
||||
->action('createBackup'),
|
||||
];
|
||||
}
|
||||
|
||||
public function exportToJson(SettingsService $settingsService): void
|
||||
{
|
||||
$jsonPath = $settingsService->getOrDefault('nitro_external_texts_file');
|
||||
|
||||
if (empty($jsonPath)) {
|
||||
Notification::make()
|
||||
->title('Export Failed')
|
||||
->body('The JSON file path is not configured in the website settings.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! file_exists($jsonPath)) {
|
||||
Notification::make()
|
||||
->title('Export Failed')
|
||||
->body('The JSON file does not exist at the specified path.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$jsonData = json_decode(file_get_contents($jsonPath), true);
|
||||
|
||||
$badges = WebsiteBadge::all();
|
||||
$badgeKeys = $badges->pluck('badge_key')->toArray();
|
||||
|
||||
foreach ($jsonData as $key => $value) {
|
||||
if (
|
||||
(str_starts_with((string) $key, 'badge_desc_') || str_starts_with((string) $key, 'badge_name_')) &&
|
||||
! in_array(str_replace(['badge_desc_', 'badge_name_'], '', $key), $badgeKeys)
|
||||
) {
|
||||
unset($jsonData[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($badges as $badge) {
|
||||
$jsonData['badge_desc_' . $badge->badge_key] = $badge->badge_description;
|
||||
$jsonData['badge_name_' . $badge->badge_key] = $badge->badge_name;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = file_put_contents(
|
||||
$jsonPath,
|
||||
json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
throw new Exception('Failed to write to the JSON file.');
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Export Successful')
|
||||
->body('Badge data exported successfully.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to export badge data: ' . $e->getMessage());
|
||||
|
||||
Notification::make()
|
||||
->title('Export Failed')
|
||||
->body('Failed to export badge data. Please check file permissions or contact your administrator.')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function createBackup(SettingsService $settingsService): void
|
||||
{
|
||||
$jsonPath = $settingsService->getOrDefault('nitro_external_texts_file');
|
||||
|
||||
if (empty($jsonPath)) {
|
||||
Notification::make()
|
||||
->title('Backup Failed')
|
||||
->body('The JSON file path is not configured in the website settings.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! file_exists($jsonPath)) {
|
||||
Notification::make()
|
||||
->title('Backup Failed')
|
||||
->body('The JSON file does not exist at the specified path.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$backupPath = dirname((string) $jsonPath) . '/ExternalTexts_' . time() . '.json';
|
||||
|
||||
if (copy($jsonPath, $backupPath)) {
|
||||
Notification::make()
|
||||
->title('Backup Successful')
|
||||
->body('A backup of the JSON file has been created: ' . basename($backupPath))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title('Backup Failed')
|
||||
->body('Failed to create a backup of the JSON file.')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\BadgeUploads;
|
||||
|
||||
use App\Filament\Resources\Hotel\BadgeUploads\Pages\ManageBadgeUploads;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class BadgeUploadResource extends Resource
|
||||
{
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Hotel';
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-gif';
|
||||
|
||||
protected static ?string $label = 'Badge Upload';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
FileUpload::make('badge_file')
|
||||
->label('Upload Badge')
|
||||
->disk('local')
|
||||
->directory(setting('badge_path_filesystem'))
|
||||
->required()
|
||||
->getUploadedFileNameForStorageUsing(
|
||||
fn (TemporaryUploadedFile $file): string => strtolower(str_replace([' ', '-', 'æ', 'ø', 'å'], ['_', '_', 'ae', 'oe', 'aa'], $file->getClientOriginalName())),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('filename')
|
||||
->label('File Name')
|
||||
->sortable(),
|
||||
TextColumn::make('path')
|
||||
->label('File Path'),
|
||||
])
|
||||
->filters([]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ManageBadgeUploads::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getFiles(): array
|
||||
{
|
||||
$badgePath = env('BadgePath', 'badges');
|
||||
$files = Storage::disk('local')->files($badgePath);
|
||||
|
||||
return collect($files)->map(fn ($file) => [
|
||||
'filename' => basename((string) $file),
|
||||
'path' => $file,
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Hotel\BadgeUploads\Pages;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Schemas\Components\Form;
|
||||
|
||||
/**
|
||||
* @property-read Form $form
|
||||
*/
|
||||
class ManageBadgeUploads extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
public $badge_file;
|
||||
|
||||
protected static string $resource = \App\Filament\Resources\Hotel\BadgeUploads\BadgeUploadResource::class;
|
||||
|
||||
protected string $view = 'filament.pages.manage-badge-uploads';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
// initialize form; Filament handles default values via getFormSchema
|
||||
// Avoid using fill() on the Form component in this context
|
||||
// No explicit form fill here
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
FileUpload::make('badge_file')
|
||||
->label('Upload Badge')
|
||||
->disk('badges')
|
||||
->preserveFilenames()
|
||||
->acceptedFileTypes(['image/gif'])
|
||||
->rules(['mimes:gif'])
|
||||
->required(),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
Notification::make()
|
||||
->title('Badge uploaded successfully!')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user