From 25ec25e08979b63634fe1d8419cfca82426a300f Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:28:43 +0330 Subject: [PATCH 01/22] feat(admin): create product resource and related entities - Add ProductResource with form and table definitions - Create Product model with necessary attributes and relationships - Generate ProductFactory for creating test data - Implement migration for products table - Add pages for listing, creating, and editing products Signed-off-by: Bahman Jafarzadeh --- .../Filament/Resources/ProductResource.php | 249 ++++++++++++++++++ .../ProductResource/Pages/CreateProduct.php | 13 + .../ProductResource/Pages/EditProduct.php | 21 ++ .../ProductResource/Pages/ListProducts.php | 21 ++ admin/app/Models/Product.php | 153 +++++++++++ admin/database/factories/ProductFactory.php | 70 +++++ ...025_08_08_221913_create_products_table.php | 60 +++++ 7 files changed, 587 insertions(+) create mode 100644 admin/app/Filament/Resources/ProductResource.php create mode 100644 admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php create mode 100644 admin/app/Filament/Resources/ProductResource/Pages/EditProduct.php create mode 100644 admin/app/Filament/Resources/ProductResource/Pages/ListProducts.php create mode 100644 admin/app/Models/Product.php create mode 100644 admin/database/factories/ProductFactory.php create mode 100644 admin/database/migrations/2025_08_08_221913_create_products_table.php diff --git a/admin/app/Filament/Resources/ProductResource.php b/admin/app/Filament/Resources/ProductResource.php new file mode 100644 index 00000000..57e44fbd --- /dev/null +++ b/admin/app/Filament/Resources/ProductResource.php @@ -0,0 +1,249 @@ +schema([ + Forms\Components\TextInput::make('heading') + ->required() + ->live(onBlur: true) + ->maxLength(255) + ->afterStateUpdated(function (string $operation, ?string $state, Forms\Set $set): void { + if ($operation === 'create') { + $set('slug', Str::slug($state)); + } + }), + Forms\Components\TextInput::make('slug') + ->disabled() + ->dehydrated() + ->required() + ->maxLength(255) + ->unique(Product::class, 'slug', ignoreRecord: true), + Forms\Components\TextInput::make('price') + ->required() + ->numeric() + ->prefix('تومان'), + TiptapEditor::make('content') + ->output(TiptapOutput::Html) + ->columnSpanFull() + ->extraInputAttributes(['style' => 'min-height: 12rem;']) + ->required(), + Forms\Components\TextInput::make('title') + ->maxLength(255), + Forms\Components\Textarea::make('description') + ->maxLength(255) + ->columnSpanFull(), + Forms\Components\Toggle::make('no_index') + ->required(), + Forms\Components\TextInput::make('canonical') + ->maxLength(255), + Forms\Components\Repeater::make('images') + ->relationship('images') // link to morphMany + ->schema([ + Forms\Components\FileUpload::make('path') + ->nullable() + ->columns(1) + ->columnSpanFull(), + + Forms\Components\Toggle::make('is_featured') + ->label('Featured Image') + ->reactive(), + + Forms\Components\TextInput::make('alt_text') + ->label('Alt Text'), + ]) + ->columnSpanFull(), + + Forms\Components\Select::make('attribute_group_id') + ->relationship('attributeGroup', 'name') + ->native(false) + ->preload(), + Forms\Components\Select::make('category_id') + ->relationship('category', 'heading') + ->required() + ->native(false) + ->preload(), + Forms\Components\Select::make('brand_id') + ->relationship('brand', 'heading') + ->required() + ->native(false) + ->preload(), + Forms\Components\TextInput::make('minimum') + ->required() + ->numeric() + ->default(1), + Forms\Components\TextInput::make('maximum') + ->numeric(), + Forms\Components\TextInput::make('step') + ->required() + ->numeric() + ->default(1), + Forms\Components\TextInput::make('profit_percent') + ->required() + ->numeric() + ->suffix('%') + ->default(0), + Forms\Components\TextInput::make('attributes') + ->nullable(), + Forms\Components\TextInput::make('highlight') + ->nullable(), + Forms\Components\Toggle::make('has_stock') + ->required(), + Forms\Components\TextInput::make('variety_counts') + ->required() + ->numeric() + ->default(0), + Forms\Components\TextInput::make('weight') + ->numeric() + ->nullable(), + Forms\Components\TextInput::make('length') + ->numeric() + ->nullable(), + Forms\Components\TextInput::make('width') + ->numeric() + ->nullable(), + Forms\Components\TextInput::make('height') + ->numeric() + ->nullable(), + Forms\Components\Select::make('status') + ->required() + ->options(ProductStatusEnum::options()) + ->default(ProductStatusEnum::DRAFT->value), + Forms\Components\TextInput::make('seen') + ->required() + ->numeric() + ->default(0), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('heading') + ->limit(30) + ->wrap(), + Tables\Columns\TextColumn::make('slug') + ->limit(30) + ->wrap(), + Tables\Columns\TextColumn::make('price') + ->money() + ->sortable(), + Tables\Columns\TextColumn::make('title') + ->searchable(), + Tables\Columns\IconColumn::make('no_index') + ->boolean(), + Tables\Columns\TextColumn::make('canonical') + ->searchable(), + + Tables\Columns\ImageColumn::make('featuredImage.path') + ->label('Featured') + ->square(), + + Tables\Columns\TextColumn::make('attributeGroup.name') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('category.title') + ->numeric() + ->limit(60), + Tables\Columns\TextColumn::make('brand.title') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('minimum') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('maximum') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('step') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('profit_percent') + ->numeric() + ->sortable(), + Tables\Columns\IconColumn::make('has_stock') + ->boolean(), + Tables\Columns\TextColumn::make('variety_counts') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('weight') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('length') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('width') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('height') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('status') + ->getStateUsing(fn (Product $record) => $record->status->label()) + ->color(fn (Product $record): string => $record->status->color()) + ->sortable(), + Tables\Columns\TextColumn::make('seen') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListProducts::route('/'), + 'create' => Pages\CreateProduct::route('/create'), + 'edit' => Pages\EditProduct::route('/{record}/edit'), + ]; + } +} diff --git a/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php b/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php new file mode 100644 index 00000000..2aae6a31 --- /dev/null +++ b/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php @@ -0,0 +1,13 @@ + $images + * @property AttributeGroup $attributeGroup + * @property Category $category + * @property Brand $brand + */ +class Product extends Model +{ + /** @use HasFactory */ + use HasFactory; + + public const string DEFAULT_IMAGE_PATH = '/images/product.jpeg'; + + public const string DEFAULT_ALT_TEXT = 'Shape your perfect store'; + + protected $fillable = [ + 'heading', + 'slug', + 'price', + 'content', + 'title', + 'description', + 'no_index', + 'canonical', + 'image_id', + 'attribute_group_id', + 'category_id', + 'brand_id', + 'minimum', + 'maximum', + 'step', + 'profit_percent', + 'attributes', + 'highlight', + 'has_stock', + 'variety_counts', + 'weight', + 'length', + 'width', + 'height', + 'status', + 'seen', + ]; + + protected $casts = [ + 'no_index' => 'boolean', + 'has_stock' => 'boolean', + 'attributes' => 'array', + 'highlight' => 'array', + 'minimum' => 'integer', + 'maximum' => 'integer', + 'step' => 'integer', + 'variety_counts' => 'integer', + 'seen' => 'integer', + 'status' => ProductStatusEnum::class, + ]; + + public function scopePublished($query) + { + return $query->where('status', ProductStatusEnum::PUBLISHED->value); + } + + public function scopeDraft($query) + { + return $query->where('status', ProductStatusEnum::DRAFT->value); + } + + public function scopeDeleted($query) + { + return $query->where('status', ProductStatusEnum::DELETED->value); + } + + public function images(): MorphMany + { + return $this->morphMany(Image::class, 'imageable'); + } + + public function featuredImage(): HasOne + { + return $this->hasOne(Image::class, 'imageable_id') + ->where('imageable_type', self::class) + ->where('is_featured', true); + } + + protected function getDefaultImageModel(): Image + { + return new Image([ + 'path' => self::DEFAULT_IMAGE_PATH, + 'alt_text' => self::DEFAULT_ALT_TEXT . $this->heading, + 'is_featured' => true, + 'order' => 0, + 'is_default' => true, + ]); + } + + public function attributeGroup(): BelongsTo + { + return $this->belongsTo(AttributeGroup::class); + } + + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + public function brand(): BelongsTo + { + return $this->belongsTo(Brand::class); + } +} diff --git a/admin/database/factories/ProductFactory.php b/admin/database/factories/ProductFactory.php new file mode 100644 index 00000000..cd1dfe49 --- /dev/null +++ b/admin/database/factories/ProductFactory.php @@ -0,0 +1,70 @@ + + */ +class ProductFactory extends Factory +{ + protected $model = Product::class; + + public function definition(): array + { + + return [ + 'heading' => fake()->text, + 'slug' => fn (array $attributes): string => Str::slug($attributes['heading']), + 'price' => fake()->numberBetween(1000, 100000), + 'content' => fake()->paragraph(), + 'title' => fake()->words(3, true), + 'description' => fake()->text(200), + 'no_index' => fake()->boolean, + 'canonical' => null, + 'attribute_group_id' => AttributeGroup::factory(), + 'category_id' => Category::query()->inRandomOrder()->first()?->id ?? Category::factory(), + 'brand_id' => Brand::factory(), + 'minimum' => 1, + 'maximum' => null, + 'step' => 1, + 'profit_percent' => fake()->numberBetween(0, 50), + 'attributes' => null, + 'highlight' => null, + 'has_stock' => fake()->boolean, + 'variety_counts' => fake()->numberBetween(0, 5), + 'weight' => fake()->randomNumber(3), + 'length' => fake()->randomNumber(3), + 'width' => fake()->randomNumber(3), + 'height' => fake()->randomNumber(3), + 'status' => fake()->randomElement(ProductStatusEnum::cases()), + 'seen' => fake()->numberBetween(0, 1000), + ]; + } + + public function withImages(int $count = 3): static + { + return $this->afterCreating(function (Product $product) use ($count) { + for ($i = 0; $i < $count; $i++) { + $product->images()->create([ + 'path' => fake()->imageUrl(), + 'is_featured' => $i === 0, // First image featured + 'order' => $i, + 'alt_text' => fake()->words(2, true), + 'imageable_type' => Product::class, + 'imageable_id' => $product->id, + ]); + } + }); + } +} diff --git a/admin/database/migrations/2025_08_08_221913_create_products_table.php b/admin/database/migrations/2025_08_08_221913_create_products_table.php new file mode 100644 index 00000000..edb3b020 --- /dev/null +++ b/admin/database/migrations/2025_08_08_221913_create_products_table.php @@ -0,0 +1,60 @@ +id(); + $table->string('heading'); + $table->string('slug')->unique(); + $table->decimal('price', 10, 2); + $table->text('content')->nullable(); + $table->string('title')->nullable(); + $table->text('description')->nullable(); + $table->boolean('no_index')->default(false); + $table->string('canonical')->nullable(); + $table->foreignIdFor(Image::class)->nullable(); + $table->foreignIdFor(AttributeGroup::class)->nullable(); + $table->foreignIdFor(Category::class); + $table->foreignIdFor(Brand::class)->nullable(); + $table->unsignedInteger('minimum')->default(1); + $table->unsignedInteger('maximum')->nullable(); + $table->unsignedInteger('step')->default(1); + $table->decimal('profit_percent', 5, 2)->default(0); + $table->json('attributes')->nullable(); + $table->json('highlight')->nullable(); + $table->boolean('has_stock')->default(true); + $table->unsignedInteger('variety_counts')->default(0); + $table->decimal('weight', 10, 2)->nullable(); + $table->decimal('length', 10, 2)->nullable(); + $table->decimal('width', 10, 2)->nullable(); + $table->decimal('height', 10, 2)->nullable(); + $table->unsignedTinyInteger('status')->default(ProductStatusEnum::PUBLISHED->value); + $table->unsignedBigInteger('seen')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('products'); + } +}; From 54bdf8de17f2b991d6b4a1d820dba2c05999f94c Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:30:06 +0330 Subject: [PATCH 02/22] build(admin): update .env.example with new URLs - Change APP_URL to http://127.0.0.1:4040 - Add ASSET_URL with the same value as APP_URL Signed-off-by: Bahman Jafarzadeh --- admin/.env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/.env.example b/admin/.env.example index b90caa61..dc507dd2 100644 --- a/admin/.env.example +++ b/admin/.env.example @@ -3,7 +3,8 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_TIMEZONE=UTC -APP_URL=http://localhost +APP_URL=http://127.0.0.1:4040 +ASSET_URL=http://127.0.0.1:4040 APP_LOCALE=en APP_FALLBACK_LOCALE=en From 5543339e8a4fb69ec4bf0e49ddc131514d99a3d1 Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:30:16 +0330 Subject: [PATCH 03/22] feat(app): add product status enum- Create ProductStatusEnum with DELETED, PUBLISHED, and DRAFT cases - Implement label() method for status labels - Implement color() method for status colors Signed-off-by: Bahman Jafarzadeh --- admin/app/Enums/ProductStatusEnum.php | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 admin/app/Enums/ProductStatusEnum.php diff --git a/admin/app/Enums/ProductStatusEnum.php b/admin/app/Enums/ProductStatusEnum.php new file mode 100644 index 00000000..ed887fb3 --- /dev/null +++ b/admin/app/Enums/ProductStatusEnum.php @@ -0,0 +1,34 @@ + 'Deleted', + self::PUBLISHED => 'Published', + self::DRAFT => 'Draft', + }; + } + + public function color(): string + { + return match ($this) { + self::DELETED => 'danger', + self::PUBLISHED => 'success', + self::DRAFT => 'warning', + }; + } +} From ebccc34984526b5cc8609bc90ddde3d87b810984 Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:30:34 +0330 Subject: [PATCH 04/22] feat(admin): remove searchable functionality from category description and content columns - Removed searchable() method from description and content columns in CategoryResource - Added Versioning class to manage version information Signed-off-by: Bahman Jafarzadeh --- .../Filament/Resources/CategoryResource.php | 6 ++--- admin/app/Filament/Versioning.php | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100755 admin/app/Filament/Versioning.php diff --git a/admin/app/Filament/Resources/CategoryResource.php b/admin/app/Filament/Resources/CategoryResource.php index cf8eb001..afeff1ce 100644 --- a/admin/app/Filament/Resources/CategoryResource.php +++ b/admin/app/Filament/Resources/CategoryResource.php @@ -96,11 +96,9 @@ public static function table(Table $table): Table ->wrap(), Tables\Columns\TextColumn::make('description') ->limit(30) - ->wrap() - ->searchable(), + ->wrap(), Tables\Columns\TextColumn::make('content') - ->limit(60) - ->searchable(), + ->limit(60), Tables\Columns\TextColumn::make('status') ->getStateUsing(fn (Category $record) => $record->status->label()) ->color(fn (Category $record): string => $record->status->color()) diff --git a/admin/app/Filament/Versioning.php b/admin/app/Filament/Versioning.php new file mode 100755 index 00000000..99c865f3 --- /dev/null +++ b/admin/app/Filament/Versioning.php @@ -0,0 +1,24 @@ +run([ + // 'git', 'describe', '--tags', '--abbrev=0', + // ])->output()); + + return 'v1.5.0'; + } +} From 5d929bff90df6b9acd254170edee489197f18444 Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:31:00 +0330 Subject: [PATCH 05/22] feat(models): add product relations and image properties - Add products() relation to Brand and Category models - Add alt_text, is_featured, and order properties to Image model - Update model type hints for new properties and relations Signed-off-by: Bahman Jafarzadeh --- admin/app/Models/Brand.php | 8 ++++++++ admin/app/Models/Category.php | 7 +++++++ admin/app/Models/Image.php | 11 +++++++++++ 3 files changed, 26 insertions(+) diff --git a/admin/app/Models/Brand.php b/admin/app/Models/Brand.php index 98d7c4a9..f842a9b7 100644 --- a/admin/app/Models/Brand.php +++ b/admin/app/Models/Brand.php @@ -6,8 +6,10 @@ use App\Enums\BrandStatusEnum; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphOne; /** @@ -24,6 +26,7 @@ * @property BrandStatusEnum $status * @property Carbon|null $created_at * @property Carbon|null $updated_at + * @property Collection $products * @property Image|null $image */ class Brand extends Model @@ -50,4 +53,9 @@ public function image(): MorphOne { return $this->morphOne(Image::class, 'imageable'); } + + public function products(): HasMany + { + return $this->hasMany(Product::class); + } } diff --git a/admin/app/Models/Category.php b/admin/app/Models/Category.php index 3ecedf0d..f65c7bb6 100644 --- a/admin/app/Models/Category.php +++ b/admin/app/Models/Category.php @@ -7,6 +7,7 @@ use App\Enums\CategoryStatusEnum; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -26,6 +27,7 @@ * @property Image $image * @property Carbon|null $created_at * @property Carbon|null $updated_at + * @property Collection $products * * @method static Builder|Category active() */ @@ -50,6 +52,11 @@ class Category extends Model 'status' => CategoryStatusEnum::class, ]; + public function products(): HasMany + { + return $this->hasMany(Product::class); + } + public function image(): MorphOne { return $this->morphOne(Image::class, 'imageable'); diff --git a/admin/app/Models/Image.php b/admin/app/Models/Image.php index bfa64e59..817c6090 100644 --- a/admin/app/Models/Image.php +++ b/admin/app/Models/Image.php @@ -16,6 +16,9 @@ * @property string $path * @property positive-int $imageable_id * @property string $imageable_type + * @property string|null $alt_text + * @property bool $is_featured + * @property positive-int|null $order * @property Carbon|null $created_at * @property Carbon|null $updated_at */ @@ -27,6 +30,14 @@ class Image extends Model 'path', 'imageable_id', 'imageable_type', + 'alt_text', + 'is_featured', + 'order', + ]; + + protected $casts = [ + 'is_featured' => 'boolean', + 'order' => 'integer', ]; /** From 3765f41b80f696a421f4900d30f7aab688f1bdfb Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:31:10 +0330 Subject: [PATCH 06/22] feat(admin): add Filament service provider - Create FilamentServiceProvider to configure Filament components - Set up TextEntry and Column configurations - Register custom icons for sidebar - Add versioning information to sidebar footer Signed-off-by: Bahman Jafarzadeh --- .../app/Providers/FilamentServiceProvider.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 admin/app/Providers/FilamentServiceProvider.php diff --git a/admin/app/Providers/FilamentServiceProvider.php b/admin/app/Providers/FilamentServiceProvider.php new file mode 100644 index 00000000..c6425189 --- /dev/null +++ b/admin/app/Providers/FilamentServiceProvider.php @@ -0,0 +1,60 @@ +placeholder('-'); + }); + + Column::configureUsing(function (Column $column): void { + $column + ->placeholder('-') + ->toggleable() + ->searchable() + ->sortable(); + }); + + FilamentIcon::register([ + 'panels::sidebar.expand-button' => 'heroicon-o-bars-3', + 'panels::sidebar.collapse-button' => 'heroicon-o-bars-3', + ]); + + FilamentView::registerRenderHook( + name: 'panels::sidebar.footer', + hook: function (): View { + $versioning = new Versioning; + + return view('filament.sidebar-widget', [ + 'name' => $versioning->getName(), + 'version' => $versioning->getVersion(), + ]); + } + ); + } +} From a59779b366d435a156d5a84028961314b11af9b7 Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:31:26 +0330 Subject: [PATCH 07/22] feat(admin): add FilamentServiceProvider to providers list - Include FilamentServiceProvider in the list of service providers - This addition is essential for integrating Filament functionality into the admin area Signed-off-by: Bahman Jafarzadeh --- admin/bootstrap/providers.php | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/bootstrap/providers.php b/admin/bootstrap/providers.php index 701ef3e4..1ec8afe0 100644 --- a/admin/bootstrap/providers.php +++ b/admin/bootstrap/providers.php @@ -5,4 +5,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\Filament\AdminPanelProvider::class, + App\Providers\FilamentServiceProvider::class, ]; From 2b79561647683b79b317f567d3452b2450fdbd5b Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:31:38 +0330 Subject: [PATCH 08/22] refactor(factories): limit heading length for Brand model - Update BrandFactory to generate headings with a maximum length of 30 characters - Improve data consistency and readability in testing environments Signed-off-by: Bahman Jafarzadeh --- admin/database/factories/BrandFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/database/factories/BrandFactory.php b/admin/database/factories/BrandFactory.php index 6a5cfe8e..16304463 100644 --- a/admin/database/factories/BrandFactory.php +++ b/admin/database/factories/BrandFactory.php @@ -22,7 +22,7 @@ class BrandFactory extends Factory public function definition(): array { return [ - 'heading' => fake()->text, + 'heading' => fake()->text(30), 'slug' => fn (array $attributes): string => Str::slug($attributes['heading']), 'content' => fake()->optional()->paragraph(), 'title' => fake()->optional()->text, From 57384b42167db60acc8a9f548efeda81d4a0fabe Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:31:55 +0330 Subject: [PATCH 09/22] feat(images): add additional fields to images table - Add 'order' column to sort images - Add 'is_featured' column to mark featured images- Add 'alt_text' column for alternative text description Signed-off-by: Bahman Jafarzadeh --- .../migrations/2024_08_07_151806_create_images_table.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/admin/database/migrations/2024_08_07_151806_create_images_table.php b/admin/database/migrations/2024_08_07_151806_create_images_table.php index cfae1940..13d96594 100644 --- a/admin/database/migrations/2024_08_07_151806_create_images_table.php +++ b/admin/database/migrations/2024_08_07_151806_create_images_table.php @@ -17,6 +17,9 @@ public function up(): void $table->id(); $table->text('path')->nullable(); $table->morphs('imageable'); + $table->integer('order')->default(0); + $table->boolean('is_featured')->default(false); + $table->string('alt_text')->nullable(); $table->timestamps(); }); } From 3e40e04084ffc4e868c6d6546d89524dc2c9092e Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:32:18 +0330 Subject: [PATCH 10/22] add product.jpeg Signed-off-by: Bahman Jafarzadeh --- admin/public/image/product.jpeg | Bin 0 -> 57600 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 admin/public/image/product.jpeg diff --git a/admin/public/image/product.jpeg b/admin/public/image/product.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d8b8e042c55ed88aad6e6933c71c0cec0b7eb3a9 GIT binary patch literal 57600 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/DXT!hB~6khv#J+N17 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 Date: Sat, 9 Aug 2025 18:34:06 +0330 Subject: [PATCH 11/22] feat(admin): add sidebar widget for displaying Filament versions - Create a new sidebar widget to show Filament version information - Add styling for the sidebar and its components - Implement conditional visibility based on sidebar collapse settings - Include version details in a list format at the bottom of the sidebar Signed-off-by: Bahman Jafarzadeh --- .../views/filament/sidebar-widget.blade.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 admin/resources/views/filament/sidebar-widget.blade.php diff --git a/admin/resources/views/filament/sidebar-widget.blade.php b/admin/resources/views/filament/sidebar-widget.blade.php new file mode 100755 index 00000000..f53ba4d3 --- /dev/null +++ b/admin/resources/views/filament/sidebar-widget.blade.php @@ -0,0 +1,26 @@ + +
isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop()) + x-cloak + x-show="$store.sidebar.isOpen" + @endif +> +
    +
  • {{ $name }}: {{ $version }}
  • +
+
From 4a16113ab989260b95fb1855f3388578a5122ea1 Mon Sep 17 00:00:00 2001 From: Bahman Jafarzadeh Date: Sat, 9 Aug 2025 18:34:17 +0330 Subject: [PATCH 12/22] test(filament): add product resource test cases - Create test cases for ProductResource in the admin panel - Cover index page rendering, product listing, edit page rendering, product updates, product creation, and product deletion - Include image upload functionality in the tests Signed-off-by: Bahman Jafarzadeh --- .../Resource/CategoryResourceTest.php | 3 +- .../Filament/Resource/ProductResourceTest.php | 193 ++++++++++++++++++ 2 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 admin/tests/Feature/Filament/Resource/ProductResourceTest.php diff --git a/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php b/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php index 781f33bb..eaa438ae 100644 --- a/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php +++ b/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php @@ -131,7 +131,6 @@ // Act & Assert livewire(CategoryResource\Pages\EditCategory::class, [ 'record' => $category->getRouteKey(), - ]) - ->callAction(DeleteAction::class); + ])->callAction(DeleteAction::class); $this->assertModelMissing($category); }); diff --git a/admin/tests/Feature/Filament/Resource/ProductResourceTest.php b/admin/tests/Feature/Filament/Resource/ProductResourceTest.php new file mode 100644 index 00000000..fa627211 --- /dev/null +++ b/admin/tests/Feature/Filament/Resource/ProductResourceTest.php @@ -0,0 +1,193 @@ +assertOk(); +}); + +it('can list products in the table.', function () { + $products = Product::factory()->count(5)->create(); + + livewire(ProductResource\Pages\ListProducts::class) + ->assertCanSeeTableRecords($products); +}); + +it('can render edit product page.', function () { + get(ProductResource::getUrl('edit', [ + 'record' => Product::factory()->create(), + ]))->assertSuccessful(); +}); + +it('can update product model.', function () { + $product = Product::factory()->create(); + $newProduct = Product::factory()->make(); + $file = UploadedFile::fake()->image('image.png', 500); + + livewire(ProductResource\Pages\EditProduct::class, [ + 'record' => $product->getRouteKey(), + ]) + ->fillForm([ + 'heading' => $newProduct->heading, + 'slug' => $newProduct->slug, + 'price' => $newProduct->price, + 'content' => $newProduct->content, + 'title' => $newProduct->title, + 'description' => $newProduct->description, + 'no_index' => $newProduct->no_index, + 'canonical' => $newProduct->canonical, + 'attribute_group_id' => $newProduct->attribute_group_id, + 'category_id' => $newProduct->category_id, + 'brand_id' => $newProduct->brand_id, + 'minimum' => $newProduct->minimum, + 'maximum' => $newProduct->maximum, + 'step' => $newProduct->step, + 'profit_percent' => $newProduct->profit_percent, + 'attributes' => $newProduct->attributes, + 'highlight' => $newProduct->highlight, + 'has_stock' => $newProduct->has_stock, + 'variety_counts' => $newProduct->variety_counts, + 'weight' => $newProduct->weight, + 'length' => $newProduct->length, + 'width' => $newProduct->width, + 'height' => $newProduct->height, + 'status' => $newProduct->status->value, + 'seen' => $newProduct->seen, + ]) + ->set('data.images.0.path', [$file->getClientOriginalName()]) // you may adjust this based on your form + ->set('data.images.0.is_featured', false) + ->call('save') + ->assertHasNoFormErrors(); + + expect($product->refresh()) + ->heading->toBe($newProduct->heading) + ->slug->toBe($newProduct->slug) + ->price->toBe($newProduct->price) + ->content->toBe($newProduct->content ? '

' . $newProduct->content . '

' : null) + ->title->toBe($newProduct->title) + ->description->toBe($newProduct->description) + ->no_index->toBe($newProduct->no_index) + ->canonical->toBe($newProduct->canonical) + ->attribute_group_id->toBe($newProduct->attribute_group_id) + ->category_id->toBe($newProduct->category_id) + ->brand_id->toBe($newProduct->brand_id) + ->minimum->toBe($newProduct->minimum) + ->maximum->toBe($newProduct->maximum) + ->step->toBe($newProduct->step) + ->profit_percent->toBe($newProduct->profit_percent) + ->attributes->toBe($newProduct->attributes) + ->highlight->toBe($newProduct->highlight) + ->has_stock->toBe($newProduct->has_stock) + ->variety_counts->toBe($newProduct->variety_counts) + ->weight->toBe($newProduct->weight) + ->length->toBe($newProduct->length) + ->width->toBe($newProduct->width) + ->height->toBe($newProduct->height) + ->status->toBe($newProduct->status) + ->seen->toBe($newProduct->seen); + + $product = Product::query()->where('slug', $newProduct->slug)->first(); + + $this->assertDatabaseHas(Image::class, [ + 'path' => $file->getClientOriginalName(), + 'imageable_id' => $product->id, + 'imageable_type' => Product::class, + ]); +}); + +it('can create product model.', function () { + $newProduct = Product::factory()->make(); + $file = UploadedFile::fake()->image('image.png', 500); + + livewire(ProductResource\Pages\CreateProduct::class) + ->fillForm([ + 'heading' => $newProduct->heading, + 'slug' => $newProduct->slug, + 'price' => $newProduct->price, + 'content' => $newProduct->content, + 'title' => $newProduct->title, + 'description' => $newProduct->description, + 'no_index' => $newProduct->no_index, + 'canonical' => $newProduct->canonical, + 'attribute_group_id' => $newProduct->attribute_group_id, + 'category_id' => $newProduct->category_id, + 'brand_id' => $newProduct->brand_id, + 'minimum' => $newProduct->minimum, + 'maximum' => $newProduct->maximum, + 'step' => $newProduct->step, + 'profit_percent' => $newProduct->profit_percent, + 'attributes' => $newProduct->attributes, + 'highlight' => $newProduct->highlight, + 'has_stock' => $newProduct->has_stock, + 'variety_counts' => $newProduct->variety_counts, + 'weight' => $newProduct->weight, + 'length' => $newProduct->length, + 'width' => $newProduct->width, + 'height' => $newProduct->height, + 'status' => $newProduct->status->value, + 'seen' => $newProduct->seen, + ]) + ->set('data.images.0.path', [$file->getClientOriginalName()]) + ->set('data.images.0.is_featured', false) + ->call('create') + ->assertHasNoFormErrors(); + + $this->assertDatabaseHas(Product::class, [ + 'heading' => $newProduct->heading, + 'slug' => $newProduct->slug, + 'price' => $newProduct->price, + 'content' => $newProduct->content ? '

' . $newProduct->content . '

' : null, + 'title' => $newProduct->title, + 'description' => $newProduct->description, + 'no_index' => $newProduct->no_index, + 'canonical' => $newProduct->canonical, + 'attribute_group_id' => $newProduct->attribute_group_id, + 'category_id' => $newProduct->category_id, + 'brand_id' => $newProduct->brand_id, + 'minimum' => $newProduct->minimum, + 'maximum' => $newProduct->maximum, + 'step' => $newProduct->step, + 'profit_percent' => $newProduct->profit_percent, + 'attributes' => $newProduct->attributes, + 'highlight' => $newProduct->highlight, + 'has_stock' => $newProduct->has_stock, + 'variety_counts' => $newProduct->variety_counts, + 'weight' => $newProduct->weight, + 'length' => $newProduct->length, + 'width' => $newProduct->width, + 'height' => $newProduct->height, + 'status' => $newProduct->status->value, + 'seen' => $newProduct->seen, + ]); + + $product = Product::query()->where('slug', $newProduct->slug)->first(); + + $this->assertDatabaseHas(Image::class, [ + 'path' => $file->getClientOriginalName(), + 'imageable_id' => $product->id, + 'imageable_type' => Product::class, + ]); +}); + +it('can delete product model.', function () { + $product = Product::factory()->create(); + + livewire(ProductResource\Pages\EditProduct::class, [ + 'record' => $product->getRouteKey(), + ])->callAction(DeleteAction::class); + + $this->assertModelMissing($product); +}); From 8ed5a748c3952fc33a345104afd7dae71a4d6563 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:21:51 +0330 Subject: [PATCH 13/22] refactor(admin): replace TiptapEditor with TinyEditor for brand content- Remove TiptapEditor and related attributes from BrandResource - Add TinyEditor for 'content' field with full column span - Simplify the form structure for better usability --- admin/app/Filament/Resources/BrandResource.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/admin/app/Filament/Resources/BrandResource.php b/admin/app/Filament/Resources/BrandResource.php index 41ebe51a..dfe52971 100644 --- a/admin/app/Filament/Resources/BrandResource.php +++ b/admin/app/Filament/Resources/BrandResource.php @@ -12,9 +12,8 @@ use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use FilamentTiptapEditor\Enums\TiptapOutput; -use FilamentTiptapEditor\TiptapEditor; use Illuminate\Support\Str; +use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class BrandResource extends Resource { @@ -44,10 +43,8 @@ public static function form(Form $form): Form ->maxLength(255) ->unique(Brand::class, 'slug', ignoreRecord: true), - TiptapEditor::make('content') - ->output(TiptapOutput::Html) // optional, change the format for saved data, default is html - ->columnSpanFull() - ->extraInputAttributes(['style' => 'min-height: 12rem;']), + TinyEditor::make('content') + ->columnSpanFull(), Forms\Components\TextInput::make('title') ->maxLength(255), Forms\Components\TextInput::make('description') From d4ed2a8bbdcf3f5f3cf5fcf23150b07561ba36a1 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:21:57 +0330 Subject: [PATCH 14/22] feat(admin): replace TiptapEditor with TinyEditor for category content - Remove TiptapEditor and related attributes - Add TinyEditor for 'content' field - Keep existing functionality and layout --- admin/app/Filament/Resources/CategoryResource.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/admin/app/Filament/Resources/CategoryResource.php b/admin/app/Filament/Resources/CategoryResource.php index afeff1ce..2f863f96 100644 --- a/admin/app/Filament/Resources/CategoryResource.php +++ b/admin/app/Filament/Resources/CategoryResource.php @@ -13,9 +13,8 @@ use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use FilamentTiptapEditor\Enums\TiptapOutput; -use FilamentTiptapEditor\TiptapEditor; use Illuminate\Support\Str; +use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class CategoryResource extends Resource { @@ -46,10 +45,8 @@ public static function form(Form $form): Form ->unique(Category::class, 'slug', ignoreRecord: true), Forms\Components\TextInput::make('title') ->maxLength(255), - TiptapEditor::make('content') - ->output(TiptapOutput::Html) // optional, change the format for saved data, default is html + TinyEditor::make('content') ->columnSpanFull() - ->extraInputAttributes(['style' => 'min-height: 12rem;']) ->required(), Forms\Components\Textarea::make('description') ->maxLength(255) From a75835cca9967477a81e5ccb67e9244b0ebb0864 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:22:10 +0330 Subject: [PATCH 15/22] feat(product): add attribute management and enhance product form - Replace TiptapEditor with TinyEditor for product content- Add attribute management using Forms\Components\Repeater - Improve product form layout and functionality - Adjust table columns for better visibility --- .../Filament/Resources/ProductResource.php | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/admin/app/Filament/Resources/ProductResource.php b/admin/app/Filament/Resources/ProductResource.php index 57e44fbd..33a5b4ef 100644 --- a/admin/app/Filament/Resources/ProductResource.php +++ b/admin/app/Filament/Resources/ProductResource.php @@ -6,15 +6,15 @@ use App\Enums\ProductStatusEnum; use App\Filament\Resources\ProductResource\Pages; +use App\Models\Attribute; use App\Models\Product; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use FilamentTiptapEditor\Enums\TiptapOutput; -use FilamentTiptapEditor\TiptapEditor; use Illuminate\Support\Str; +use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class ProductResource extends Resource { @@ -47,10 +47,8 @@ public static function form(Form $form): Form ->required() ->numeric() ->prefix('تومان'), - TiptapEditor::make('content') - ->output(TiptapOutput::Html) + TinyEditor::make('content') ->columnSpanFull() - ->extraInputAttributes(['style' => 'min-height: 12rem;']) ->required(), Forms\Components\TextInput::make('title') ->maxLength(255), @@ -62,7 +60,7 @@ public static function form(Form $form): Form Forms\Components\TextInput::make('canonical') ->maxLength(255), Forms\Components\Repeater::make('images') - ->relationship('images') // link to morphMany + ->relationship('images') ->schema([ Forms\Components\FileUpload::make('path') ->nullable() @@ -107,11 +105,46 @@ public static function form(Form $form): Form ->numeric() ->suffix('%') ->default(0), - Forms\Components\TextInput::make('attributes') - ->nullable(), - Forms\Components\TextInput::make('highlight') - ->nullable(), + + Forms\Components\Repeater::make('attributes') + ->schema([ + Forms\Components\Select::make('attribute_id') + ->label('Attribute') + ->options( + Attribute::with('attributeGroup')->get()->mapWithKeys(function ($attribute) { + return [ + $attribute->id => $attribute->attributeGroup->name . ' - ' . $attribute->value, + ]; + }) + ) + ->searchable(), + Forms\Components\Checkbox::make('pivot.is_highlight') + ->label('Highlight'), + ]) + ->dehydrated(false) + ->afterStateHydrated(function ($state, callable $set, $livewire) { + if ($livewire->record) { + $attributes = $livewire->record->attributes()->get(); + + $data = $attributes->map(function ($attribute) { + return [ + 'attribute_id' => $attribute->id, + 'pivot' => ['is_highlight' => $attribute->pivot->is_highlight], + ]; + })->toArray(); + + $set('attributes', $data); + } + }) + ->saveRelationshipsUsing(function ($record, $state) { + $syncData = []; + foreach ($state as $item) { + $syncData[$item['attribute_id']] = ['is_highlight' => $item['pivot']['is_highlight'] ?? false]; + } + $record->attributes()->sync($syncData); + }), Forms\Components\Toggle::make('has_stock') + ->default(true) ->required(), Forms\Components\TextInput::make('variety_counts') ->required() @@ -150,6 +183,9 @@ public static function table(Table $table): Table Tables\Columns\TextColumn::make('slug') ->limit(30) ->wrap(), + Tables\Columns\ImageColumn::make('featuredImage.path') + ->label('Featured') + ->square(), Tables\Columns\TextColumn::make('price') ->money() ->sortable(), @@ -159,11 +195,6 @@ public static function table(Table $table): Table ->boolean(), Tables\Columns\TextColumn::make('canonical') ->searchable(), - - Tables\Columns\ImageColumn::make('featuredImage.path') - ->label('Featured') - ->square(), - Tables\Columns\TextColumn::make('attributeGroup.name') ->numeric() ->sortable(), From c186d0f0aa42dbac05375c81144938d0c2ee6678 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:22:23 +0330 Subject: [PATCH 16/22] feat(ProductFactory): add product attributes and highlights - Add 'withAttributes' method to the ProductFactory - This method attaches random attributes to a product after creation - It also assigns a random highlight status to the product - Remove unnecessary 'attributes' and 'highlight' fields from the factory definition --- admin/database/factories/ProductFactory.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/admin/database/factories/ProductFactory.php b/admin/database/factories/ProductFactory.php index cd1dfe49..c4819480 100644 --- a/admin/database/factories/ProductFactory.php +++ b/admin/database/factories/ProductFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories; use App\Enums\ProductStatusEnum; +use App\Models\Attribute; use App\Models\AttributeGroup; use App\Models\Brand; use App\Models\Category; @@ -39,8 +40,6 @@ public function definition(): array 'maximum' => null, 'step' => 1, 'profit_percent' => fake()->numberBetween(0, 50), - 'attributes' => null, - 'highlight' => null, 'has_stock' => fake()->boolean, 'variety_counts' => fake()->numberBetween(0, 5), 'weight' => fake()->randomNumber(3), @@ -67,4 +66,19 @@ public function withImages(int $count = 3): static } }); } + + public function withAttributes(int $count = 3): static + { + $highlight = fake()->boolean; + + return $this->afterCreating(function (Product $product) use ($count, $highlight) { + $attributes = Attribute::query()->inRandomOrder()->take($count)->get(); + + foreach ($attributes as $attribute) { + $product->attributes()->attach($attribute->id, [ + 'is_highlight' => $highlight, + ]); + } + }); + } } From d62606ddd12c16778c0f5fd07576b2156aa60edd Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:22:36 +0330 Subject: [PATCH 17/22] feat(Product): implement attribute and highlight relationships - Add attributes() and highlights() methods to establish BelongsToMany relationship with Attribute model - Remove 'attributes' and 'highlight' properties from fillable and casts arrays - Update Product model to use new relationship methods for attribute management --- admin/app/Models/Product.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/admin/app/Models/Product.php b/admin/app/Models/Product.php index 1d4688b6..f7748ecd 100644 --- a/admin/app/Models/Product.php +++ b/admin/app/Models/Product.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphMany; @@ -31,8 +32,6 @@ * @property positive-int|null $maximum * @property positive-int|null $step * @property positive-int|null $profit_percent - * @property array|null $attributes - * @property bool $highlight * @property bool $has_stock * @property positive-int|null $variety_counts * @property positive-int|null $weight @@ -73,8 +72,6 @@ class Product extends Model 'maximum', 'step', 'profit_percent', - 'attributes', - 'highlight', 'has_stock', 'variety_counts', 'weight', @@ -88,8 +85,6 @@ class Product extends Model protected $casts = [ 'no_index' => 'boolean', 'has_stock' => 'boolean', - 'attributes' => 'array', - 'highlight' => 'array', 'minimum' => 'integer', 'maximum' => 'integer', 'step' => 'integer', @@ -136,6 +131,21 @@ protected function getDefaultImageModel(): Image ]); } + public function attributes(): BelongsToMany + { + return $this->belongsToMany(Attribute::class, 'product_attribute') + ->withPivot('is_highlight') + ->withTimestamps(); + } + + public function highlights(): BelongsToMany + { + return $this->belongsToMany(Attribute::class, 'product_attribute') + ->wherePivot('is_highlight', true) + ->withPivot('is_highlight') + ->withTimestamps(); + } + public function attributeGroup(): BelongsTo { return $this->belongsTo(AttributeGroup::class); From 8b9585ea1911b84faadea87edd7a7abb7be53b7e Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:22:53 +0330 Subject: [PATCH 18/22] refactor(products): remove unused JSON columns from products table - Remove 'attributes' and 'highlight' JSON columns from products table - This change simplifies the table structure and removes unnecessary fields --- .../migrations/2025_08_08_221913_create_products_table.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/admin/database/migrations/2025_08_08_221913_create_products_table.php b/admin/database/migrations/2025_08_08_221913_create_products_table.php index edb3b020..6a3bd68f 100644 --- a/admin/database/migrations/2025_08_08_221913_create_products_table.php +++ b/admin/database/migrations/2025_08_08_221913_create_products_table.php @@ -36,8 +36,6 @@ public function up(): void $table->unsignedInteger('maximum')->nullable(); $table->unsignedInteger('step')->default(1); $table->decimal('profit_percent', 5, 2)->default(0); - $table->json('attributes')->nullable(); - $table->json('highlight')->nullable(); $table->boolean('has_stock')->default(true); $table->unsignedInteger('variety_counts')->default(0); $table->decimal('weight', 10, 2)->nullable(); From 4c5237647d0209a81dd4546ea58f1321c4d193d7 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:23:13 +0330 Subject: [PATCH 19/22] refactor(admin): simplify entrypoint path in Dockerfile - Rename `docker-entrypoint.sh` to `entrypoint.sh` - Update file path and entrypoint declaration in Dockerfile --- admin/docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/docker/Dockerfile b/admin/docker/Dockerfile index 4731c35a..d2fbc7b0 100755 --- a/admin/docker/Dockerfile +++ b/admin/docker/Dockerfile @@ -56,10 +56,10 @@ RUN chown -R $uid:$gid "/var/www/.pm2" WORKDIR /var/www/html/admin # Copy entrypoint file -COPY entrypoint.sh /usr/local/bin/docker-entrypoint.sh +COPY entrypoint.sh /usr/local/bin/entrypoint.sh # Make it executable -RUN chmod +x /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh # Set the entrypoint -ENTRYPOINT ["docker-entrypoint.sh"] +ENTRYPOINT ["entrypoint.sh"] From 5f198cc15fa5518938ee5989f5766d954612d73e Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:24:07 +0330 Subject: [PATCH 20/22] test: remove HTML wrapping from content assertions - Remove unnecessary HTML wrapping from content assertions in BrandResourceTest and CategoryResourceTest - This change ensures that the tests accurately reflect the expected content without additional HTML formatting --- admin/tests/Feature/Filament/Resource/BrandResourceTest.php | 4 ++-- .../tests/Feature/Filament/Resource/CategoryResourceTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/tests/Feature/Filament/Resource/BrandResourceTest.php b/admin/tests/Feature/Filament/Resource/BrandResourceTest.php index 22f37ea3..4e44a23a 100644 --- a/admin/tests/Feature/Filament/Resource/BrandResourceTest.php +++ b/admin/tests/Feature/Filament/Resource/BrandResourceTest.php @@ -65,7 +65,7 @@ ->heading->toBe($newBrand->heading) ->slug->toBe($newBrand->slug) ->title->toBe($newBrand->title) - ->content->toBe($newBrand->content ? '

' . $newBrand->content . '

' : null) + ->content->toBe($newBrand->content ? $newBrand->content : null) ->description->toBe($newBrand->description) ->no_index->toBe($newBrand->no_index) ->canonical->toBe($newBrand->canonical) @@ -105,7 +105,7 @@ 'heading' => $newBrand->heading, 'slug' => $newBrand->slug, 'title' => $newBrand->title, - 'content' => $newBrand->content ? '

' . $newBrand->content . '

' : null, + 'content' => $newBrand->content ? $newBrand->content : null, 'description' => $newBrand->description, 'no_index' => $newBrand->no_index, 'canonical' => $newBrand->canonical, diff --git a/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php b/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php index eaa438ae..db617054 100644 --- a/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php +++ b/admin/tests/Feature/Filament/Resource/CategoryResourceTest.php @@ -66,7 +66,7 @@ ->heading->toBe($newCategory->heading) ->slug->toBe($newCategory->slug) ->title->toBe($newCategory->title) - ->content->toBe($newCategory->content ? '

' . $newCategory->content . '

' : null) + ->content->toBe($newCategory->content ? $newCategory->content : null) ->description->toBe($newCategory->description) ->no_index->toBe($newCategory->no_index) ->canonical->toBe($newCategory->canonical) @@ -107,7 +107,7 @@ 'heading' => $newCategory->heading, 'slug' => $newCategory->slug, 'title' => $newCategory->title, - 'content' => $newCategory->content ? '

' . $newCategory->content . '

' : null, + 'content' => $newCategory->content ? $newCategory->content : null, 'description' => $newCategory->description, 'no_index' => $newCategory->no_index, 'canonical' => $newCategory->canonical, From 95fdf8843815223fea66e61ad910e194d483456a Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:24:36 +0330 Subject: [PATCH 21/22] fix(filament-forms-tinyeditor): reinitialize tinyeditor after sort- Add JavaScript to reinitialize tinyeditor instances after sorting elements - Remove unnecessary attributes from product resource tests - Adjust content formatting in product resource tests --- .../filament-forms-tinyeditor/tiny-editor.js | 38 +++++++++++++++++++ .../Filament/Resource/ProductResourceTest.php | 12 +----- 2 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 admin/public/js/mohamedsabil83/filament-forms-tinyeditor/tiny-editor.js diff --git a/admin/public/js/mohamedsabil83/filament-forms-tinyeditor/tiny-editor.js b/admin/public/js/mohamedsabil83/filament-forms-tinyeditor/tiny-editor.js new file mode 100644 index 00000000..40433a89 --- /dev/null +++ b/admin/public/js/mohamedsabil83/filament-forms-tinyeditor/tiny-editor.js @@ -0,0 +1,38 @@ +document.addEventListener('DOMContentLoaded', function () { + const sortableClass = [ + 'fi-fo-builder-item', + 'fi-fo-repeater-item', + ]; + + Livewire.hook('morph.updated', (el) => { + if (!window.tinySettingsCopy) { + return; + } + + const isModalOpen = document.body.classList.contains('tox-dialog__disable-scroll'); + + if (!isModalOpen && sortableClass.some(i => el.el.classList.contains(i))) { + removeEditors(); + setTimeout(reinitializeEditors, 1); + } + }) + + const removeEditors = debounce(() => { + window.tinySettingsCopy.forEach(i => tinymce.execCommand('mceRemoveEditor', false, i.target.id)); + }, 50); + + const reinitializeEditors = debounce(() => { + window.tinySettingsCopy = window.tinySettingsCopy.filter(obj => document.getElementById(obj.id)); + window.tinySettingsCopy.forEach(settings => tinymce.init(settings)) + }); + + function debounce(callback, timeout = 100) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + callback.apply(this, args); + }, timeout); + }; + } +}) diff --git a/admin/tests/Feature/Filament/Resource/ProductResourceTest.php b/admin/tests/Feature/Filament/Resource/ProductResourceTest.php index fa627211..6f9ca311 100644 --- a/admin/tests/Feature/Filament/Resource/ProductResourceTest.php +++ b/admin/tests/Feature/Filament/Resource/ProductResourceTest.php @@ -56,8 +56,6 @@ 'maximum' => $newProduct->maximum, 'step' => $newProduct->step, 'profit_percent' => $newProduct->profit_percent, - 'attributes' => $newProduct->attributes, - 'highlight' => $newProduct->highlight, 'has_stock' => $newProduct->has_stock, 'variety_counts' => $newProduct->variety_counts, 'weight' => $newProduct->weight, @@ -76,7 +74,7 @@ ->heading->toBe($newProduct->heading) ->slug->toBe($newProduct->slug) ->price->toBe($newProduct->price) - ->content->toBe($newProduct->content ? '

' . $newProduct->content . '

' : null) + ->content->toBe($newProduct->content ? $newProduct->content : null) ->title->toBe($newProduct->title) ->description->toBe($newProduct->description) ->no_index->toBe($newProduct->no_index) @@ -88,8 +86,6 @@ ->maximum->toBe($newProduct->maximum) ->step->toBe($newProduct->step) ->profit_percent->toBe($newProduct->profit_percent) - ->attributes->toBe($newProduct->attributes) - ->highlight->toBe($newProduct->highlight) ->has_stock->toBe($newProduct->has_stock) ->variety_counts->toBe($newProduct->variety_counts) ->weight->toBe($newProduct->weight) @@ -129,8 +125,6 @@ 'maximum' => $newProduct->maximum, 'step' => $newProduct->step, 'profit_percent' => $newProduct->profit_percent, - 'attributes' => $newProduct->attributes, - 'highlight' => $newProduct->highlight, 'has_stock' => $newProduct->has_stock, 'variety_counts' => $newProduct->variety_counts, 'weight' => $newProduct->weight, @@ -149,7 +143,7 @@ 'heading' => $newProduct->heading, 'slug' => $newProduct->slug, 'price' => $newProduct->price, - 'content' => $newProduct->content ? '

' . $newProduct->content . '

' : null, + 'content' => $newProduct->content ? $newProduct->content : null, 'title' => $newProduct->title, 'description' => $newProduct->description, 'no_index' => $newProduct->no_index, @@ -161,8 +155,6 @@ 'maximum' => $newProduct->maximum, 'step' => $newProduct->step, 'profit_percent' => $newProduct->profit_percent, - 'attributes' => $newProduct->attributes, - 'highlight' => $newProduct->highlight, 'has_stock' => $newProduct->has_stock, 'variety_counts' => $newProduct->variety_counts, 'weight' => $newProduct->weight, From 9661ffd02aef3e52b969dc1d374fe48fd200bd84 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Tue, 12 Aug 2025 21:25:20 +0330 Subject: [PATCH 22/22] build(admin): replace Filament Tiptap Editor with TinyEditor - Remove awcodes/filament-tiptap-editor - Add mohamedsabil83/filament-forms-tinyeditor --- admin/composer.json | 2 +- admin/composer.lock | 689 +++++++++--------- ..._162004_create_product_attribute_table.php | 34 + 3 files changed, 362 insertions(+), 363 deletions(-) create mode 100644 admin/database/migrations/2025_08_12_162004_create_product_attribute_table.php diff --git a/admin/composer.json b/admin/composer.json index 79d0769b..29ceed83 100644 --- a/admin/composer.json +++ b/admin/composer.json @@ -9,10 +9,10 @@ "license": "MIT", "require": { "php": "^8.4", - "awcodes/filament-tiptap-editor": "^3.0", "filament/filament": "^3.2", "laravel/framework": "^12.0", "laravel/tinker": "^2.9", + "mohamedsabil83/filament-forms-tinyeditor": "^2.4", "spatie/laravel-permission": "^6.19" }, "require-dev": { diff --git a/admin/composer.lock b/admin/composer.lock index 27d72a68..913822b7 100644 --- a/admin/composer.lock +++ b/admin/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d5a00380f577a7e9c55a1194e13e8360", + "content-hash": "cd2e00a7f008b381878c87d6cc019664", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -72,88 +72,6 @@ }, "time": "2025-07-30T15:45:57+00:00" }, - { - "name": "awcodes/filament-tiptap-editor", - "version": "v3.5.14", - "source": { - "type": "git", - "url": "https://github.com/awcodes/filament-tiptap-editor.git", - "reference": "ba0194224e34a1cd3c5b92c881c474c713321572" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/awcodes/filament-tiptap-editor/zipball/ba0194224e34a1cd3c5b92c881c474c713321572", - "reference": "ba0194224e34a1cd3c5b92c881c474c713321572", - "shasum": "" - }, - "require": { - "filament/filament": "^3.2.138", - "php": "^8.1", - "spatie/laravel-package-tools": "^1.9.2", - "ueberdosis/tiptap-php": "^1.1" - }, - "require-dev": { - "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.0", - "orchestra/testbench": "^8.0", - "pestphp/pest": "^2.19", - "pestphp/pest-plugin-laravel": "^2.2", - "pestphp/pest-plugin-livewire": "^2.1", - "spatie/laravel-ray": "^1.26" - }, - "type": "package", - "extra": { - "aliases": { - "TiptapConverter": "FilamentTiptapEditor\\Facades\\TiptapConverter" - }, - "laravel": { - "providers": [ - "FilamentTiptapEditor\\FilamentTiptapEditorServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "FilamentTiptapEditor\\": "src/", - "FilamentTiptapEditor\\Tests\\": "tests/src", - "FilamentTiptapEditor\\Tests\\Database\\Factories\\": "tests/database/factories" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Adam Weston", - "email": "awcodes1@gmail.com", - "role": "Developer" - } - ], - "description": "A Tiptap integration for Filament Admin/Forms.", - "keywords": [ - "editor", - "filament", - "framework", - "laravel", - "tiptap", - "wysiwyg" - ], - "support": { - "issues": "https://github.com/awcodes/filament-tiptap-editor/issues", - "source": "https://github.com/awcodes/filament-tiptap-editor/tree/v3.5.14" - }, - "funding": [ - { - "url": "https://github.com/awcodes", - "type": "github" - } - ], - "time": "2025-05-15T15:50:41+00:00" - }, { "name": "blade-ui-kit/blade-heroicons", "version": "2.6.0", @@ -769,33 +687,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -840,7 +757,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -856,7 +773,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -1069,7 +986,7 @@ }, { "name": "filament/actions", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", @@ -1122,16 +1039,16 @@ }, { "name": "filament/filament", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "d9d2367c910956e1e7a4c2903600f37bd740dd11" + "reference": "6f460f7f5146217b71fc242b288f908fa58c9131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/d9d2367c910956e1e7a4c2903600f37bd740dd11", - "reference": "d9d2367c910956e1e7a4c2903600f37bd740dd11", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/6f460f7f5146217b71fc242b288f908fa58c9131", + "reference": "6f460f7f5146217b71fc242b288f908fa58c9131", "shasum": "" }, "require": { @@ -1183,20 +1100,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-08-04T10:34:25+00:00" + "time": "2025-08-12T13:15:51+00:00" }, { "name": "filament/forms", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "158177c4a551c8aba5be3f45bc423195ab28e5bc" + "reference": "6d2eddf754f30dee8730535dfcbcefb4cd5e1136" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/158177c4a551c8aba5be3f45bc423195ab28e5bc", - "reference": "158177c4a551c8aba5be3f45bc423195ab28e5bc", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/6d2eddf754f30dee8730535dfcbcefb4cd5e1136", + "reference": "6d2eddf754f30dee8730535dfcbcefb4cd5e1136", "shasum": "" }, "require": { @@ -1239,20 +1156,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-08-04T10:34:20+00:00" + "time": "2025-08-12T13:15:47+00:00" }, { "name": "filament/infolists", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa" + "reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/89a3f1f236863e2035be3d7b0c68987508dd06fa", - "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de", + "reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de", "shasum": "" }, "require": { @@ -1290,11 +1207,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-23T10:46:53+00:00" + "time": "2025-08-12T13:15:27+00:00" }, { "name": "filament/notifications", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -1346,16 +1263,16 @@ }, { "name": "filament/support", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "89d8e729025c195a06f2e510af4517fc9a8fd07f" + "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/89d8e729025c195a06f2e510af4517fc9a8fd07f", - "reference": "89d8e729025c195a06f2e510af4517fc9a8fd07f", + "url": "https://api.github.com/repos/filamentphp/support/zipball/afafd5e7a2f8cf052f70f989b52d82d0a1df5c78", + "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78", "shasum": "" }, "require": { @@ -1401,20 +1318,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-28T09:02:43+00:00" + "time": "2025-08-12T13:15:44+00:00" }, { "name": "filament/tables", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "22bc439ec6f2b5fd5703ef499381d7beb0a6b369" + "reference": "20ce6217382785df7b39b8473644c1bfe967963c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/22bc439ec6f2b5fd5703ef499381d7beb0a6b369", - "reference": "22bc439ec6f2b5fd5703ef499381d7beb0a6b369", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/20ce6217382785df7b39b8473644c1bfe967963c", + "reference": "20ce6217382785df7b39b8473644c1bfe967963c", "shasum": "" }, "require": { @@ -1453,11 +1370,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-28T09:02:34+00:00" + "time": "2025-08-12T13:15:31+00:00" }, { "name": "filament/widgets", - "version": "v3.3.35", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -2108,16 +2025,16 @@ }, { "name": "laravel/framework", - "version": "v12.21.0", + "version": "v12.23.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b" + "reference": "1f81af17619f0bc8a87ec385a71546430c301b24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b", - "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "url": "https://api.github.com/repos/laravel/framework/zipball/1f81af17619f0bc8a87ec385a71546430c301b24", + "reference": "1f81af17619f0bc8a87ec385a71546430c301b24", "shasum": "" }, "require": { @@ -2158,6 +2075,8 @@ "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php84": "^1.31", + "symfony/polyfill-php85": "^1.31", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -2319,7 +2238,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-22T15:41:55+00:00" + "time": "2025-08-12T15:37:17+00:00" }, { "name": "laravel/prompts", @@ -3292,6 +3211,88 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "mohamedsabil83/filament-forms-tinyeditor", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/mohamedsabil83/filament-forms-tinyeditor.git", + "reference": "a6697e57113100583b3876f070a4425b172ba9b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mohamedsabil83/filament-forms-tinyeditor/zipball/a6697e57113100583b3876f070a4425b172ba9b7", + "reference": "a6697e57113100583b3876f070a4425b172ba9b7", + "shasum": "" + }, + "require": { + "filament/forms": "^3.0", + "illuminate/contracts": "^9.0 || ^10.0 || ^11.0 || ^12.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.14.0" + }, + "require-dev": { + "larastan/larastan": "^2.2", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0 || ^8.0", + "orchestra/testbench": "8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "spatie/laravel-ray": "^1.26" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Mohamedsabil83\\FilamentFormsTinyeditor\\FilamentFormsTinyeditorServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Mohamedsabil83\\FilamentFormsTinyeditor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "MohamedSabil83", + "email": "me@mohamedsabil83.com", + "role": "Developer" + } + ], + "description": "A TinyMce editor component for Filament", + "homepage": "https://github.com/mohamedsabil83/filament-forms-tinyeditor", + "keywords": [ + "Forms", + "filament", + "laravel", + "tinyeditor", + "tinymce" + ], + "support": { + "issues": "https://github.com/mohamedsabil83/filament-forms-tinyeditor/issues", + "source": "https://github.com/mohamedsabil83/filament-forms-tinyeditor/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://paypal.me/technolizer", + "type": "custom" + }, + { + "url": "https://github.com/mohamedsabil83", + "type": "github" + } + ], + "time": "2025-02-26T11:52:24+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -4779,84 +4780,6 @@ ], "time": "2025-02-25T09:09:36+00:00" }, - { - "name": "scrivo/highlight.php", - "version": "v9.18.1.10", - "source": { - "type": "git", - "url": "https://github.com/scrivo/highlight.php.git", - "reference": "850f4b44697a2552e892ffe71490ba2733c2fc6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/850f4b44697a2552e892ffe71490ba2733c2fc6e", - "reference": "850f4b44697a2552e892ffe71490ba2733c2fc6e", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=5.4" - }, - "require-dev": { - "phpunit/phpunit": "^4.8|^5.7", - "sabberworm/php-css-parser": "^8.3", - "symfony/finder": "^2.8|^3.4|^5.4", - "symfony/var-dumper": "^2.8|^3.4|^5.4" - }, - "suggest": { - "ext-mbstring": "Allows highlighting code with unicode characters and supports language with unicode keywords" - }, - "type": "library", - "autoload": { - "files": [ - "HighlightUtilities/functions.php" - ], - "psr-0": { - "Highlight\\": "", - "HighlightUtilities\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Geert Bergman", - "homepage": "http://www.scrivo.org/", - "role": "Project Author" - }, - { - "name": "Vladimir Jimenez", - "homepage": "https://allejo.io", - "role": "Maintainer" - }, - { - "name": "Martin Folkers", - "homepage": "https://twobrain.io", - "role": "Contributor" - } - ], - "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js", - "keywords": [ - "code", - "highlight", - "highlight.js", - "highlight.php", - "syntax" - ], - "support": { - "issues": "https://github.com/scrivo/highlight.php/issues", - "source": "https://github.com/scrivo/highlight.php" - }, - "funding": [ - { - "url": "https://github.com/allejo", - "type": "github" - } - ], - "time": "2022-12-17T21:53:22+00:00" - }, { "name": "spatie/color", "version": "1.8.0", @@ -5119,71 +5042,6 @@ ], "time": "2025-07-23T16:08:05+00:00" }, - { - "name": "spatie/shiki-php", - "version": "2.3.2", - "source": { - "type": "git", - "url": "https://github.com/spatie/shiki-php.git", - "reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/shiki-php/zipball/a2e78a9ff8a1290b25d550be8fbf8285c13175c5", - "reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^8.0", - "symfony/process": "^5.4|^6.4|^7.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.0", - "pestphp/pest": "^1.8", - "phpunit/phpunit": "^9.5", - "spatie/pest-plugin-snapshots": "^1.1", - "spatie/ray": "^1.10" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\ShikiPhp\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Rias Van der Veken", - "email": "rias@spatie.be", - "role": "Developer" - }, - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "role": "Developer" - } - ], - "description": "Highlight code using Shiki in PHP", - "homepage": "https://github.com/spatie/shiki-php", - "keywords": [ - "shiki", - "spatie" - ], - "support": { - "source": "https://github.com/spatie/shiki-php/tree/2.3.2" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-02-21T14:16:57+00:00" - }, { "name": "symfony/clock", "version": "v7.3.0", @@ -6797,6 +6655,158 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-02T08:40:52+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.32.0", @@ -7590,75 +7600,6 @@ }, "time": "2024-12-21T16:25:41+00:00" }, - { - "name": "ueberdosis/tiptap-php", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/ueberdosis/tiptap-php.git", - "reference": "640667176da4cdfaa84c32093171e0674c3c807f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ueberdosis/tiptap-php/zipball/640667176da4cdfaa84c32093171e0674c3c807f", - "reference": "640667176da4cdfaa84c32093171e0674c3c807f", - "shasum": "" - }, - "require": { - "php": "^7.4|^8.0", - "scrivo/highlight.php": "^9.18", - "spatie/shiki-php": "^2.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.5", - "pestphp/pest": "^1.21", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^4.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Tiptap\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Hans Pagel", - "email": "humans@tiptap.dev", - "role": "Developer" - } - ], - "description": "A PHP package to work with Tiptap output", - "homepage": "https://github.com/ueberdosis/tiptap-php", - "keywords": [ - "prosemirror", - "tiptap", - "ueberdosis" - ], - "support": { - "issues": "https://github.com/ueberdosis/tiptap-php/issues", - "source": "https://github.com/ueberdosis/tiptap-php/tree/1.4.0" - }, - "funding": [ - { - "url": "https://tiptap.dev/pricing", - "type": "custom" - }, - { - "url": "https://github.com/ueberdosis", - "type": "github" - }, - { - "url": "https://opencollective.com/tiptap", - "type": "open_collective" - } - ], - "time": "2024-08-12T08:25:45+00:00" - }, { "name": "vlucas/phpdotenv", "version": "v5.6.2", @@ -8096,16 +8037,16 @@ }, { "name": "filp/whoops", - "version": "2.18.3", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "59a123a3d459c5a23055802237cb317f609867e5" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", - "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -8155,7 +8096,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.3" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -8163,7 +8104,7 @@ "type": "github" } ], - "time": "2025-06-16T00:02:10+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -10318,16 +10259,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.1", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { @@ -10386,15 +10327,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2025-03-07T06:57:01+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", @@ -10975,16 +10928,16 @@ }, { "name": "sebastian/type", - "version": "5.1.2", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { @@ -11020,15 +10973,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2025-03-18T13:35:50+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", @@ -11381,12 +11346,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.4" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/admin/database/migrations/2025_08_12_162004_create_product_attribute_table.php b/admin/database/migrations/2025_08_12_162004_create_product_attribute_table.php new file mode 100644 index 00000000..bf6a373a --- /dev/null +++ b/admin/database/migrations/2025_08_12_162004_create_product_attribute_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignIdFor(Product::class); + $table->foreignIdFor(Attribute::class); + $table->boolean('is_highlight')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_attribute'); + } +};