first commit

This commit is contained in:
nihonbuzz
2026-01-23 17:28:21 +07:00
commit 29ff8992b9
331 changed files with 30545 additions and 0 deletions

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

65
.env.example Normal file
View File

@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/auth.json
/.fleet
/.idea
/.nova
/.vscode
/.zed

84
BLUEPRINT.md Normal file
View File

@@ -0,0 +1,84 @@
# Nihonbuzz Academy - Master Blueprint
## 1. Project Vision
To create a premier Japanese language learning platform that combines Spaced Repetition System (SRS), structured lessons, and gamification to help students master Japanese efficiently. The platform focuses on long-term retention through smart algorithms and engaging UI.
## 2. Core Architecture
- **Backend:** Laravel 11 (PHP 8.2+)
- **Frontend:** React + TypeScript (via Inertia.js)
- **UI Framework:** TailwindCSS + Shadcn/UI
- **Database:** MySQL/MariaDB
- **Authentication:** Laravel Breeze + Socialite (Google OAuth)
## 3. Feature Roadmap
### Phase 1: Foundation (Current Focus)
- [ ] **Authentication & Identity**:
- Complete Google OAuth integration.
- Setup Role-Based Access Control (RBAC): Student, Instructor, Admin.
- Profile management (Avatar, Timezone settings for SRS).
- [ ] **Basic UI Layout**:
- Responsive Application Shell (Sidebar/Navbar).
- Dark/Light mode support.
### Phase 2: The SRS Engine (Core Value)
- [ ] **Content Database**:
- `vocabularies` table: Kanji, Kana, Meaning, Audio, Example Sentences, JLPT Level.
- [ ] **SRS Algorithm Logic**:
- Implementation of SuperMemo-2 or Anki-like algorithm.
- `srs_reviews` table: Tracks `user_id`, `vocabulary_id`, `interval`, `ease_factor`, `next_review_at`.
- [ ] **Study Interface**:
- Flashcard UI (Front/Back).
- Self-grading buttons (Again, Hard, Good, Easy).
- Keyboard shortcuts for rapid reviewing.
### Phase 3: Student Dashboard & Analytics
- [ ] **Dashboard Widgets**:
- "Reviews Due" counter.
- "New Cards" available.
- Heatmap of study activity (like GitHub contributions).
- SRS distribution chart (Learning vs. Mature cards).
- [ ] **Study Stats**:
- Retention rate graphs.
- Forecast of future reviews.
### Phase 4: Gamification & Engagement
- [ ] **Streak System**: Daily login/study tracking.
- [ ] **XP & Levels**: Points for every correct card.
- [ ] **Leaderboards**: Weekly/Monthly rankings among friends/global.
- [ ] **Badges**: Achievements for milestones (e.g., "1000 Words Learned").
### Phase 5: Administration & Content Management
- [ ] **Admin Dashboard**:
- CRUD interface for Vocabulary/Kanji (Mass import via CSV/JSON).
- User management.
- [ ] **Audio System**: Text-to-Speech integration or hosted audio files.
## 4. Technical Specifications
### Database Schema Overview
- `users`: Core user data
- `social_accounts`: OAuth links
- `vocabularies`: Dictionary data (kanji, kana, meaning, example_sentence)
- `srs_reviews`: Tracks user progress per item (next_review_date, interval, ease_factor)
- `study_sessions`: Logs of review activity
### `vocabularies` table structure
| Column | Type | Notes |
| :--- | :--- | :--- |
| id | bigint | Primary Key |
| word | string | The Kanji/Word (e.g., 猫) |
| reading | string | Kana reading (e.g., ねこ) |
| meaning | text | English/Indonesian meaning |
| parts_of_speech| string | Noun, Verb, etc. |
| level | integer | JLPT Level (5, 4, 3, 2, 1) |
### `srs_reviews` table structure
| Column | Type | Notes |
| :--- | :--- | :--- |
| id | bigint | Primary Key |
| user_id | bigint | FK to users |
| vocabulary_id | bigint | FK to vocabularies |
| interval | integer | Days until next review |
| ease_factor | float | Multiplier (default 2.5) |
| streak | integer | Consecutive correct answers |
| next_review_at | datetime | When this card becomes due |

66
README.md Normal file
View File

