Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .github/workflows/deploy-application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ jobs:
npm ci
npm run build

- name: Run Frontend Lint & Format Check
working-directory: ./shop
run: |
npm run lint
npm run format:check

- name: Setup Environment
working-directory: ./shop
run: |
Expand Down
4 changes: 2 additions & 2 deletions admin/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
# Test Enforcement

- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
- Run the minimum number of tests needed to ensure code quality and speed. Use Pest directly (not `php artisan test`): `vendor/bin/pest` with a specific filename or `--filter`.

=== laravel/core rules ===

Expand Down Expand Up @@ -158,7 +158,7 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac

- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Run tests with Pest directly (not `php artisan test`): `vendor/bin/pest` or filter: `vendor/bin/pest --filter=testName`.
- Do NOT delete tests without approval.

=== filament/filament rules ===
Expand Down
23 changes: 23 additions & 0 deletions admin/app/Filament/Resources/ProductResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Models\AttributeGroup;
use App\Models\AttributeGroupCategory;
use App\Models\Product;
use App\Models\Variety;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
Expand All @@ -27,6 +28,7 @@
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
Expand Down Expand Up @@ -356,6 +358,27 @@ public static function form(Schema $schema): Schema
->required()
->options(VarietyStatusEnum::options())
->default(VarietyStatusEnum::PUBLISHED->value),
Fieldset::make(trans('product.variety_image'))
->relationship('image')
->schema([
FileUpload::make('path')
->label(trans('product.path'))
->image()
->nullable()
->columnSpanFull(),
TextInput::make('alt_text')
->label(trans('product.alt_text'))
->nullable()
->maxLength(255),
])
->mutateRelationshipDataBeforeSaveUsing(function (array $data, Variety $record): array {
if (empty($data['path'])) {
$record->image?->delete();
}

return $data;
})
->columnSpanFull(),
])
->columnSpanFull(),
]);
Expand Down
26 changes: 26 additions & 0 deletions admin/app/Filament/Resources/VarietyResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\ColorColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
Expand Down Expand Up @@ -130,13 +133,36 @@ public static function form(Schema $schema): Schema
->columnSpanFull()
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip(trans('variety.additional_attributes_hint')),
Fieldset::make(trans('variety.image'))
->relationship('image')
->schema([
FileUpload::make('path')
->label(trans('variety.path'))
->image()
->nullable()
->columnSpanFull(),
TextInput::make('alt_text')
->label(trans('variety.alt_text'))
->nullable()
->maxLength(255),
])
->mutateRelationshipDataBeforeSaveUsing(function (array $data, Variety $record): array {
if (empty($data['path'])) {
$record->image?->delete();
}

return $data;
})
->columnSpanFull(),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
ImageColumn::make('image.path')
->label(trans('variety.image')),
TextColumn::make('product.heading')
->label(trans('variety.product'))
->limit(30)
Expand Down
9 changes: 9 additions & 0 deletions admin/app/Models/Variety.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;

/**
* @property positive-int $id
Expand All @@ -29,6 +30,7 @@
* @property Collection<Attribute> $attributes
* @property Collection<Discount> $discounts
* @property Collection<Review> $reviews
* @property Image|null $image
*/
class Variety extends Model
{
Expand Down Expand Up @@ -68,6 +70,8 @@ protected static function booted(): void

static::saved(fn (Variety $variety) => $variety->syncProductVarietyCount());
static::deleted(fn (Variety $variety) => $variety->syncProductVarietyCount());

static::deleting(fn (Variety $variety) => $variety->image?->delete());
}

public function syncProductVarietyCount(): void
Expand All @@ -94,6 +98,11 @@ public function attribute(): BelongsTo
return $this->belongsTo(Attribute::class);
}

public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}

