Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
25ec25e
feat(admin): create product resource and related entities
Aug 9, 2025
54bdf8d
build(admin): update .env.example with new URLs
Aug 9, 2025
5543339
feat(app): add product status enum- Create ProductStatusEnum with DEL…
Aug 9, 2025
ebccc34
feat(admin): remove searchable functionality from category descriptio…
Aug 9, 2025
5d929bf
feat(models): add product relations and image properties
Aug 9, 2025
3765f41
feat(admin): add Filament service provider
Aug 9, 2025
a59779b
feat(admin): add FilamentServiceProvider to providers list
Aug 9, 2025
2b79561
refactor(factories): limit heading length for Brand model
Aug 9, 2025
57384b4
feat(images): add additional fields to images table
Aug 9, 2025
3e40e04
add product.jpeg
Aug 9, 2025
a3d68f3
feat(admin): add sidebar widget for displaying Filament versions
Aug 9, 2025
4a16113
test(filament): add product resource test cases
Aug 9, 2025
8ed5a74
refactor(admin): replace TiptapEditor with TinyEditor for brand conte…
bahman026 Aug 12, 2025
d4ed2a8
feat(admin): replace TiptapEditor with TinyEditor for category content
bahman026 Aug 12, 2025
a75835c
feat(product): add attribute management and enhance product form
bahman026 Aug 12, 2025
c186d0f
feat(ProductFactory): add product attributes and highlights
bahman026 Aug 12, 2025
d62606d
feat(Product): implement attribute and highlight relationships
bahman026 Aug 12, 2025
8b9585e
refactor(products): remove unused JSON columns from products table
bahman026 Aug 12, 2025
4c52376
refactor(admin): simplify entrypoint path in Dockerfile
bahman026 Aug 12, 2025
5f198cc
test: remove HTML wrapping from content assertions
bahman026 Aug 12, 2025
95fdf88
fix(filament-forms-tinyeditor): reinitialize tinyeditor after sort- A…
bahman026 Aug 12, 2025
9661ffd
build(admin): replace Filament Tiptap Editor with TinyEditor
bahman026 Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion admin/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions admin/app/Enums/ProductStatusEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use App\Traits\HasOptions;

enum ProductStatusEnum: int
{
use HasOptions;

case DELETED = 10;
case PUBLISHED = 20;
case DRAFT = 30;

public function label(): string
{
return match ($this) {
self::DELETED => 'Deleted',
self::PUBLISHED => 'Published',
self::DRAFT => 'Draft',
};
}

public function color(): string
{
return match ($this) {
self::DELETED => 'danger',
self::PUBLISHED => 'success',
self::DRAFT => 'warning',
};
}
}
9 changes: 3 additions & 6 deletions admin/app/Filament/Resources/BrandResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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')
Expand Down
13 changes: 4 additions & 9 deletions admin/app/Filament/Resources/CategoryResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -96,11 +93,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())
Expand Down
280 changes: 280 additions & 0 deletions admin/app/Filament/Resources/ProductResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources;

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 Illuminate\Support\Str;
use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor;

class ProductResource extends Resource
{
protected static ?string $model = Product::class;

protected static ?string $navigationGroup = 'Product';

protected static ?string $navigationIcon = 'heroicon-o-shopping-bag';

public static function form(Form $form): Form
{
return $form
->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('تومان'),
TinyEditor::make('content')
->columnSpanFull()
->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')
->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\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()
->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\ImageColumn::make('featuredImage.path')
->label('Featured')
->square(),
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\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'),
];
}
}
Loading