@@ -0,0 +1,66 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Admin\Resources\Courses;
use App\Filament\Admin\Resources\Courses\Pages\CreateCourse;
use App\Filament\Admin\Resources\Courses\Pages\EditCourse;
use App\Filament\Admin\Resources\Courses\Pages\ListCourses;
use App\Filament\Admin\Resources\Courses\Schemas\CourseForm;
use App\Filament\Admin\Resources\Courses\Tables\CoursesTable;
use App\Models\Course;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class CourseResource extends Resource
{
protected static ?string $model = Course::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return CourseForm::configure($schema);
}
public static function table(Table $table): Table
{
return CoursesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListCourses::route('/'),
'create' => CreateCourse::route('/create'),
'edit' => EditCourse::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\Courses\Pages;
use App\Filament\Admin\Resources\Courses\CourseResource;
use Filament\Resources\Pages\CreateRecord;
class CreateCourse extends CreateRecord
{
protected static string $resource = CourseResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Admin\Resources\Courses\Pages;
use App\Filament\Admin\Resources\Courses\CourseResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditCourse extends EditRecord
{
protected static string $resource = CourseResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Courses\Pages;
use App\Filament\Admin\Resources\Courses\CourseResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListCourses extends ListRecords
{
protected static string $resource = CourseResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Filament\Admin\Resources\Courses\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class CourseForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('teacher_id')
->relationship('teacher', 'name')
->required(),
TextInput::make('title')
->required(),
TextInput::make('slug')
->required(),
Textarea::make('description')
->columnSpanFull(),
Select::make('level_id')
->relationship('level', 'name'),
TextInput::make('price')
->required()
->numeric()
->default(0)
->prefix('$'),
TextInput::make('thumbnail_url')
->url(),
Toggle::make('is_published')
->required(),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Filament\Admin\Resources\Courses\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class CoursesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('teacher.name')
->searchable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')
->searchable(),
TextColumn::make('level.name')
->searchable(),
TextColumn::make('price')
->money()
->sortable(),
TextColumn::make('thumbnail_url')
->searchable(),
IconColumn::make('is_published')
->boolean(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TrashedFilter::make(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Admin\Resources\Lessons;
use App\Filament\Admin\Resources\Lessons\Pages\CreateLesson;
use App\Filament\Admin\Resources\Lessons\Pages\EditLesson;
use App\Filament\Admin\Resources\Lessons\Pages\ListLessons;
use App\Filament\Admin\Resources\Lessons\Schemas\LessonForm;
use App\Filament\Admin\Resources\Lessons\Tables\LessonsTable;
use App\Models\Lesson;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class LessonResource extends Resource
{
protected static ?string $model = Lesson::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return LessonForm::configure($schema);
}
public static function table(Table $table): Table
{
return LessonsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListLessons::route('/'),
'create' => CreateLesson::route('/create'),
'edit' => EditLesson::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\Lessons\Pages;
use App\Filament\Admin\Resources\Lessons\LessonResource;
use Filament\Resources\Pages\CreateRecord;
class CreateLesson extends CreateRecord
{
protected static string $resource = LessonResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Admin\Resources\Lessons\Pages;
use App\Filament\Admin\Resources\Lessons\LessonResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditLesson extends EditRecord
{
protected static string $resource = LessonResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Lessons\Pages;
use App\Filament\Admin\Resources\Lessons\LessonResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListLessons extends ListRecords
{
protected static string $resource = LessonResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Filament\Admin\Resources\Lessons\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class LessonForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('module_id')
->relationship('module', 'title')
->required(),
TextInput::make('title')
->required(),
TextInput::make('slug')
->required(),
TextInput::make('type')
->required()
->default('text'),
Textarea::make('content')
->columnSpanFull(),
TextInput::make('video_url')
->url(),
TextInput::make('content_pdf'),
TextInput::make('duration_seconds')
->required()
->numeric()
->default(0),
Toggle::make('is_free_preview')
->required(),
TextInput::make('order_index')
->required()
->numeric()
->default(0),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Filament\Admin\Resources\Lessons\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class LessonsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('module.title')
->searchable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')
->searchable(),
TextColumn::make('type')
->searchable(),
TextColumn::make('video_url')
->searchable(),
TextColumn::make('content_pdf')
->searchable(),
TextColumn::make('duration_seconds')
->numeric()
->sortable(),
IconColumn::make('is_free_preview')
->boolean(),
TextColumn::make('order_index')
->numeric()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TrashedFilter::make(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Filament\Admin\Resources\Levels;
use App\Filament\Admin\Resources\Levels\Pages\CreateLevel;
use App\Filament\Admin\Resources\Levels\Pages\EditLevel;
use App\Filament\Admin\Resources\Levels\Pages\ListLevels;
use App\Filament\Admin\Resources\Levels\Schemas\LevelForm;
use App\Filament\Admin\Resources\Levels\Tables\LevelsTable;
use App\Models\Level;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class LevelResource extends Resource
{
protected static ?string $model = Level::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'name';
public static function form(Schema $schema): Schema
{
return LevelForm::configure($schema);
}
public static function table(Table $table): Table
{
return LevelsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListLevels::route('/'),
'create' => CreateLevel::route('/create'),
'edit' => EditLevel::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\Levels\Pages;
use App\Filament\Admin\Resources\Levels\LevelResource;
use Filament\Resources\Pages\CreateRecord;
class CreateLevel extends CreateRecord
{
protected static string $resource = LevelResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Levels\Pages;
use App\Filament\Admin\Resources\Levels\LevelResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditLevel extends EditRecord
{
protected static string $resource = LevelResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Levels\Pages;
use App\Filament\Admin\Resources\Levels\LevelResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListLevels extends ListRecords
{
protected static string $resource = LevelResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Admin\Resources\Levels\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class LevelForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required(),
TextInput::make('code')
->required(),
Textarea::make('description')
->columnSpanFull(),
TextInput::make('order_index')
->required()
->numeric()
->default(0),
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Admin\Resources\Levels\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class LevelsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('name')
->searchable(),
TextColumn::make('code')
->searchable(),
TextColumn::make('order_index')
->numeric()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Admin\Resources\Modules;
use App\Filament\Admin\Resources\Modules\Pages\CreateModule;
use App\Filament\Admin\Resources\Modules\Pages\EditModule;
use App\Filament\Admin\Resources\Modules\Pages\ListModules;
use App\Filament\Admin\Resources\Modules\Schemas\ModuleForm;
use App\Filament\Admin\Resources\Modules\Tables\ModulesTable;
use App\Models\Module;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class ModuleResource extends Resource
{
protected static ?string $model = Module::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return ModuleForm::configure($schema);
}
public static function table(Table $table): Table
{
return ModulesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListModules::route('/'),
'create' => CreateModule::route('/create'),
'edit' => EditModule::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\Modules\Pages;
use App\Filament\Admin\Resources\Modules\ModuleResource;
use Filament\Resources\Pages\CreateRecord;
class CreateModule extends CreateRecord
{
protected static string $resource = ModuleResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Admin\Resources\Modules\Pages;
use App\Filament\Admin\Resources\Modules\ModuleResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditModule extends EditRecord
{
protected static string $resource = ModuleResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Modules\Pages;
use App\Filament\Admin\Resources\Modules\ModuleResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListModules extends ListRecords
{
protected static string $resource = ModuleResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Filament\Admin\Resources\Modules\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class ModuleForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('course_id')
->relationship('course', 'title')
->required(),
TextInput::make('title')
->required(),
Textarea::make('description')
->columnSpanFull(),
TextInput::make('order_index')
->required()
->numeric()
->default(0),
Toggle::make('is_free_preview')
->required(),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Admin\Resources\Modules\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class ModulesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('course.title')
->searchable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('order_index')
->numeric()
->sortable(),
IconColumn::make('is_free_preview')
->boolean(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TrashedFilter::make(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\Vocabularies\Pages;
use App\Filament\Admin\Resources\Vocabularies\VocabularyResource;
use Filament\Resources\Pages\CreateRecord;
class CreateVocabulary extends CreateRecord
{
protected static string $resource = VocabularyResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Vocabularies\Pages;
use App\Filament\Admin\Resources\Vocabularies\VocabularyResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditVocabulary extends EditRecord
{
protected static string $resource = VocabularyResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Vocabularies\Pages;
use App\Filament\Admin\Resources\Vocabularies\VocabularyResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListVocabularies extends ListRecords
{
protected static string $resource = VocabularyResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Admin\Resources\Vocabularies\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class VocabularyForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('word')
->required(),
TextInput::make('reading'),
TextInput::make('romaji'),
TextInput::make('meaning_en'),
TextInput::make('meaning_id'),
TextInput::make('level_id'),
TextInput::make('audio_url')
->url(),
TextInput::make('type'),
Textarea::make('stroke_order_svg')
->columnSpanFull(),
Textarea::make('example_sentences')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Admin\Resources\Vocabularies\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class VocabulariesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('word')
->searchable(),
TextColumn::make('reading')
->searchable(),
TextColumn::make('romaji')
->searchable(),
TextColumn::make('meaning_en')
->searchable(),
TextColumn::make('meaning_id')
->searchable(),
TextColumn::make('level_id')
->searchable(),
TextColumn::make('audio_url')
->searchable(),
TextColumn::make('type')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Filament\Admin\Resources\Vocabularies;
use App\Filament\Admin\Resources\Vocabularies\Pages\CreateVocabulary;
use App\Filament\Admin\Resources\Vocabularies\Pages\EditVocabulary;
use App\Filament\Admin\Resources\Vocabularies\Pages\ListVocabularies;
use App\Filament\Admin\Resources\Vocabularies\Schemas\VocabularyForm;
use App\Filament\Admin\Resources\Vocabularies\Tables\VocabulariesTable;
use App\Models\Vocabulary;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class VocabularyResource extends Resource
{
protected static ?string $model = Vocabulary::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?string $recordTitleAttribute = 'word';
public static function form(Schema $schema): Schema
{
return VocabularyForm::configure($schema);
}
public static function table(Table $table): Table
{
return VocabulariesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListVocabularies::route('/'),
'create' => CreateVocabulary::route('/create'),
'edit' => EditVocabulary::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Resources\Courses;
use App\Filament\Resources\Courses\Pages\CreateCourse;
use App\Filament\Resources\Courses\Pages\EditCourse;
use App\Filament\Resources\Courses\Pages\ListCourses;
use App\Filament\Resources\Courses\Schemas\CourseForm;
use App\Filament\Resources\Courses\Tables\CoursesTable;
use App\Models\Course;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class CourseResource extends Resource
{
protected static ?string $model = Course::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
protected static ?int $navigationSort = 1;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\TextInput::make('title')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(fn ($state, $set) => $set('slug', \Illuminate\Support\Str::slug($state))),
Forms\Components\TextInput::make('slug')
->required()
->maxLength(255)
->unique(ignoreRecord: true),
Forms\Components\RichEditor::make('description')
->columnSpanFull(),
Forms\Components\Select::make('level_id')
->label('JLPT Level')
->relationship('level', 'code')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('price')
->numeric()
->prefix('IDR')
->maxValue(42949672.95),
Forms\Components\FileUpload::make('thumbnail_url')
->label('Thumbnail Image')
->image()
->disk('r2')
->directory('thumbnails')
->visibility('public')
->imageEditor(),
Forms\Components\Select::make('teacher_id')
->relationship('teacher', 'name')
->searchable()
->preload()
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\ImageColumn::make('thumbnail_url')
->disk('r2')
->visibility('public'),
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('level.code')
->label('Level')
->badge(),
Tables\Columns\TextColumn::make('price')
->money('IDR'),
Tables\Columns\TextColumn::make('teacher.name')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListCourses::route('/'),
'create' => CreateCourse::route('/create'),
'edit' => EditCourse::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Courses\Pages;
use App\Filament\Resources\Courses\CourseResource;
use Filament\Resources\Pages\CreateRecord;
class CreateCourse extends CreateRecord
{
protected static string $resource = CourseResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Resources\Courses\Pages;
use App\Filament\Resources\Courses\CourseResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditCourse extends EditRecord
{
protected static string $resource = CourseResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Courses\Pages;
use App\Filament\Resources\Courses\CourseResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListCourses extends ListRecords
{
protected static string $resource = CourseResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Filament\Resources\Courses\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class CourseForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('teacher_id')
->relationship('teacher', 'name')
->required(),
TextInput::make('title')
->required(),
TextInput::make('slug')
->required(),
Textarea::make('description')
->columnSpanFull(),
TextInput::make('level')
->required()
->default('N5'),
TextInput::make('price')
->required()
->numeric()
->default(0)
->prefix('$'),
TextInput::make('thumbnail_url')
->url(),
Toggle::make('is_published')
->required(),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Filament\Resources\Courses\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class CoursesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('teacher.name')
->searchable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')
->searchable(),
TextColumn::make('level')
->searchable(),
TextColumn::make('price')
->money()
->sortable(),
TextColumn::make('thumbnail_url')
->searchable(),
IconColumn::make('is_published')
->boolean(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TrashedFilter::make(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Resources\Lessons;
use App\Filament\Resources\Lessons\Pages\CreateLesson;
use App\Filament\Resources\Lessons\Pages\EditLesson;
use App\Filament\Resources\Lessons\Pages\ListLessons;
use App\Filament\Resources\Lessons\Schemas\LessonForm;
use App\Filament\Resources\Lessons\Tables\LessonsTable;
use App\Models\Lesson;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class LessonResource extends Resource
{
protected static ?string $model = Lesson::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-play-circle';
protected static ?int $navigationSort = 3;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\Select::make('module_id')
->relationship('module', 'title')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('title')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(fn ($state, $set) => $set('slug', \Illuminate\Support\Str::slug($state))),
Forms\Components\TextInput::make('slug')
->required()
->maxLength(255)
->unique(ignoreRecord: true),
Forms\Components\Select::make('type')
->options([
'video' => 'Video',
'text' => 'Text',
'quiz' => 'Quiz',
'vocab_list' => 'Vocabulary List',
])
->default('text')
->required(),
Forms\Components\FileUpload::make('content_pdf')
->label('Lesson PDF (Paid Content)')
->disk('r2_private')
->directory('pdfs')
->visibility('private')
->acceptedFileTypes(['application/pdf'])
->downloadable()
->columnSpanFull(),
Forms\Components\RichEditor::make('content')
->columnSpanFull(),
Forms\Components\TextInput::make('video_url')
->label('Video URL (YouTube/Vimeo)')
->url(),
Forms\Components\Toggle::make('is_free_preview')
->label('Free Preview?')
->default(false),
Forms\Components\TextInput::make('order_index')
->numeric()
->default(0),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('module.title')
->sortable(),
Tables\Columns\TextColumn::make('type')
->badge(),
Tables\Columns\IconColumn::make('is_free_preview')
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('order_index', 'asc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListLessons::route('/'),
'create' => CreateLesson::route('/create'),
'edit' => EditLesson::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Lessons\Pages;
use App\Filament\Resources\Lessons\LessonResource;
use Filament\Resources\Pages\CreateRecord;
class CreateLesson extends CreateRecord
{
protected static string $resource = LessonResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Resources\Lessons\Pages;
use App\Filament\Resources\Lessons\LessonResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditLesson extends EditRecord
{
protected static string $resource = LessonResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Lessons\Pages;
use App\Filament\Resources\Lessons\LessonResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListLessons extends ListRecords
{
protected static string $resource = LessonResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Filament\Resources\Lessons\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class LessonForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('module_id')
->relationship('module', 'title')
->required(),
TextInput::make('title')
->required(),
TextInput::make('slug')
->required(),
TextInput::make('type')
->required()
->default('text'),
Textarea::make('content')
->columnSpanFull(),
TextInput::make('video_url')
->url(),
TextInput::make('duration_seconds')
->required()
->numeric()
->default(0),
Toggle::make('is_free_preview')
->required(),
TextInput::make('order_index')
->required()
->numeric()
->default(0),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Filament\Resources\Lessons\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class LessonsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('module.title')
->searchable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')
->searchable(),
TextColumn::make('type')
->searchable(),
TextColumn::make('video_url')
->searchable(),
TextColumn::make('duration_seconds')
->numeric()
->sortable(),
IconColumn::make('is_free_preview')
->boolean(),
TextColumn::make('order_index')
->numeric()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TrashedFilter::make(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Filament\Resources\Levels;
use App\Filament\Resources\Levels\Pages\CreateLevel;
use App\Filament\Resources\Levels\Pages\EditLevel;
use App\Filament\Resources\Levels\Pages\ListLevels;
use App\Filament\Resources\Levels\Schemas\LevelForm;
use App\Filament\Resources\Levels\Tables\LevelsTable;
use App\Models\Level;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
class LevelResource extends Resource
{
protected static ?string $model = Level::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-signal';
protected static ?int $navigationSort = 10;
protected static ?string $recordTitleAttribute = 'code';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\TextInput::make('code')
->required()
->maxLength(255)
->unique(ignoreRecord: true),
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\Textarea::make('description')
->maxLength(65535)
->columnSpanFull(),
Forms\Components\TextInput::make('order_index')
->numeric()
->default(0),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('code')
->badge()
->searchable(),
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('order_index')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('order_index', 'asc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListLevels::route('/'),
'create' => CreateLevel::route('/create'),
'edit' => EditLevel::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Levels\Pages;
use App\Filament\Resources\Levels\LevelResource;
use Filament\Resources\Pages\CreateRecord;
class CreateLevel extends CreateRecord
{
protected static string $resource = LevelResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Levels\Pages;
use App\Filament\Resources\Levels\LevelResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditLevel extends EditRecord
{
protected static string $resource = LevelResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Levels\Pages;
use App\Filament\Resources\Levels\LevelResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListLevels extends ListRecords
{
protected static string $resource = LevelResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Resources\Levels\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class LevelForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required(),
TextInput::make('code')
->required(),
Textarea::make('description')
->columnSpanFull(),
TextInput::make('order_index')
->required()
->numeric()
->default(0),
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Resources\Levels\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class LevelsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('name')
->searchable(),
TextColumn::make('code')
->searchable(),
TextColumn::make('order_index')
->numeric()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Filament\Resources\Modules;
use App\Filament\Resources\Modules\Pages\CreateModule;
use App\Filament\Resources\Modules\Pages\EditModule;
use App\Filament\Resources\Modules\Pages\ListModules;
use App\Filament\Resources\Modules\Schemas\ModuleForm;
use App\Filament\Resources\Modules\Tables\ModulesTable;
use App\Models\Module;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class ModuleResource extends Resource
{
protected static ?string $model = Module::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-group';
protected static ?int $navigationSort = 2;
protected static ?string $recordTitleAttribute = 'title';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\Select::make('course_id')
->relationship('course', 'title')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('title')
->required()
->maxLength(255),
Forms\Components\RichEditor::make('description')
->columnSpanFull(),
Forms\Components\TextInput::make('order_index')
->numeric()
->default(0),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('course.title')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('order_index')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('order_index', 'asc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListModules::route('/'),
'create' => CreateModule::route('/create'),
'edit' => EditModule::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Modules\Pages;
use App\Filament\Resources\Modules\ModuleResource;
use Filament\Resources\Pages\CreateRecord;
class CreateModule extends CreateRecord
{
protected static string $resource = ModuleResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Resources\Modules\Pages;
use App\Filament\Resources\Modules\ModuleResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditModule extends EditRecord
{
protected static string $resource = ModuleResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Modules\Pages;
use App\Filament\Resources\Modules\ModuleResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListModules extends ListRecords
{
protected static string $resource = ModuleResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Filament\Resources\Modules\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class ModuleForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('course_id')
->relationship('course', 'title')
->required(),
TextInput::make('title')
->required(),
Textarea::make('description')
->columnSpanFull(),
TextInput::make('order_index')
->required()
->numeric()
->default(0),
Toggle::make('is_free_preview')
->required(),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Resources\Modules\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class ModulesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('course.title')
->searchable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('order_index')
->numeric()
->sortable(),
IconColumn::make('is_free_preview')
->boolean(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TrashedFilter::make(),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Roles\Pages;
use App\Filament\Resources\Roles\RoleResource;
use Filament\Resources\Pages\CreateRecord;
class CreateRole extends CreateRecord
{
protected static string $resource = RoleResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Roles\Pages;
use App\Filament\Resources\Roles\RoleResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditRole extends EditRecord
{
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Roles\Pages;
use App\Filament\Resources\Roles\RoleResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListRoles extends ListRecords
{
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Filament\Resources\Roles;
use App\Filament\Resources\Roles\Pages\CreateRole;
use App\Filament\Resources\Roles\Pages\EditRole;
use App\Filament\Resources\Roles\Pages\ListRoles;
use App\Filament\Resources\Roles\Schemas\RoleForm;
use App\Filament\Resources\Roles\Tables\RolesTable;
use App\Models\Role;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
class RoleResource extends Resource
{
protected static ?string $model = Role::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?int $navigationSort = 91;
protected static ?string $recordTitleAttribute = 'name';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('guard_name')
->required()
->maxLength(255)
->default('web'),
Forms\Components\Select::make('permissions')
->relationship('permissions', 'name')
->multiple()
->preload(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('guard_name')
->searchable(),
Tables\Columns\TextColumn::make('permissions_count')
->counts('permissions')
->label('Perms'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListRoles::route('/'),
'create' => CreateRole::route('/create'),
'edit' => EditRole::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Roles\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class RoleForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required(),
TextInput::make('guard_name')
->required(),
]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Filament\Resources\Roles\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class RolesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('name')
->searchable(),
TextColumn::make('guard_name')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Filament\Resources\Users\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class UserForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required(),
TextInput::make('email')
->label('Email address')
->email()
->required(),
DateTimePicker::make('email_verified_at'),
TextInput::make('password')
->password(),
TextInput::make('avatar_url')
->url(),
TextInput::make('xp_points')
->required()
->numeric()
->default(0),
TextInput::make('current_streak')
->required()
->numeric()
->default(0),
Textarea::make('metadata')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Resources\Users\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class UsersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('name')
->searchable(),
TextColumn::make('email')
->label('Email address')
->searchable(),
TextColumn::make('email_verified_at')
->dateTime()
->sortable(),
TextColumn::make('avatar_url')
->searchable(),
TextColumn::make('xp_points')
->numeric()
->sortable(),
TextColumn::make('current_streak')
->numeric()
->sortable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Filament\Resources\Users;
use App\Filament\Resources\Users\Pages\CreateUser;
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Filament\Resources\Users\Schemas\UserForm;
use App\Filament\Resources\Users\Tables\UsersTable;
use App\Models\User;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?int $navigationSort = 90;
protected static ?string $recordTitleAttribute = 'name';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->maxLength(255),
Forms\Components\DateTimePicker::make('email_verified_at'),
Forms\Components\TextInput::make('password')
->password()
->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null)
->required(fn (string $context): bool => $context === 'create')
->maxLength(255),
Forms\Components\Select::make('roles')
->relationship('roles', 'name')
->multiple()
->preload(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('roles.name')
->badge(),
Tables\Columns\TextColumn::make('email_verified_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Vocabularies\Pages;
use App\Filament\Resources\Vocabularies\VocabularyResource;
use Filament\Resources\Pages\CreateRecord;
class CreateVocabulary extends CreateRecord
{
protected static string $resource = VocabularyResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Vocabularies\Pages;
use App\Filament\Resources\Vocabularies\VocabularyResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditVocabulary extends EditRecord
{
protected static string $resource = VocabularyResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Vocabularies\Pages;
use App\Filament\Resources\Vocabularies\VocabularyResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListVocabularies extends ListRecords
{
protected static string $resource = VocabularyResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\Vocabularies\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class VocabularyForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('word')
->required(),
TextInput::make('reading'),
TextInput::make('romaji'),
TextInput::make('meaning_en'),
TextInput::make('meaning_id'),
TextInput::make('level'),
TextInput::make('audio_url')
->url(),
TextInput::make('type'),
Textarea::make('stroke_order_svg')
->columnSpanFull(),
Textarea::make('example_sentences')
->columnSpanFull(),
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Resources\Vocabularies\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class VocabulariesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('word')
->searchable(),
TextColumn::make('reading')
->searchable(),
TextColumn::make('romaji')
->searchable(),
TextColumn::make('meaning_en')
->searchable(),
TextColumn::make('meaning_id')
->searchable(),
TextColumn::make('level')
->searchable(),
TextColumn::make('audio_url')
->searchable(),
TextColumn::make('type')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Filament\Resources\Vocabularies;
use App\Filament\Resources\Vocabularies\Pages\CreateVocabulary;
use App\Filament\Resources\Vocabularies\Pages\EditVocabulary;
use App\Filament\Resources\Vocabularies\Pages\ListVocabularies;
use App\Filament\Resources\Vocabularies\Schemas\VocabularyForm;
use App\Filament\Resources\Vocabularies\Tables\VocabulariesTable;
use App\Models\Vocabulary;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Components as Schemas;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables;
use Filament\Tables\Table;
class VocabularyResource extends Resource
{
protected static ?string $model = Vocabulary::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-language';
protected static ?int $navigationSort = 4;
protected static ?string $recordTitleAttribute = 'word';
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Schemas\Section::make('Core Info')
->schema([
Forms\Components\TextInput::make('word')
->label('Word (Kanji/Kana)')
->required()
->maxLength(255),
Forms\Components\TextInput::make('reading')
->label('Reading (Kana)')
->required()
->maxLength(255),
Forms\Components\TextInput::make('romaji')
->maxLength(255),
Forms\Components\TextInput::make('meaning_id')
->label('Meaning (Bahasa Indonesia)')
->required()
->maxLength(255),
Forms\Components\TextInput::make('meaning_en')
->label('Meaning (English)')
->required()
->maxLength(255),
])->columns(2),
Schemas\Section::make('Metadata & Media')
->schema([
Forms\Components\Select::make('level_id')
->label('JLPT Level')
->relationship('level', 'code')
->searchable()
->preload()
->required(),
Forms\Components\Select::make('type')
->options([
'noun' => 'Noun',
'verb' => 'Verb',
'adjective' => 'Adjective',
'adverb' => 'Adverb',
'particle' => 'Particle',
'expression' => 'Expression',
])
->required(),
Forms\Components\FileUpload::make('audio_url')
->label('Pronunciation Audio')
->disk('r2')
->directory('audio/vocab')
->visibility('public')
->acceptedFileTypes(['audio/mpeg', 'audio/wav', 'audio/ogg']),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('word')
->label('Word')
->searchable()
->description(fn (Vocabulary $record): string => $record->reading),
Tables\Columns\TextColumn::make('meaning_id')
->label('Meaning')
->searchable(),
Tables\Columns\TextColumn::make('level.code')
->label('Level')
->badge()
->color(fn (?string $state): string => match ($state) {
'N1' => 'danger',
'N2' => 'warning',
'N3' => 'info',
default => 'success',
}),
Tables\Columns\TextColumn::make('type')
->badge(),
Tables\Columns\IconColumn::make('audio_url')
->label('Audio')
->icon('heroicon-o-speaker-wave')
->color(fn ($state) => $state ? 'success' : 'gray'),
])
->filters([
Tables\Filters\SelectFilter::make('level_id')
->label('Level')
->relationship('level', 'code'),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListVocabularies::route('/'),
'create' => CreateVocabulary::route('/create'),
'edit' => EditVocabulary::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
use App\Services\SocialAuthService;
class AuthenticatedSessionController extends Controller
{
public function __construct(
protected SocialAuthService $socialAuthService
) {}
/**
* Display the login view.
*/
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
$user = Auth::user();
// Revoke Google tokens if present
if ($user) {
$user->socialAccounts()->where('provider', 'google')->get()->each(function ($account) {
$this->socialAuthService->revokeToken($account);
});
}
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): Response
{
return Inertia::render('Auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\SocialAccount;
use Exception;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Str;
class SocialAuthController extends Controller
{
/**
* Redirect to the provider's authentication page.
*/
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
/**
* Obtain the user information from the provider.
*/
public function handleProviderCallback($provider)
{
try {
$socialUser = Socialite::driver($provider)->user();
} catch (Exception $e) {
return redirect()->route('login')->with('error', 'Authentication failed.');
}
$user = $this->findOrCreateUser($socialUser, $provider);
Auth::login($user, true);
return redirect()->intended(route('dashboard'));
}
/**
* Find or create a user based on social account information.
*/
protected function findOrCreateUser($socialUser, $provider)
{
$account = SocialAccount::where('provider', $provider)
->where('provider_id', $socialUser->getId())
->first();
if ($account) {
// Update tokens
$account->update([
'token' => $socialUser->token,
'refresh_token' => $socialUser->refreshToken,
'expires_at' => $socialUser->expiresIn ? now()->addSeconds($socialUser->expiresIn) : null,
]);
return $account->user;
}
// Check if user with same email exists
$user = User::where('email', $socialUser->getEmail())->first();
if (!$user) {
// Create a new user
$user = User::create([
'name' => $socialUser->getName() ?? $socialUser->getNickname() ?? 'User',
'email' => $socialUser->getEmail(),
'avatar_url' => $socialUser->getAvatar(),
'password' => null, // Social users don't need a local password initially
]);
// Assign default role
$user->assignRole('student');
}
// Link social account
SocialAccount::create([
'user_id' => $user->id,
'provider' => $provider,
'provider_id' => $socialUser->getId(),
'token' => $socialUser->token,
'refresh_token' => $socialUser->refreshToken,
'expires_at' => $socialUser->expiresIn ? now()->addSeconds($socialUser->expiresIn) : null,
]);
return $user;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Models\Course;
use App\Models\Level;
use App\Models\Enrollment;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class CourseLibraryController extends Controller
{
/**
* Display the course library.
*/
public function index(): Response
{
$levels = Level::with(['courses' => function($query) {
$query->withCount('modules');
}])->get();
$enrolledCourseIds = Enrollment::where('user_id', auth()->id())
->pluck('course_id')
->toArray();
return Inertia::render('Courses/Library', [
'levels' => $levels->map(function($level) use ($enrolledCourseIds) {
return [
'id' => $level->id,
'name' => $level->name,
'code' => $level->code,
'courses' => $level->courses->map(function($course) use ($enrolledCourseIds) {
return [
'id' => $course->id,
'title' => $course->title,
'description' => $course->description,
'thumbnail' => $course->thumbnail_url,
'slug' => $course->slug,
'modulesCount' => $course->modules_count,
'isEnrolled' => in_array($course->id, $enrolledCourseIds),
];
})
];
})
]);
}
/**
* Enroll in a course.
*/
public function enroll(Request $request, Course $course)
{
$user = auth()->user();
// Check if already enrolled
$exists = Enrollment::where('user_id', $user->id)
->where('course_id', $course->id)
->exists();
if (!$exists) {
Enrollment::create([
'user_id' => $user->id,
'course_id' => $course->id,
'status' => 'active',
'enrolled_at' => now(),
]);
}
return redirect()->route('courses.learn', ['course' => $course->slug])
->with('status', "Berhasil mendaftar di kursus: {$course->title}");
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers;
use App\Models\Course;
use App\Models\Lesson;
use App\Models\Enrollment;
use App\Models\UserProgress;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class CoursePlayerController extends Controller
{
/**
* Display the course player.
*/
public function show(Request $request, string $courseSlug, string $lessonSlug = null): Response
{
$user = $request->user();
$course = Course::where('slug', $courseSlug)
->with(['modules.lessons'])
->firstOrFail();
// Check enrollment
$isEnrolled = Enrollment::where('user_id', $user->id)
->where('course_id', $course->id)
->exists();
if (!$isEnrolled) {
return Inertia::render('Errors/Unauthorized', [
'message' => 'Anda belum terdaftar di kursus ini.'
]);
}
// Get all lessons for navigation and progress check
$allLessons = $course->modules->flatMap->lessons;
// Find current lesson
$currentLesson = $lessonSlug
? Lesson::where('slug', $lessonSlug)->firstOrFail()
: $allLessons->first();
// Get user progress for this course
$completedLessonsIds = UserProgress::where('user_id', $user->id)
->whereIn('lesson_id', $allLessons->pluck('id'))
->pluck('lesson_id')
->toArray();
return Inertia::render('Courses/Player', [
'course' => [
'id' => $course->id,
'title' => $course->title,
'slug' => $course->slug,
'modules' => $course->modules->map(function ($module) use ($completedLessonsIds) {
return [
'id' => $module->id,
'title' => $module->title,
'lessons' => $module->lessons->map(function ($lesson) use ($completedLessonsIds) {
return [
'id' => $lesson->id,
'title' => $lesson->title,
'slug' => $lesson->slug,
'type' => $lesson->type,
'is_completed' => in_array($lesson->id, $completedLessonsIds),
];
})
];
})
],
'currentLesson' => [
'id' => $currentLesson->id,
'title' => $currentLesson->title,
'slug' => $currentLesson->slug,
'type' => $currentLesson->type,
'content' => $currentLesson->content,
'video_url' => $currentLesson->video_url,
'content_pdf' => $currentLesson->content_pdf,
],
'progress' => [
'completed_count' => count($completedLessonsIds),
'total_count' => count($allLessons),
'percentage' => count($allLessons) > 0 ? round((count($completedLessonsIds) / count($allLessons)) * 100) : 0,
]
]);
}
/**
* Mark a lesson as completed.
*/
public function complete(Request $request, Lesson $lesson)
{
$user = $request->user();
UserProgress::updateOrCreate(
['user_id' => $user->id, 'lesson_id' => $lesson->id],
['completed_at' => now()]
);
// Optional: Add XP points logic here later
return back()->with('success', 'Materi selesai!');
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\Course;
use App\Models\Enrollment;
use Illuminate\Http\Request;
use App\Services\SrsService;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
protected $srsService;
public function __construct(SrsService $srsService)
{
$this->srsService = $srsService;
}
/**
* Display the student dashboard.
*/
public function index(Request $request): Response
{
$user = $request->user();
// Fetch enrolled courses with their progress
$enrolledCourses = Enrollment::where('user_id', $user->id)
->with(['course' => function($query) {
$query->withCount('modules');
$query->with(['level']);
}])
->get()
->map(function ($enrollment) use ($user) {
$course = $enrollment->course;
// Calculate progress (this logic will be refined in the future)
// For now, we'll try to find any completed lessons in this course
$totalLessons = \App\Models\Lesson::whereIn('module_id', $course->modules->pluck('id'))->count();
$completedLessonsCount = \App\Models\UserProgress::where('user_id', $user->id)
->whereIn('lesson_id', function($query) use ($course) {
$query->select('id')
->from('lessons')
->whereIn('module_id', $course->modules->pluck('id'));
})
->count();
$progress = $totalLessons > 0 ? round(($completedLessonsCount / $totalLessons) * 100) : 0;
return [
'id' => $course->id,
'title' => $course->title,
'thumbnail' => $course->thumbnail_url,
'level' => $course->level->code ?? 'Basic',
'progress' => $progress,
'lessonsCount' => $totalLessons,
'completedLessons' => $completedLessonsCount,
'slug' => $course->slug,
];
});
// Fetch Real SRS Stats
$dueCount = $this->srsService->getDueReviews($user, 1000)->count();
$newCount = $this->srsService->getNewCards($user, 1000)->count();
return Inertia::render('Dashboard', [
'stats' => [
'xp_points' => $user->xp_points ?? 0,
'current_streak' => $user->current_streak ?? 0,
'active_courses' => $enrolledCourses->count(),
'certificates' => 0,
'srs_due' => $dueCount,
'srs_new' => $newCount,
],
'activeCourses' => $enrolledCourses,
'user' => [
'name' => $user->name,
'avatar' => $user->avatar_url,
'rank' => 'Genin',
'xp_points' => $user->xp_points ?? 0,
]
]);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers;
use App\Models\Vocabulary;
use App\Models\SrsReview;
use App\Services\SrsService;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SrsController extends Controller
{
protected $srsService;
public function __construct(SrsService $srsService)
{
$this->srsService = $srsService;
}
public function index()
{
$user = auth()->user();
$dueCount = $this->srsService->getDueReviews($user, 1000)->count();
$newCount = $this->srsService->getNewCards($user, 1000)->count();
return Inertia::render('Srs/Index', [
'stats' => [
'due' => $dueCount,
'new' => $newCount,
'total_learned' => SrsReview::where('user_id', $user->id)->count()
]
]);
}
public function practice()
{
$user = auth()->user();
$reviews = $this->srsService->getDueReviews($user, 20);
// If Reviews < 10, fill with New Cards
$newCards = $reviews->count() < 10
? $this->srsService->getNewCards($user, 10 - $reviews->count())
: collect([]);
// Normalize items for frontend
$items = $reviews->toBase()->map(function($review) {
return [
'type' => 'review',
'id' => $review->vocabulary->id,
'word' => $review->vocabulary->word,
'reading' => $review->vocabulary->reading,
'meaning' => $review->vocabulary->meaning_en,
'audio_url' => $review->vocabulary->audio_url,
'srs_id' => $review->id
];
})->merge($newCards->toBase()->map(function($vocab) {
return [
'type' => 'new',
'id' => $vocab->id,
'word' => $vocab->word,
'reading' => $vocab->reading,
'meaning' => $vocab->meaning_en,
'audio_url' => $vocab->audio_url,
];
}));
return Inertia::render('Srs/Practice', [
'items' => $items
]);
}
public function store(Request $request)
{
$request->validate([
'vocabulary_id' => 'required|exists:vocabularies,id',
'grade' => 'required|integer|min:1|max:4' // 1=Again, 2=Hard, 3=Good, 4=Easy
]);
$user = auth()->user();
$vocab = Vocabulary::find($request->vocabulary_id);
$this->srsService->processReview($user, $vocab, $request->grade);
if ($request->wantsJson()) {
return response()->json(['status' => 'success']);
}
return back();
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

43
app/Models/Course.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Course extends Model
{
use HasFactory, HasUuids, SoftDeletes;
protected $guarded = [];
protected $casts = [
'metadata' => 'array',
'is_published' => 'boolean',
'price' => 'decimal:2',
];
public function level(): BelongsTo
{
return $this->belongsTo(Level::class);
}
public function enrollments(): HasMany
{
return $this->hasMany(Enrollment::class);
}
public function modules(): HasMany
{
return $this->hasMany(Module::class)->orderBy('order_index');
}
public function teacher(): BelongsTo
{
return $this->belongsTo(User::class, 'teacher_id');
}
}

28
app/Models/Enrollment.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Enrollment extends Model
{
use HasUuids;
protected $guarded = [];
protected $casts = [
'enrolled_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function course(): BelongsTo
{
return $this->belongsTo(Course::class);
}
}

26
app/Models/Lesson.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Lesson extends Model
{
use HasFactory, HasUuids, SoftDeletes;
protected $guarded = [];
protected $casts = [
'metadata' => 'array',
'is_free_preview' => 'boolean',
];
public function module(): BelongsTo
{
return $this->belongsTo(Module::class);
}
}

Some files were not shown because too many files have changed in this diff Show More