public function attributes(): BelongsToMany
{
return $this->belongsToMany(Attribute::class)->withTimestamps();
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/AddressFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function definition(): array
'postal_code' => $this->faker->postcode(),
'address' => $this->faker->address(),
'description' => $this->faker->text(),
'city_id' => City::factory(),
'city_id' => City::query()->inRandomOrder()->value('id') ?? City::factory(),
'prime' => false,
];
}
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/BannerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function withImages(int $count = 3): static
return $this->afterCreating(function (Banner $banner) use ($count) {
for ($i = 0; $i < $count; $i++) {
$banner->images()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'is_featured' => $i === 0,
'order' => $i,
'alt_text' => fake()->words(2, true),
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/BrandFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function withImage(): BrandFactory | Factory
{
return $this->afterCreating(function (Brand $brand) {
$brand->image()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'imageable_type' => Brand::class,
'imageable_id' => $brand->id,
]);
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/CategoryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function withImage(): BrandFactory | Factory
{
return $this->afterCreating(function (Category $category) {
$category->image()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'imageable_type' => Category::class,
'imageable_id' => $category->id,
]);
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/CityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function definition(): array
{
return [
'name' => $this->faker->city,
'province_id' => Province::factory()->create()->id,
'province_id' => Province::query()->inRandomOrder()->value('id') ?? Province::factory(),
];
}
}
2 changes: 1 addition & 1 deletion admin/database/factories/GatewayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function withImage(): static
{
return $this->afterCreating(function (Gateway $gateway): void {
$gateway->image()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'alt_text' => fake()->words(2, true),
]);
});
Expand Down
15 changes: 14 additions & 1 deletion admin/database/factories/ImageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,22 @@ public function definition(): array
$imageableType = $this->faker->randomElement($imageableTypes);

return [
'path' => fake()->imageUrl,
'path' => self::placeholderUrl(),
'imageable_id' => $imageableType::factory(),
'imageable_type' => $imageableType,
];
}

/**
* A live placeholder image URL.
*
* Faker's default `imageUrl()` points at via.placeholder.com, which has
* been shut down; placehold.co is a working replacement.
*/
public static function placeholderUrl(int $width = 640, int $height = 480): string
{
$color = ltrim(fake()->hexColor(), '#');

return "https://placehold.co/{$width}x{$height}/{$color}/ffffff?text=" . urlencode(fake()->word());
}
}
2 changes: 1 addition & 1 deletion admin/database/factories/PageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function withImage(): static
{
return $this->afterCreating(function (Page $page): void {
$page->image()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'alt_text' => fake()->words(2, true),
]);
});
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/ProductFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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(),
'path' => ImageFactory::placeholderUrl(),
'is_featured' => $i === 0, // First image featured
'order' => $i,
'alt_text' => fake()->words(2, true),
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/ReceiptFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function withImage(): static
{
return $this->afterCreating(function (Receipt $receipt): void {
$receipt->image()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'alt_text' => fake()->words(2, true),
]);
});
Expand Down
2 changes: 1 addition & 1 deletion admin/database/factories/SlideFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function withImage(): static
{
return $this->afterCreating(function (Slide $slide): void {
$slide->image()->create([
'path' => fake()->imageUrl(),
'path' => ImageFactory::placeholderUrl(),
'is_featured' => true,
'order' => 0,
'alt_text' => $slide->heading,
Expand Down
27 changes: 27 additions & 0 deletions admin/database/factories/VarietyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,31 @@ public function withAttribute(Attribute $attribute): static
'attribute_id' => $attribute->id,
]);
}

public function published(): static
{
return $this->state([
'status' => VarietyStatusEnum::PUBLISHED,
]);
}

public function inStock(): static
{
return $this->state([
'has_stock' => true,
'inventory' => fake()->numberBetween(5, 100),
]);
}

public function withImage(): static
{
return $this->afterCreating(function (Variety $variety): void {
$variety->image()->create([
'path' => ImageFactory::placeholderUrl(),
'is_featured' => true,
'order' => 0,
'alt_text' => $variety->attribute_value ?? $variety->color,
]);
});
}
}
8 changes: 8 additions & 0 deletions admin/database/seeders/CategorySeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Models\Category;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr; // Import the Arr facade
use Illuminate\Support\Facades\DB;

class CategorySeeder extends Seeder
{
Expand Down Expand Up @@ -108,6 +109,13 @@ public function run(): void
$this->createChildren($category['children'], $category['id']);
}
}

// Explicit IDs above leave Postgres' auto-increment sequence behind, so
// advance it past the seeded rows to avoid duplicate-key errors on
// categories created later via the admin panel.
if (DB::getDriverName() === 'pgsql') {
DB::statement("SELECT setval(pg_get_serial_sequence('categories', 'id'), (SELECT MAX(id) FROM categories))");
}
}

private function createChildren(array $children, int $parentId): void
Expand Down
7 changes: 7 additions & 0 deletions admin/database/seeders/CitySeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\City;
use App\Models\Province;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class CitySeeder extends Seeder
{
Expand Down Expand Up @@ -5747,5 +5748,11 @@ public function run(): void

City::query()->insert($cities);

// Rows are inserted with explicit ids, so advance the auto-increment
// sequences; otherwise the next factory-created province/city collides.
if (DB::getDriverName() === 'pgsql') {
DB::statement("SELECT setval(pg_get_serial_sequence('provinces', 'id'), (SELECT MAX(id) FROM provinces))");
DB::statement("SELECT setval(pg_get_serial_sequence('cities', 'id'), (SELECT MAX(id) FROM cities))");
}
}
}
14 changes: 13 additions & 1 deletion admin/database/seeders/ProductSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@
namespace Database\Seeders;

use App\Models\Product;
use App\Models\Variety;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
public function run(): void
{
Product::query()->truncate();
Product::factory()->count(20)->create();

Product::factory()
->count(20)
->withImages()
->has(
Variety::factory()
->count(3)
->published()
->inStock()
->withImage()
)
->create();
}
}
Loading
Loading