added PSR-12 rules
This commit is contained in:
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-01-02
|
||||||
|
- Added ACP general settings for forum name, theme, accents, and logo (no reload required).
|
||||||
|
- Added admin-only upload endpoints and ACP UI for logos and favicons.
|
||||||
|
- Applied forum branding, theme defaults, accents, logos, and favicon links in the SPA header.
|
||||||
|
|
||||||
|
## 2025-12-30
|
||||||
|
- Added soft deletes with audit metadata (deleted_at/deleted_by) for forums, threads, and posts.
|
||||||
|
- Ensured API listings and ACP forum tree omit soft-deleted records by default.
|
||||||
|
- Added thread seeding for forum test data.
|
||||||
|
- Enforced category-only roots for forums (API validation, UI, and database constraint).
|
||||||
|
- Added portal header with phpBB-style breadcrumb + quick links, plus notifications/messages + user menu.
|
||||||
|
- Replaced the home page with a portal-style layout and latest posts list.
|
||||||
|
- Added a dedicated board index page with phpBB-like sections and per-category collapse toggles.
|
||||||
|
- Persisted board index collapse state per user via user_settings (DB + API + client cache).
|
||||||
|
- Added category view rendering for subcategories in ForumView.
|
||||||
|
- Updated thread list UI (icons, spacing) and New topic button styling in ForumView.
|
||||||
|
- Added ACP per-category quick-create buttons for child categories and forums.
|
||||||
|
- Removed the legacy navbar and cleaned up related styling.
|
||||||
|
|
||||||
|
## 2025-12-29
|
||||||
|
- Merged the React app into the Laravel codebase under `resources/js`.
|
||||||
|
- Moved the Laravel app to the repo root and removed the separate `api/` and `frontend/` folders.
|
||||||
|
- Serve the SPA shell via Blade with Vite-managed React assets.
|
||||||
|
- Dropped Tailwind tooling to keep the frontend Bootstrap-only.
|
||||||
|
- Replaced the default Laravel README with a forum placeholder.
|
||||||
|
- Updated ACP forum tools with accent-tinted buttons and larger action spacing.
|
||||||
|
- Defaulted the ACP forum tree to collapsed on page load.
|
||||||
|
- Improved ACP create/edit dialog copy based on forum vs category and hid type selection during creation.
|
||||||
|
|
||||||
|
## 2025-12-26
|
||||||
|
- Replaced the Symfony backend with a fresh Laravel app in `api/`.
|
||||||
|
- Added Fortify + Sanctum, API routes, and controllers for forums/threads/posts/settings/version.
|
||||||
|
- Added Laravel migrations for forums, threads, posts, and settings (seeded with version/build/accent).
|
||||||
|
- Added Laravel i18n endpoint backed by JSON translation files.
|
||||||
|
- Updated frontend auth/register flow to work with Laravel tokens.
|
||||||
|
|
||||||
## 2025-12-24
|
## 2025-12-24
|
||||||
- Reworked the domain model into a single forum tree (category/forum types) with parent/child hierarchy and threads restricted to forum nodes.
|
- Reworked the domain model into a single forum tree (category/forum types) with parent/child hierarchy and threads restricted to forum nodes.
|
||||||
- Updated API Platform resources, filters, migrations, and JSON format support.
|
- Updated API Platform resources, filters, migrations, and JSON format support.
|
||||||
@@ -22,37 +58,3 @@
|
|||||||
- Improved ACP drag-and-drop hover reordering and visual drop target feedback.
|
- Improved ACP drag-and-drop hover reordering and visual drop target feedback.
|
||||||
- Hardened ACP access so admin tools require authentication.
|
- Hardened ACP access so admin tools require authentication.
|
||||||
- Updated the home page to render the forum tree with ACP-style rows and icons.
|
- Updated the home page to render the forum tree with ACP-style rows and icons.
|
||||||
|
|
||||||
## 2025-12-26
|
|
||||||
- Replaced the Symfony backend with a fresh Laravel app in `api/`.
|
|
||||||
- Added Fortify + Sanctum, API routes, and controllers for forums/threads/posts/settings/version.
|
|
||||||
- Added Laravel migrations for forums, threads, posts, and settings (seeded with version/build/accent).
|
|
||||||
- Added Laravel i18n endpoint backed by JSON translation files.
|
|
||||||
- Updated frontend auth/register flow to work with Laravel tokens.
|
|
||||||
|
|
||||||
## 2025-12-29
|
|
||||||
- Merged the React app into the Laravel codebase under `resources/js`.
|
|
||||||
- Moved the Laravel app to the repo root and removed the separate `api/` and `frontend/` folders.
|
|
||||||
- Serve the SPA shell via Blade with Vite-managed React assets.
|
|
||||||
- Dropped Tailwind tooling to keep the frontend Bootstrap-only.
|
|
||||||
- Replaced the default Laravel README with a forum placeholder.
|
|
||||||
- Updated ACP forum tools with accent-tinted buttons and larger action spacing.
|
|
||||||
- Defaulted the ACP forum tree to collapsed on page load.
|
|
||||||
- Improved ACP create/edit dialog copy based on forum vs category and hid type selection during creation.
|
|
||||||
|
|
||||||
## 2025-12-30
|
|
||||||
- Added soft deletes with audit metadata (deleted_at/deleted_by) for forums, threads, and posts.
|
|
||||||
- Ensured API listings and ACP forum tree omit soft-deleted records by default.
|
|
||||||
- Added thread seeding for forum test data.
|
|
||||||
- Enforced category-only roots for forums (API validation, UI, and database constraint).
|
|
||||||
- Added portal header with phpBB-style breadcrumb + quick links, plus notifications/messages + user menu.
|
|
||||||
- Replaced the home page with a portal-style layout and latest posts list.
|
|
||||||
- Added a dedicated board index page with phpBB-like sections and per-category collapse toggles.
|
|
||||||
- Persisted board index collapse state per user via user_settings (DB + API + client cache).
|
|
||||||
- Added category view rendering for subcategories in ForumView.
|
|
||||||
- Updated thread list UI (icons, spacing) and New topic button styling in ForumView.
|
|
||||||
- Added ACP per-category quick-create buttons for child categories and forums.
|
|
||||||
- Removed the legacy navbar and cleaned up related styling.
|
|
||||||
- Added ACP general settings for forum name, theme, accents, and logo (no reload required).
|
|
||||||
- Added admin-only upload endpoints and ACP UI for logos and favicons.
|
|
||||||
- Applied forum branding, theme defaults, accents, logos, and favicon links in the SPA header.
|
|
||||||
|
|||||||
@@ -19,22 +19,22 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
*/
|
*/
|
||||||
public function create(array $input): User
|
public function create(array $input): User
|
||||||
{
|
{
|
||||||
Validator::make($input, [
|
Validator::make(data: $input, rules: [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
'email',
|
'email',
|
||||||
'max:255',
|
'max:255',
|
||||||
Rule::unique(User::class),
|
Rule::unique(table: User::class),
|
||||||
],
|
],
|
||||||
'password' => $this->passwordRules(),
|
'password' => $this->passwordRules(),
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
return User::create([
|
return User::create(attributes: [
|
||||||
'name' => $input['name'],
|
'name' => $input['name'],
|
||||||
'email' => $input['email'],
|
'email' => $input['email'],
|
||||||
'password' => Hash::make($input['password']),
|
'password' => Hash::make(value: $input['password']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Fortify;
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Validation\Rules\Password;
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
trait PasswordValidationRules
|
trait PasswordValidationRules
|
||||||
@@ -9,7 +10,7 @@ trait PasswordValidationRules
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules used to validate passwords.
|
* Get the validation rules used to validate passwords.
|
||||||
*
|
*
|
||||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
* @return array<int, ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
protected function passwordRules(): array
|
protected function passwordRules(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
],
|
],
|
||||||
])->validateWithBag('updateProfileInformation');
|
])->validateWithBag('updateProfileInformation');
|
||||||
|
|
||||||
if ($input['email'] !== $user->email &&
|
if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) {
|
||||||
$user instanceof MustVerifyEmail) {
|
|
||||||
$this->updateVerifiedUser($user, $input);
|
$this->updateVerifiedUser($user, $input);
|
||||||
} else {
|
} else {
|
||||||
$user->forceFill([
|
$user->forceFill([
|
||||||
|
|||||||
@@ -50,4 +50,34 @@ class SettingController extends Controller
|
|||||||
'value' => $setting->value,
|
'value' => $setting->value,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function bulkStore(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
if (!$user || !$user->roles()->where('name', 'ROLE_ADMIN')->exists()) {
|
||||||
|
return response()->json(['message' => 'Forbidden'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'settings' => ['required', 'array'],
|
||||||
|
'settings.*.key' => ['required', 'string', 'max:191'],
|
||||||
|
'settings.*.value' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updated = [];
|
||||||
|
|
||||||
|
foreach ($data['settings'] as $entry) {
|
||||||
|
$setting = Setting::updateOrCreate(
|
||||||
|
['key' => $entry['key']],
|
||||||
|
['value' => $entry['value'] ?? '']
|
||||||
|
);
|
||||||
|
$updated[] = [
|
||||||
|
'id' => $setting->id,
|
||||||
|
'key' => $setting->key,
|
||||||
|
'value' => $setting->value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($updated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,38 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\DatabaseNotification;
|
||||||
|
use Illuminate\Notifications\DatabaseNotificationCollection;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $email
|
* @property string $email
|
||||||
* @property \Illuminate\Support\Carbon|null $email_verified_at
|
* @property Carbon|null $email_verified_at
|
||||||
* @property string $password
|
* @property string $password
|
||||||
* @property string|null $two_factor_secret
|
* @property string|null $two_factor_secret
|
||||||
* @property string|null $two_factor_recovery_codes
|
* @property string|null $two_factor_recovery_codes
|
||||||
* @property string|null $two_factor_confirmed_at
|
* @property string|null $two_factor_confirmed_at
|
||||||
* @property string|null $remember_token
|
* @property string|null $remember_token
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
|
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
|
||||||
* @property-read int|null $notifications_count
|
* @property-read int|null $notifications_count
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Role> $roles
|
* @property-read Collection<int, Role> $roles
|
||||||
* @property-read int|null $roles_count
|
* @property-read int|null $roles_count
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
|
* @property-read Collection<int, PersonalAccessToken> $tokens
|
||||||
* @property-read int|null $tokens_count
|
* @property-read int|null $tokens_count
|
||||||
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
* @method static UserFactory factory($count = null, $state = [])
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
||||||
@@ -46,8 +52,10 @@ use Laravel\Sanctum\HasApiTokens;
|
|||||||
*/
|
*/
|
||||||
class User extends Authenticatable implements MustVerifyEmail
|
class User extends Authenticatable implements MustVerifyEmail
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasApiTokens, HasFactory, Notifiable;
|
use HasApiTokens;
|
||||||
|
use HasFactory;
|
||||||
|
use Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class FortifyServiceProvider extends ServiceProvider
|
|||||||
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
|
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
|
||||||
|
|
||||||
RateLimiter::for('login', function (Request $request) {
|
RateLimiter::for('login', function (Request $request) {
|
||||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());
|
||||||
|
|
||||||
return Limit::perMinute(5)->by($throttleKey);
|
return Limit::perMinute(5)->by($throttleKey);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"laravel/fortify": "*",
|
"laravel/fortify": "*",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "*",
|
"laravel/sanctum": "*",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"ext-pdo": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-ide-helper": "^3.6",
|
"barryvdh/laravel-ide-helper": "^3.6",
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
"laravel/sail": "^1.41",
|
"laravel/sail": "^1.41",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"phpunit/phpunit": "^11.5.3"
|
"phpunit/phpunit": "^11.5.3",
|
||||||
|
"squizlabs/php_codesniffer": "^4.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"phpcs": "phpcs",
|
||||||
"setup": [
|
"setup": [
|
||||||
"composer install",
|
"composer install",
|
||||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
|||||||
81
composer.lock
generated
81
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "5b4bf74d07107b744033916f34a83be1",
|
"content-hash": "8e91d3287080a532070e38f61106f41c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -8827,6 +8827,85 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-10-09T05:16:32+00:00"
|
"time": "2024-10-09T05:16:32+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "squizlabs/php_codesniffer",
|
||||||
|
"version": "4.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
|
||||||
|
"reference": "0525c73950de35ded110cffafb9892946d7771b5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5",
|
||||||
|
"reference": "0525c73950de35ded110cffafb9892946d7771b5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"ext-tokenizer": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"php": ">=7.2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/phpcbf",
|
||||||
|
"bin/phpcs"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Greg Sherwood",
|
||||||
|
"role": "Former lead"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Juliette Reinders Folmer",
|
||||||
|
"role": "Current lead"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Contributors",
|
||||||
|
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.",
|
||||||
|
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
|
||||||
|
"keywords": [
|
||||||
|
"phpcs",
|
||||||
|
"standards",
|
||||||
|
"static analysis"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
|
||||||
|
"security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
|
||||||
|
"source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
|
||||||
|
"wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/PHPCSStandards",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/jrfnl",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://opencollective.com/php_codesniffer",
|
||||||
|
"type": "open_collective"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://thanks.dev/u/gh/phpcsstandards",
|
||||||
|
"type": "thanks_dev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-10T16:43:36+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "staabm/side-effects-detector",
|
"name": "staabm/side-effects-detector",
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
|
|||||||
@@ -112,6 +112,6 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-cache-'),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ return [
|
|||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
(PHP_VERSION_ID >= 80500 ?
|
||||||
|
\Pdo\Mysql::ATTR_SSL_CA :
|
||||||
|
\PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -79,7 +81,9 @@ return [
|
|||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
(PHP_VERSION_ID >= 80500 ?
|
||||||
|
\Pdo\Mysql::ATTR_SSL_CA :
|
||||||
|
\PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
]) : [],
|
]) : [],
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -148,7 +152,7 @@ return [
|
|||||||
|
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-database-'),
|
||||||
'persistent' => env('REDIS_PERSISTENT', false),
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ return [
|
|||||||
|
|
|
|
||||||
| Here you may specify the default filesystem disk that should be used
|
| Here you may specify the default filesystem disk that should be used
|
||||||
| by the framework. The "local" disk, as well as a variety of cloud
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
| based disks are available to your application for file storage.
|
| based disks, are available to your application for file storage.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ return [
|
|||||||
'public' => [
|
'public' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/public'),
|
'root' => storage_path('app/public'),
|
||||||
'url' => env('APP_URL').'/storage',
|
'url' => env('APP_URL') . '/storage',
|
||||||
'visibility' => 'public',
|
'visibility' => 'public',
|
||||||
'throw' => false,
|
'throw' => false,
|
||||||
'report' => false,
|
'report' => false,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ return [
|
|||||||
'handler_with' => [
|
'handler_with' => [
|
||||||
'host' => env('PAPERTRAIL_URL'),
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
'port' => env('PAPERTRAIL_PORT'),
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
|
||||||
],
|
],
|
||||||
'processors' => [PsrLogMessageProcessor::class],
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ return [
|
|||||||
'username' => env('MAIL_USERNAME'),
|
'username' => env('MAIL_USERNAME'),
|
||||||
'password' => env('MAIL_PASSWORD'),
|
'password' => env('MAIL_PASSWORD'),
|
||||||
'timeout' => null,
|
'timeout' => null,
|
||||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
'local_domain' => env(
|
||||||
|
'MAIL_EHLO_DOMAIN',
|
||||||
|
parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
'ses' => [
|
'ses' => [
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ return [
|
|||||||
|
|
||||||
'cookie' => env(
|
'cookie' => env(
|
||||||
'SESSION_COOKIE',
|
'SESSION_COOKIE',
|
||||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
Str::slug((string) env('APP_NAME', 'laravel')) . '-session'
|
||||||
),
|
),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
18
phpcs.xml
Normal file
18
phpcs.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="speedBB">
|
||||||
|
<description>Project coding standard based on PSR-12.</description>
|
||||||
|
|
||||||
|
<rule ref="PSR12"/>
|
||||||
|
|
||||||
|
<file>app</file>
|
||||||
|
<file>config</file>
|
||||||
|
<file>database</file>
|
||||||
|
<file>routes</file>
|
||||||
|
<file>tests</file>
|
||||||
|
|
||||||
|
<exclude-pattern>bootstrap/cache/*</exclude-pattern>
|
||||||
|
<exclude-pattern>node_modules/*</exclude-pattern>
|
||||||
|
<exclude-pattern>public/build/*</exclude-pattern>
|
||||||
|
<exclude-pattern>storage/*</exclude-pattern>
|
||||||
|
<exclude-pattern>vendor/*</exclude-pattern>
|
||||||
|
</ruleset>
|
||||||
@@ -11,7 +11,7 @@ import Acp from './pages/Acp'
|
|||||||
import BoardIndex from './pages/BoardIndex'
|
import BoardIndex from './pages/BoardIndex'
|
||||||
import Ucp from './pages/Ucp'
|
import Ucp from './pages/Ucp'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { fetchSetting, fetchVersion, getForum, getThread } from './api/client'
|
import { fetchSettings, fetchVersion, getForum, getThread } from './api/client'
|
||||||
|
|
||||||
function PortalHeader({ userMenu, isAuthenticated, forumName, logoUrl, showHeaderName }) {
|
function PortalHeader({ userMenu, isAuthenticated, forumName, logoUrl, showHeaderName }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -231,53 +231,24 @@ function AppShell() {
|
|||||||
let active = true
|
let active = true
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const [
|
const allSettings = await fetchSettings()
|
||||||
forumNameSetting,
|
const settingsMap = new Map(allSettings.map((setting) => [setting.key, setting.value]))
|
||||||
defaultThemeSetting,
|
|
||||||
accentDarkSetting,
|
|
||||||
accentLightSetting,
|
|
||||||
logoDarkSetting,
|
|
||||||
logoLightSetting,
|
|
||||||
showHeaderNameSetting,
|
|
||||||
faviconIcoSetting,
|
|
||||||
favicon16Setting,
|
|
||||||
favicon32Setting,
|
|
||||||
favicon48Setting,
|
|
||||||
favicon64Setting,
|
|
||||||
favicon128Setting,
|
|
||||||
favicon256Setting,
|
|
||||||
] = await Promise.all([
|
|
||||||
fetchSetting('forum_name'),
|
|
||||||
fetchSetting('default_theme'),
|
|
||||||
fetchSetting('accent_color_dark'),
|
|
||||||
fetchSetting('accent_color_light'),
|
|
||||||
fetchSetting('logo_dark'),
|
|
||||||
fetchSetting('logo_light'),
|
|
||||||
fetchSetting('show_header_name'),
|
|
||||||
fetchSetting('favicon_ico'),
|
|
||||||
fetchSetting('favicon_16'),
|
|
||||||
fetchSetting('favicon_32'),
|
|
||||||
fetchSetting('favicon_48'),
|
|
||||||
fetchSetting('favicon_64'),
|
|
||||||
fetchSetting('favicon_128'),
|
|
||||||
fetchSetting('favicon_256'),
|
|
||||||
])
|
|
||||||
if (!active) return
|
if (!active) return
|
||||||
const next = {
|
const next = {
|
||||||
forumName: forumNameSetting?.value || '',
|
forumName: settingsMap.get('forum_name') || '',
|
||||||
defaultTheme: defaultThemeSetting?.value || 'auto',
|
defaultTheme: settingsMap.get('default_theme') || 'auto',
|
||||||
accentDark: accentDarkSetting?.value || '',
|
accentDark: settingsMap.get('accent_color_dark') || '',
|
||||||
accentLight: accentLightSetting?.value || '',
|
accentLight: settingsMap.get('accent_color_light') || '',
|
||||||
logoDark: logoDarkSetting?.value || '',
|
logoDark: settingsMap.get('logo_dark') || '',
|
||||||
logoLight: logoLightSetting?.value || '',
|
logoLight: settingsMap.get('logo_light') || '',
|
||||||
showHeaderName: showHeaderNameSetting?.value !== 'false',
|
showHeaderName: settingsMap.get('show_header_name') !== 'false',
|
||||||
faviconIco: faviconIcoSetting?.value || '',
|
faviconIco: settingsMap.get('favicon_ico') || '',
|
||||||
favicon16: favicon16Setting?.value || '',
|
favicon16: settingsMap.get('favicon_16') || '',
|
||||||
favicon32: favicon32Setting?.value || '',
|
favicon32: settingsMap.get('favicon_32') || '',
|
||||||
favicon48: favicon48Setting?.value || '',
|
favicon48: settingsMap.get('favicon_48') || '',
|
||||||
favicon64: favicon64Setting?.value || '',
|
favicon64: settingsMap.get('favicon_64') || '',
|
||||||
favicon128: favicon128Setting?.value || '',
|
favicon128: settingsMap.get('favicon_128') || '',
|
||||||
favicon256: favicon256Setting?.value || '',
|
favicon256: settingsMap.get('favicon_256') || '',
|
||||||
}
|
}
|
||||||
setSettings(next)
|
setSettings(next)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export async function fetchVersion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSetting(key) {
|
export async function fetchSetting(key) {
|
||||||
|
// TODO: Prefer fetchSettings() when multiple settings are needed.
|
||||||
const cacheBust = Date.now()
|
const cacheBust = Date.now()
|
||||||
const data = await apiFetch(
|
const data = await apiFetch(
|
||||||
`/settings?key=${encodeURIComponent(key)}&pagination=false&_=${cacheBust}`,
|
`/settings?key=${encodeURIComponent(key)}&pagination=false&_=${cacheBust}`,
|
||||||
@@ -86,6 +87,15 @@ export async function fetchSetting(key) {
|
|||||||
return data?.['hydra:member']?.[0] || null
|
return data?.['hydra:member']?.[0] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSettings() {
|
||||||
|
const cacheBust = Date.now()
|
||||||
|
const data = await apiFetch(`/settings?pagination=false&_=${cacheBust}`, { cache: 'no-store' })
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return data?.['hydra:member'] || []
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveSetting(key, value) {
|
export async function saveSetting(key, value) {
|
||||||
return apiFetch('/settings', {
|
return apiFetch('/settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -93,6 +103,13 @@ export async function saveSetting(key, value) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveSettings(settings) {
|
||||||
|
return apiFetch('/settings/bulk', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ settings }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadLogo(file) {
|
export async function uploadLogo(file) {
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
body.append('file', file)
|
body.append('file', file)
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { useDropzone } from 'react-dropzone'
|
|||||||
import {
|
import {
|
||||||
createForum,
|
createForum,
|
||||||
deleteForum,
|
deleteForum,
|
||||||
fetchSetting,
|
fetchSettings,
|
||||||
listAllForums,
|
listAllForums,
|
||||||
listUsers,
|
listUsers,
|
||||||
reorderForums,
|
reorderForums,
|
||||||
saveSetting,
|
saveSetting,
|
||||||
|
saveSettings,
|
||||||
uploadFavicon,
|
uploadFavicon,
|
||||||
uploadLogo,
|
uploadLogo,
|
||||||
updateForum,
|
updateForum,
|
||||||
@@ -35,21 +36,37 @@ export default function Acp({ isAdmin }) {
|
|||||||
const [generalUploading, setGeneralUploading] = useState(false)
|
const [generalUploading, setGeneralUploading] = useState(false)
|
||||||
const [generalError, setGeneralError] = useState('')
|
const [generalError, setGeneralError] = useState('')
|
||||||
const [generalSettings, setGeneralSettings] = useState({
|
const [generalSettings, setGeneralSettings] = useState({
|
||||||
forumName: '',
|
forum_name: '',
|
||||||
defaultTheme: 'auto',
|
default_theme: 'auto',
|
||||||
darkAccent: '',
|
accent_color_dark: '',
|
||||||
lightAccent: '',
|
accent_color_light: '',
|
||||||
darkLogo: '',
|
logo_dark: '',
|
||||||
lightLogo: '',
|
logo_light: '',
|
||||||
showHeaderName: true,
|
show_header_name: 'true',
|
||||||
faviconIco: '',
|
favicon_ico: '',
|
||||||
favicon16: '',
|
favicon_16: '',
|
||||||
favicon32: '',
|
favicon_32: '',
|
||||||
favicon48: '',
|
favicon_48: '',
|
||||||
favicon64: '',
|
favicon_64: '',
|
||||||
favicon128: '',
|
favicon_128: '',
|
||||||
favicon256: '',
|
favicon_256: '',
|
||||||
})
|
})
|
||||||
|
const settingsDetailMap = {
|
||||||
|
forum_name: 'forumName',
|
||||||
|
default_theme: 'defaultTheme',
|
||||||
|
accent_color_dark: 'accentDark',
|
||||||
|
accent_color_light: 'accentLight',
|
||||||
|
logo_dark: 'logoDark',
|
||||||
|
logo_light: 'logoLight',
|
||||||
|
show_header_name: 'showHeaderName',
|
||||||
|
favicon_ico: 'faviconIco',
|
||||||
|
favicon_16: 'favicon16',
|
||||||
|
favicon_32: 'favicon32',
|
||||||
|
favicon_48: 'favicon48',
|
||||||
|
favicon_64: 'favicon64',
|
||||||
|
favicon_128: 'favicon128',
|
||||||
|
favicon_256: 'favicon256',
|
||||||
|
}
|
||||||
const [themeMode, setThemeMode] = useState(
|
const [themeMode, setThemeMode] = useState(
|
||||||
document.documentElement.getAttribute('data-bs-theme') || 'light'
|
document.documentElement.getAttribute('data-bs-theme') || 'light'
|
||||||
)
|
)
|
||||||
@@ -105,39 +122,24 @@ export default function Acp({ isAdmin }) {
|
|||||||
let active = true
|
let active = true
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const keys = [
|
const allSettings = await fetchSettings()
|
||||||
'forum_name',
|
const settingsMap = new Map(allSettings.map((setting) => [setting.key, setting.value]))
|
||||||
'default_theme',
|
|
||||||
'accent_color_dark',
|
|
||||||
'accent_color_light',
|
|
||||||
'logo_dark',
|
|
||||||
'logo_light',
|
|
||||||
'show_header_name',
|
|
||||||
'favicon_ico',
|
|
||||||
'favicon_16',
|
|
||||||
'favicon_32',
|
|
||||||
'favicon_48',
|
|
||||||
'favicon_64',
|
|
||||||
'favicon_128',
|
|
||||||
'favicon_256',
|
|
||||||
]
|
|
||||||
const results = await Promise.all(keys.map((key) => fetchSetting(key)))
|
|
||||||
if (!active) return
|
if (!active) return
|
||||||
const next = {
|
const next = {
|
||||||
forumName: results[0]?.value || '',
|
forum_name: settingsMap.get('forum_name') || '',
|
||||||
defaultTheme: results[1]?.value || 'auto',
|
default_theme: settingsMap.get('default_theme') || 'auto',
|
||||||
darkAccent: results[2]?.value || '',
|
accent_color_dark: settingsMap.get('accent_color_dark') || '',
|
||||||
lightAccent: results[3]?.value || '',
|
accent_color_light: settingsMap.get('accent_color_light') || '',
|
||||||
darkLogo: results[4]?.value || '',
|
logo_dark: settingsMap.get('logo_dark') || '',
|
||||||
lightLogo: results[5]?.value || '',
|
logo_light: settingsMap.get('logo_light') || '',
|
||||||
showHeaderName: results[6]?.value !== 'false',
|
show_header_name: settingsMap.get('show_header_name') || 'true',
|
||||||
faviconIco: results[7]?.value || '',
|
favicon_ico: settingsMap.get('favicon_ico') || '',
|
||||||
favicon16: results[8]?.value || '',
|
favicon_16: settingsMap.get('favicon_16') || '',
|
||||||
favicon32: results[9]?.value || '',
|
favicon_32: settingsMap.get('favicon_32') || '',
|
||||||
favicon48: results[10]?.value || '',
|
favicon_48: settingsMap.get('favicon_48') || '',
|
||||||
favicon64: results[11]?.value || '',
|
favicon_64: settingsMap.get('favicon_64') || '',
|
||||||
favicon128: results[12]?.value || '',
|
favicon_128: settingsMap.get('favicon_128') || '',
|
||||||
favicon256: results[13]?.value || '',
|
favicon_256: settingsMap.get('favicon_256') || '',
|
||||||
}
|
}
|
||||||
setGeneralSettings(next)
|
setGeneralSettings(next)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -155,42 +157,23 @@ export default function Acp({ isAdmin }) {
|
|||||||
setGeneralSaving(true)
|
setGeneralSaving(true)
|
||||||
setGeneralError('')
|
setGeneralError('')
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await saveSettings(
|
||||||
saveSetting('forum_name', generalSettings.forumName.trim() || ''),
|
Object.entries(generalSettings).map(([key, value]) => ({
|
||||||
saveSetting('default_theme', generalSettings.defaultTheme || 'auto'),
|
key,
|
||||||
saveSetting('accent_color_dark', generalSettings.darkAccent.trim() || ''),
|
value: typeof value === 'string' ? value.trim() : String(value ?? ''),
|
||||||
saveSetting('accent_color_light', generalSettings.lightAccent.trim() || ''),
|
}))
|
||||||
saveSetting('logo_dark', generalSettings.darkLogo.trim() || ''),
|
|
||||||
saveSetting('logo_light', generalSettings.lightLogo.trim() || ''),
|
|
||||||
saveSetting('show_header_name', generalSettings.showHeaderName ? 'true' : 'false'),
|
|
||||||
saveSetting('favicon_ico', generalSettings.faviconIco.trim() || ''),
|
|
||||||
saveSetting('favicon_16', generalSettings.favicon16.trim() || ''),
|
|
||||||
saveSetting('favicon_32', generalSettings.favicon32.trim() || ''),
|
|
||||||
saveSetting('favicon_48', generalSettings.favicon48.trim() || ''),
|
|
||||||
saveSetting('favicon_64', generalSettings.favicon64.trim() || ''),
|
|
||||||
saveSetting('favicon_128', generalSettings.favicon128.trim() || ''),
|
|
||||||
saveSetting('favicon_256', generalSettings.favicon256.trim() || ''),
|
|
||||||
])
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('speedbb-settings-updated', {
|
|
||||||
detail: {
|
|
||||||
forumName: generalSettings.forumName.trim() || '',
|
|
||||||
defaultTheme: generalSettings.defaultTheme || 'auto',
|
|
||||||
accentDark: generalSettings.darkAccent.trim() || '',
|
|
||||||
accentLight: generalSettings.lightAccent.trim() || '',
|
|
||||||
logoDark: generalSettings.darkLogo.trim() || '',
|
|
||||||
logoLight: generalSettings.lightLogo.trim() || '',
|
|
||||||
showHeaderName: generalSettings.showHeaderName,
|
|
||||||
faviconIco: generalSettings.faviconIco.trim() || '',
|
|
||||||
favicon16: generalSettings.favicon16.trim() || '',
|
|
||||||
favicon32: generalSettings.favicon32.trim() || '',
|
|
||||||
favicon48: generalSettings.favicon48.trim() || '',
|
|
||||||
favicon64: generalSettings.favicon64.trim() || '',
|
|
||||||
favicon128: generalSettings.favicon128.trim() || '',
|
|
||||||
favicon256: generalSettings.favicon256.trim() || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
const detail = Object.entries(generalSettings).reduce((acc, [key, value]) => {
|
||||||
|
const mappedKey = settingsDetailMap[key]
|
||||||
|
if (!mappedKey) return acc
|
||||||
|
if (key === 'show_header_name') {
|
||||||
|
acc[mappedKey] = value !== 'false'
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
acc[mappedKey] = typeof value === 'string' ? value.trim() : String(value ?? '')
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
window.dispatchEvent(new CustomEvent('speedbb-settings-updated', { detail }))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setGeneralError(err.message)
|
setGeneralError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -199,8 +182,8 @@ export default function Acp({ isAdmin }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDefaultThemeChange = async (value) => {
|
const handleDefaultThemeChange = async (value) => {
|
||||||
const previous = generalSettings.defaultTheme
|
const previous = generalSettings.default_theme
|
||||||
setGeneralSettings((prev) => ({ ...prev, defaultTheme: value }))
|
setGeneralSettings((prev) => ({ ...prev, default_theme: value }))
|
||||||
setGeneralError('')
|
setGeneralError('')
|
||||||
try {
|
try {
|
||||||
await saveSetting('default_theme', value)
|
await saveSetting('default_theme', value)
|
||||||
@@ -210,22 +193,29 @@ export default function Acp({ isAdmin }) {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setGeneralSettings((prev) => ({ ...prev, defaultTheme: previous }))
|
setGeneralSettings((prev) => ({ ...prev, default_theme: previous }))
|
||||||
setGeneralError(err.message)
|
setGeneralError(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogoUpload = async (file, variantKey) => {
|
const handleLogoUpload = async (file, settingKey) => {
|
||||||
if (!file) return
|
if (!file) return
|
||||||
setGeneralUploading(true)
|
setGeneralUploading(true)
|
||||||
setGeneralError('')
|
setGeneralError('')
|
||||||
try {
|
try {
|
||||||
const result = await uploadLogo(file)
|
const result = await uploadLogo(file)
|
||||||
const url = result?.url || ''
|
const url = result?.url || ''
|
||||||
const settingKey = variantKey === 'darkLogo' ? 'logo_dark' : 'logo_light'
|
setGeneralSettings((prev) => ({ ...prev, [settingKey]: url }))
|
||||||
setGeneralSettings((prev) => ({ ...prev, [variantKey]: url }))
|
|
||||||
if (url) {
|
if (url) {
|
||||||
await saveSetting(settingKey, url)
|
await saveSetting(settingKey, url)
|
||||||
|
const mappedKey = settingsDetailMap[settingKey]
|
||||||
|
if (mappedKey) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('speedbb-settings-updated', {
|
||||||
|
detail: { [mappedKey]: url },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setGeneralError(err.message)
|
setGeneralError(err.message)
|
||||||
@@ -234,21 +224,24 @@ export default function Acp({ isAdmin }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFaviconUpload = async (file, settingKey, stateKey) => {
|
const handleFaviconUpload = async (file, settingKey) => {
|
||||||
if (!file) return
|
if (!file) return
|
||||||
setGeneralUploading(true)
|
setGeneralUploading(true)
|
||||||
setGeneralError('')
|
setGeneralError('')
|
||||||
try {
|
try {
|
||||||
const result = await uploadFavicon(file)
|
const result = await uploadFavicon(file)
|
||||||
const url = result?.url || ''
|
const url = result?.url || ''
|
||||||
setGeneralSettings((prev) => ({ ...prev, [stateKey]: url }))
|
setGeneralSettings((prev) => ({ ...prev, [settingKey]: url }))
|
||||||
if (url) {
|
if (url) {
|
||||||
await saveSetting(settingKey, url)
|
await saveSetting(settingKey, url)
|
||||||
window.dispatchEvent(
|
const mappedKey = settingsDetailMap[settingKey]
|
||||||
new CustomEvent('speedbb-settings-updated', {
|
if (mappedKey) {
|
||||||
detail: { [stateKey]: url },
|
window.dispatchEvent(
|
||||||
})
|
new CustomEvent('speedbb-settings-updated', {
|
||||||
)
|
detail: { [mappedKey]: url },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setGeneralError(err.message)
|
setGeneralError(err.message)
|
||||||
@@ -263,7 +256,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_ico', 'faviconIco'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_ico'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const favicon16Dropzone = useDropzone({
|
const favicon16Dropzone = useDropzone({
|
||||||
@@ -272,7 +265,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_16', 'favicon16'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_16'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const favicon32Dropzone = useDropzone({
|
const favicon32Dropzone = useDropzone({
|
||||||
@@ -281,7 +274,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_32', 'favicon32'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_32'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const favicon48Dropzone = useDropzone({
|
const favicon48Dropzone = useDropzone({
|
||||||
@@ -290,7 +283,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_48', 'favicon48'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_48'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const favicon64Dropzone = useDropzone({
|
const favicon64Dropzone = useDropzone({
|
||||||
@@ -299,7 +292,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_64', 'favicon64'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_64'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const favicon128Dropzone = useDropzone({
|
const favicon128Dropzone = useDropzone({
|
||||||
@@ -308,7 +301,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_128', 'favicon128'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_128'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const favicon256Dropzone = useDropzone({
|
const favicon256Dropzone = useDropzone({
|
||||||
@@ -317,7 +310,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/x-icon': ['.ico'],
|
'image/x-icon': ['.ico'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_256', 'favicon256'),
|
onDrop: (files) => handleFaviconUpload(files[0], 'favicon_256'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const darkLogoDropzone = useDropzone({
|
const darkLogoDropzone = useDropzone({
|
||||||
@@ -325,7 +318,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
|
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleLogoUpload(files[0], 'darkLogo'),
|
onDrop: (files) => handleLogoUpload(files[0], 'logo_dark'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const lightLogoDropzone = useDropzone({
|
const lightLogoDropzone = useDropzone({
|
||||||
@@ -333,7 +326,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
|
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
|
||||||
},
|
},
|
||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
onDrop: (files) => handleLogoUpload(files[0], 'lightLogo'),
|
onDrop: (files) => handleLogoUpload(files[0], 'logo_light'),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -970,9 +963,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.forum_name')}</Form.Label>
|
<Form.Label>{t('acp.forum_name')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="text"
|
type="text"
|
||||||
value={generalSettings.forumName}
|
value={generalSettings.forum_name}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGeneralSettings((prev) => ({ ...prev, forumName: event.target.value }))
|
setGeneralSettings((prev) => ({ ...prev, forum_name: event.target.value }))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -981,11 +974,11 @@ export default function Acp({ isAdmin }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="acp-show-header-name"
|
id="acp-show-header-name"
|
||||||
label={t('acp.show_header_name')}
|
label={t('acp.show_header_name')}
|
||||||
checked={generalSettings.showHeaderName}
|
checked={generalSettings.show_header_name !== 'false'}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGeneralSettings((prev) => ({
|
setGeneralSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
showHeaderName: event.target.checked,
|
show_header_name: event.target.checked ? 'true' : 'false',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -995,7 +988,7 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Label>{t('acp.default_theme')}</Form.Label>
|
<Form.Label>{t('acp.default_theme')}</Form.Label>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
value={generalSettings.defaultTheme}
|
value={generalSettings.default_theme}
|
||||||
onChange={(event) => handleDefaultThemeChange(event.target.value)}
|
onChange={(event) => handleDefaultThemeChange(event.target.value)}
|
||||||
>
|
>
|
||||||
<option value="auto">{t('ucp.system_default')}</option>
|
<option value="auto">{t('ucp.system_default')}</option>
|
||||||
@@ -1010,22 +1003,22 @@ export default function Acp({ isAdmin }) {
|
|||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="text"
|
type="text"
|
||||||
value={generalSettings.darkAccent}
|
value={generalSettings.accent_color_dark}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGeneralSettings((prev) => ({
|
setGeneralSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
darkAccent: event.target.value,
|
accent_color_dark: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder="#f29b3f"
|
placeholder="#f29b3f"
|
||||||
/>
|
/>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="color"
|
type="color"
|
||||||
value={generalSettings.darkAccent || '#f29b3f'}
|
value={generalSettings.accent_color_dark || '#f29b3f'}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGeneralSettings((prev) => ({
|
setGeneralSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
darkAccent: event.target.value,
|
accent_color_dark: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -1038,22 +1031,22 @@ export default function Acp({ isAdmin }) {
|
|||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="text"
|
type="text"
|
||||||
value={generalSettings.lightAccent}
|
value={generalSettings.accent_color_light}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGeneralSettings((prev) => ({
|
setGeneralSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lightAccent: event.target.value,
|
accent_color_light: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder="#f29b3f"
|
placeholder="#f29b3f"
|
||||||
/>
|
/>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="color"
|
type="color"
|
||||||
value={generalSettings.lightAccent || '#f29b3f'}
|
value={generalSettings.accent_color_light || '#f29b3f'}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setGeneralSettings((prev) => ({
|
setGeneralSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lightAccent: event.target.value,
|
accent_color_light: event.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -1069,9 +1062,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<input {...darkLogoDropzone.getInputProps()} />
|
<input {...darkLogoDropzone.getInputProps()} />
|
||||||
{generalSettings.darkLogo ? (
|
{generalSettings.logo_dark ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.darkLogo} alt={t('acp.logo_dark')} />
|
<img src={generalSettings.logo_dark} alt={t('acp.logo_dark')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1091,9 +1084,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<input {...lightLogoDropzone.getInputProps()} />
|
<input {...lightLogoDropzone.getInputProps()} />
|
||||||
{generalSettings.lightLogo ? (
|
{generalSettings.logo_light ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.lightLogo} alt={t('acp.logo_light')} />
|
<img src={generalSettings.logo_light} alt={t('acp.logo_light')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1115,9 +1108,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_ico')}</Form.Label>
|
<Form.Label>{t('acp.favicon_ico')}</Form.Label>
|
||||||
<div {...faviconIcoDropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...faviconIcoDropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...faviconIcoDropzone.getInputProps()} />
|
<input {...faviconIcoDropzone.getInputProps()} />
|
||||||
{generalSettings.faviconIco ? (
|
{generalSettings.favicon_ico ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.faviconIco} alt={t('acp.favicon_ico')} />
|
<img src={generalSettings.favicon_ico} alt={t('acp.favicon_ico')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1133,9 +1126,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_16')}</Form.Label>
|
<Form.Label>{t('acp.favicon_16')}</Form.Label>
|
||||||
<div {...favicon16Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...favicon16Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...favicon16Dropzone.getInputProps()} />
|
<input {...favicon16Dropzone.getInputProps()} />
|
||||||
{generalSettings.favicon16 ? (
|
{generalSettings.favicon_16 ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.favicon16} alt={t('acp.favicon_16')} />
|
<img src={generalSettings.favicon_16} alt={t('acp.favicon_16')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1151,9 +1144,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_32')}</Form.Label>
|
<Form.Label>{t('acp.favicon_32')}</Form.Label>
|
||||||
<div {...favicon32Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...favicon32Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...favicon32Dropzone.getInputProps()} />
|
<input {...favicon32Dropzone.getInputProps()} />
|
||||||
{generalSettings.favicon32 ? (
|
{generalSettings.favicon_32 ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.favicon32} alt={t('acp.favicon_32')} />
|
<img src={generalSettings.favicon_32} alt={t('acp.favicon_32')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1169,9 +1162,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_48')}</Form.Label>
|
<Form.Label>{t('acp.favicon_48')}</Form.Label>
|
||||||
<div {...favicon48Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...favicon48Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...favicon48Dropzone.getInputProps()} />
|
<input {...favicon48Dropzone.getInputProps()} />
|
||||||
{generalSettings.favicon48 ? (
|
{generalSettings.favicon_48 ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.favicon48} alt={t('acp.favicon_48')} />
|
<img src={generalSettings.favicon_48} alt={t('acp.favicon_48')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1187,9 +1180,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_64')}</Form.Label>
|
<Form.Label>{t('acp.favicon_64')}</Form.Label>
|
||||||
<div {...favicon64Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...favicon64Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...favicon64Dropzone.getInputProps()} />
|
<input {...favicon64Dropzone.getInputProps()} />
|
||||||
{generalSettings.favicon64 ? (
|
{generalSettings.favicon_64 ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.favicon64} alt={t('acp.favicon_64')} />
|
<img src={generalSettings.favicon_64} alt={t('acp.favicon_64')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1205,9 +1198,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_128')}</Form.Label>
|
<Form.Label>{t('acp.favicon_128')}</Form.Label>
|
||||||
<div {...favicon128Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...favicon128Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...favicon128Dropzone.getInputProps()} />
|
<input {...favicon128Dropzone.getInputProps()} />
|
||||||
{generalSettings.favicon128 ? (
|
{generalSettings.favicon_128 ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.favicon128} alt={t('acp.favicon_128')} />
|
<img src={generalSettings.favicon_128} alt={t('acp.favicon_128')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
@@ -1223,9 +1216,9 @@ export default function Acp({ isAdmin }) {
|
|||||||
<Form.Label>{t('acp.favicon_256')}</Form.Label>
|
<Form.Label>{t('acp.favicon_256')}</Form.Label>
|
||||||
<div {...favicon256Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
<div {...favicon256Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||||
<input {...favicon256Dropzone.getInputProps()} />
|
<input {...favicon256Dropzone.getInputProps()} />
|
||||||
{generalSettings.favicon256 ? (
|
{generalSettings.favicon_256 ? (
|
||||||
<div className="bb-dropzone-preview">
|
<div className="bb-dropzone-preview">
|
||||||
<img src={generalSettings.favicon256} alt={t('acp.favicon_256')} />
|
<img src={generalSettings.favicon_256} alt={t('acp.favicon_256')} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bb-dropzone-placeholder">
|
<div className="bb-dropzone-placeholder">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanc
|
|||||||
Route::get('/version', VersionController::class);
|
Route::get('/version', VersionController::class);
|
||||||
Route::get('/settings', [SettingController::class, 'index']);
|
Route::get('/settings', [SettingController::class, 'index']);
|
||||||
Route::post('/settings', [SettingController::class, 'store'])->middleware('auth:sanctum');
|
Route::post('/settings', [SettingController::class, 'store'])->middleware('auth:sanctum');
|
||||||
|
Route::post('/settings/bulk', [SettingController::class, 'bulkStore'])->middleware('auth:sanctum');
|
||||||
Route::get('/user-settings', [UserSettingController::class, 'index'])->middleware('auth:sanctum');
|
Route::get('/user-settings', [UserSettingController::class, 'index'])->middleware('auth:sanctum');
|
||||||
Route::post('/user-settings', [UserSettingController::class, 'store'])->middleware('auth:sanctum');
|
Route::post('/user-settings', [UserSettingController::class, 'store'])->middleware('auth:sanctum');
|
||||||
Route::post('/uploads/logo', [UploadController::class, 'storeLogo'])->middleware('auth:sanctum');
|
Route::post('/uploads/logo', [UploadController::class, 'storeLogo'])->middleware('auth:sanctum');
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*/
|
|
||||||
public function test_the_application_returns_a_successful_response(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*/
|
|
||||||
public function test_that_true_is_true(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user