diff --git a/app/Filament/Resources/Usernames/Pages/CreateUsername.php b/app/Filament/Resources/Usernames/Pages/CreateUsername.php new file mode 100644 index 0000000..eb7b7c8 --- /dev/null +++ b/app/Filament/Resources/Usernames/Pages/CreateUsername.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('username') + ->columnSpan(1) + ->alphaDash() + ->helperText('Email: myusername@gmail.com | Username: myusername') + ->required(), + TextInput::make('daily_mailbox_limit') + ->integer() + ->minValue(1) + ->default(100) + ->helperText('How many mailboxes can be created with this domain daily') + ->columnSpan(1) + ->required(), + ToggleButtons::make('is_active') + ->options([ + true => 'Active', + false => 'Disabled', + ]) + ->inline() + ->default(true) + ->columnSpanFull() + ->required(), + Select::make('username_type') + ->options(UsernameType::class) + ->enum(UsernameType::class) + ->required(), + Select::make('provider_type') + ->options(ProviderType::class) + ->enum(ProviderType::class) + ->required(), + DateTimePicker::make('starts_at'), + DateTimePicker::make('ends_at'), + TextEntry::make('last_used_at') + ->label('Last Used At') + ->formatStateUsing(fn ($state) => $state ? $state->diffForHumans() : 'Never') + ->visible(fn ($context) => $context === 'edit'), + TextEntry::make('checked_at') + ->label('Last Checked At') + ->formatStateUsing(fn ($state) => $state ? $state->diffForHumans() : 'Never') + ->visible(fn ($context) => $context === 'edit'), + TextEntry::make('deleted_at') + ->label('Deleted At') + ->formatStateUsing(fn ($state) => $state ? $state->diffForHumans() : null) + ->color('danger') + ->icon('heroicon-o-trash') + ->visible(fn ($record) => $record?->deleted_at !== null), + ]); + } +} diff --git a/app/Filament/Resources/Usernames/Tables/UsernamesTable.php b/app/Filament/Resources/Usernames/Tables/UsernamesTable.php new file mode 100644 index 0000000..7794a64 --- /dev/null +++ b/app/Filament/Resources/Usernames/Tables/UsernamesTable.php @@ -0,0 +1,108 @@ +columns([ + TextColumn::make('username') + ->label('Usernames') + ->searchable() + ->weight('medium') + ->icon(Heroicon::OutlinedUser) + ->copyable() + ->copyMessage('Domain copied!') + ->copyMessageDuration(1500), + ToggleColumn::make('is_active') + ->label('Active') + ->alignCenter(), + TextColumn::make('username_type') + ->label('Type') + ->formatStateUsing(fn ($state) => $state ? UsernameType::tryFrom($state)?->getLabel() : '-') + ->badge() + ->color(fn ($state) => $state ? UsernameType::tryFrom($state)?->getColor() : 'gray') + ->alignCenter(), + TextColumn::make('provider_type') + ->label('Provider') + ->formatStateUsing(fn ($state) => $state ? ProviderType::tryFrom($state)?->getLabel() : '-') + ->badge() + ->color(fn ($state) => $state ? ProviderType::tryFrom($state)?->getColor() : 'gray') + ->alignCenter(), + TextColumn::make('daily_mailbox_limit') + ->label('Daily Limit') + ->numeric() + ->formatStateUsing(fn ($state) => number_format($state)) + ->alignCenter() + ->icon('heroicon-o-inbox'), + TextColumn::make('last_used_at') + ->label('Last Used') + ->dateTime('M j, Y g:i A') + ->placeholder('Never') + ->sortable() + ->since() + ->alignCenter(), + TextColumn::make('checked_at') + ->label('Checked') + ->dateTime('M j, Y') + ->placeholder('Never') + ->sortable() + ->since() + ->alignCenter() + ->toggleable(isToggledHiddenByDefault: true), + + ]) + ->filters([ + SelectFilter::make('username_type') + ->label('Username Type') + ->options(UsernameType::class), + SelectFilter::make('provider_type') + ->label('Provider Type') + ->options(ProviderType::class), + SelectFilter::make('is_active') + ->label('Status') + ->options([ + true => 'Active', + false => 'Inactive', + ]), + TrashedFilter::make(), + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ForceDeleteBulkAction::make(), + RestoreBulkAction::make(), + ]), + ]) + ->emptyStateHeading('No usernames found') + ->emptyStateDescription('Get started by creating your first username.') + ->emptyStateActions([ + // Add create action if needed + ]) + ->poll('60s') + ->striped() + ->defaultPaginationPageOption(10) + ->paginated([10, 25, 50, 100]) + ->reorderable('sort_order') + ->defaultSort('username'); + } +} diff --git a/app/Filament/Resources/Usernames/UsernameResource.php b/app/Filament/Resources/Usernames/UsernameResource.php new file mode 100644 index 0000000..71a8646 --- /dev/null +++ b/app/Filament/Resources/Usernames/UsernameResource.php @@ -0,0 +1,60 @@ + ListUsernames::route('/'), + 'create' => CreateUsername::route('/create'), + 'edit' => EditUsername::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Models/Username.php b/app/Models/Username.php new file mode 100644 index 0000000..fb870e9 --- /dev/null +++ b/app/Models/Username.php @@ -0,0 +1,72 @@ + 'boolean', + 'daily_mailbox_limit' => 'integer', + 'starts_at' => 'datetime', + 'ends_at' => 'datetime', + 'last_used_at' => 'datetime', + 'checked_at' => 'datetime', + ]; + } + + /** + * Retrieve active username by type and provider. + * + * @param UsernameType|null $usernameType Filter by username type + * @param ProviderType|null $providerType Filter by provider type + * @return array Array of usernames + */ + public static function getActiveUsernameByType( + ?UsernameType $usernameType = null, + ?ProviderType $providerType = null, + ): array { + $query = static::query() + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }); + + if ($usernameType) { + $query->where('username_type', $usernameType); + } + + if ($providerType) { + $query->where('provider_type', $providerType); + } + + return $query->pluck('username')->toArray(); + } +} diff --git a/app/enum/UsernameType.php b/app/enum/UsernameType.php new file mode 100644 index 0000000..1b781be --- /dev/null +++ b/app/enum/UsernameType.php @@ -0,0 +1,25 @@ + 'warning', + self::PREMIUM => 'success', + }; + } + + public function getLabel(): string + { + return match ($this) { + self::PUBLIC => 'Public', + self::PREMIUM => 'Premium', + }; + } +} diff --git a/database/factories/UsernameFactory.php b/database/factories/UsernameFactory.php new file mode 100644 index 0000000..b23d0b7 --- /dev/null +++ b/database/factories/UsernameFactory.php @@ -0,0 +1,135 @@ + + */ +class UsernameFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'username' => $this->faker->unique()->userName(), + 'is_active' => $this->faker->boolean(80), // 80% chance of being active + 'daily_mailbox_limit' => $this->faker->numberBetween(50, 500), + 'username_type' => $this->faker->randomElement(UsernameType::class), + 'provider_type' => $this->faker->randomElement(ProviderType::class), + 'starts_at' => $this->faker->optional(0.3)->dateTimeBetween('-1 year', 'now'), // 30% chance of having start date + 'ends_at' => $this->faker->optional(0.2)->dateTimeBetween('now', '+2 years'), // 20% chance of having end date + 'last_used_at' => $this->faker->optional(0.7)->dateTimeBetween('-1 month', 'now'), // 70% chance of being used + 'checked_at' => $this->faker->optional(0.8)->dateTimeBetween('-1 week', 'now'), // 80% chance of being checked + ]; + } + + /** + * Indicate that the username is active. + */ + public function active(): static + { + return $this->state(fn (array $attributes) => [ + 'is_active' => true, + ]); + } + + /** + * Indicate that the username is inactive. + */ + public function inactive(): static + { + return $this->state(fn (array $attributes) => [ + 'is_active' => false, + ]); + } + + /** + * Indicate that the username is public. + */ + public function public(): static + { + return $this->state(fn (array $attributes) => [ + 'username_type' => UsernameType::PUBLIC, + ]); + } + + /** + * Indicate that the username is premium. + */ + public function premium(): static + { + return $this->state(fn (array $attributes) => [ + 'username_type' => UsernameType::PREMIUM, + ]); + } + + /** + * Indicate that the username is for Gmail. + */ + public function gmail(): static + { + return $this->state(fn (array $attributes) => [ + 'provider_type' => ProviderType::GMAIL, + ]); + } + + /** + * Indicate that the username is for Yahoo. + */ + public function yahoo(): static + { + return $this->state(fn (array $attributes) => [ + 'provider_type' => ProviderType::YAHOO, + ]); + } + + /** + * Indicate that the username is for Outlook. + */ + public function outlook(): static + { + return $this->state(fn (array $attributes) => [ + 'provider_type' => ProviderType::OUTLOOK, + ]); + } + + /** + * Indicate that the username is for custom provider. + */ + public function custom(): static + { + return $this->state(fn (array $attributes) => [ + 'provider_type' => ProviderType::CUSTOM, + ]); + } + + /** + * Indicate that the username has expiration dates. + */ + public function withExpiration(): static + { + return $this->state(fn (array $attributes) => [ + 'starts_at' => $this->faker->dateTimeBetween('-1 month', 'now'), + 'ends_at' => $this->faker->dateTimeBetween('now', '+1 year'), + ]); + } + + /** + * Indicate that the username has been recently used. + */ + public function recentlyUsed(): static + { + return $this->state(fn (array $attributes) => [ + 'last_used_at' => $this->faker->dateTimeBetween('-1 day', 'now'), + 'checked_at' => $this->faker->dateTimeBetween('-1 day', 'now'), + ]); + } +} diff --git a/database/migrations/2025_11_16_043630_create_usernames_table.php b/database/migrations/2025_11_16_043630_create_usernames_table.php new file mode 100644 index 0000000..9ea2824 --- /dev/null +++ b/database/migrations/2025_11_16_043630_create_usernames_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('username')->unique(); + $table->boolean('is_active')->default(true); + $table->integer('daily_mailbox_limit')->default(100); + $table->string('username_type')->nullable(); + $table->string('provider_type')->nullable(); + $table->timestamp('starts_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('checked_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('usernames'); + } +}; diff --git a/database/seeders/UsernameSeeder.php b/database/seeders/UsernameSeeder.php new file mode 100644 index 0000000..622fb7a --- /dev/null +++ b/database/seeders/UsernameSeeder.php @@ -0,0 +1,90 @@ +count(15) + ->active() + ->public() + ->sequence(fn ($sequence) => [ + 'username' => 'public_user_'.($sequence->index + 1), + ]) + ->create(); + + Username::factory() + ->count(10) + ->active() + ->premium() + ->sequence(fn ($sequence) => [ + 'username' => 'premium_user_'.($sequence->index + 1), + ]) + ->create(); + + Username::factory() + ->count(8) + ->active() + ->gmail() + ->sequence(fn ($sequence) => [ + 'username' => 'gmail_user_'.($sequence->index + 1), + ]) + ->create(); + + Username::factory() + ->count(6) + ->active() + ->yahoo() + ->sequence(fn ($sequence) => [ + 'username' => 'yahoo_user_'.($sequence->index + 1), + ]) + ->create(); + + Username::factory() + ->count(6) + ->active() + ->outlook() + ->sequence(fn ($sequence) => [ + 'username' => 'outlook_user_'.($sequence->index + 1), + ]) + ->create(); + + Username::factory() + ->count(4) + ->active() + ->custom() + ->withExpiration() + ->sequence(fn ($sequence) => [ + 'username' => 'custom_user_'.($sequence->index + 1), + ]) + ->create(); + + // Create some inactive usernames + Username::factory() + ->count(5) + ->inactive() + ->sequence(fn ($sequence) => [ + 'username' => 'inactive_user_'.($sequence->index + 1), + ]) + ->create(); + + // Create some recently used usernames + Username::factory() + ->count(8) + ->active() + ->recentlyUsed() + ->sequence(fn ($sequence) => [ + 'username' => 'recent_user_'.($sequence->index + 1), + ]) + ->create(); + } +}