Archived
1
0
Fork 0

initial commit

This commit is contained in:
Henrik Hautakoski 2021-06-28 17:33:29 +01:00
commit 1e1aa7d461
215 changed files with 35140 additions and 0 deletions

View file

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
class CreateUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:create {username} {password} {--admin}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a user';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$username = $this->argument('username');
$password = $this->argument('password');
$role = 'user';
if ($this->option('admin')) {
$role = 'admin';
}
User::create([
'username' => $username,
'password' => Hash::make($password),
'role' => $role
]);
$this->info('Created');
return 0;
}
}

39
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SessionController extends Controller
{
/**
* Handle an incoming authentication request.
*
* @param \App\Http\Requests\Auth\LoginRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(LoginRequest $request)
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended('/');
}
/**
* Destroy an authenticated session.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request)
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use App\Models\Character;
use App\Http\Requests\CharacterRequest;
class CharacterController extends Controller
{
public function index()
{
return view('character.list', [
'items' => Character::with('professions')->orderBy('name')->get(),
]);
}
public function show(Character $character)
{
return view('character.show', [
'character' => $character
]);
}
public function create()
{
$this->authorize('create', Character::class);
return view('character.create');
}
public function destroy(Character $character)
{
$this->authorize('delete', $character);
$character->delete();
return redirect()->back()
->with(['success' => "<strong>{$character->name}</strong> was deleted!"]);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Models\Character;
use App\Models\Profession;
use App\Models\Recipe;
use App\Models\CharacterProfession;
use App\Http\Requests\CharacterProfessionRequest;
use App\Jobs\ImportProfession;
class CharacterProfessionController extends Controller
{
public function show(Character $character, Profession $profession)
{
$ch_prof = CharacterProfession::where('character_id', $character->id)
->where('profession_id', $profession->id)
->with(['character', 'recipes.craft', 'recipes.category'])
->firstOrFail();
return view('character.profession.show', [
'ch_prof' => $ch_prof,
]);
}
public function create(Character $character)
{
$this->authorize('import_profession', $character);
return view('character.profession.create', [
'character' => $character
]);
}
public function store(Character $character, CharacterProfessionRequest $request)
{
$this->authorize('import_profession', $character);
$request->validated();
$data = json_decode($request->input('data'));
try {
ImportProfession::dispatch($character, $data);
} catch(\App\ProfessionImport\Exception $e) {
return redirect()->back()->with('error', $e->getMessage());
}
return redirect()->back()->with('success', 'Profession imported!');
}
public function destroy(Character $character, Profession $profession)
{
$ch_prof = CharacterProfession::where('character_id', $character->id)
->where('profession_id', $profession->id)->firstOrFail();
$this->authorize('delete', $ch_prof);
$ch_prof->delete();
return redirect()->back()
->with(['success' => "<strong>Profession</strong> was deleted!"]);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers;
use App\Models\Recipe;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class RecipeController extends Controller
{
/**
* List all recipies
*/
public function index()
{
return view('recipe.index');
}
/**
* Show a recipe
*/
public function show(Recipe $recipe)
{
$recipe->load([
'reagents' => function($q) {
$q->orderBy('name');
},
'crafters' => function($q) {
$q->orderBy('name');
}
]);
return view('recipe.show', [
'recipe' => $recipe
]);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\Request;
use App\Http\Requests\UserRequest;
class UserController extends Controller
{
public function index(Request $request)
{
return view('user.index', [
'user' => Auth::user(),
]);
}
public function edit(Request $request)
{
return view('user.edit', [
'user' => Auth::user(),
]);
}
public function update(UserRequest $request)
{
$data = $request->validated();
$user = $request->user();
$user->password = Hash::make($data['password']);
$user->save();
return redirect()->route('user.index')
->with('success', 'Password was updated');
}
}

66
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,66 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

View file

@ -0,0 +1,121 @@
<?php
namespace App\Http\Livewire\Form;
use App\Models\Character;
use App\Warcraft\Classes;
use App\Warcraft\Races;
use Livewire\Component;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class CreateCharacterForm extends Component
{
use AuthorizesRequests;
/**
* Character's name
*/
public string $name = '';
/**
* Character's level
*/
public string $level = '70';
/**
* All possible classes
*/
public $classes;
/**
* Character's class
*/
public string $class = '';
/**
* All possible races
*/
public $races;
/**
* Character's race
*/
public string $race = '';
/**
* All posible genders
*/
public $genders = [
'M' => 'Male',
'F' => 'Female'
];
/**
* Character gender
*/
public $gender = 'M';
/**
* Validation rules
*/
protected $rules = [
'name' => 'required|alpha|min:2|max:12|unique:characters,name',
'level' => 'required|integer|min:1|max:70',
'gender' => 'required|in:M,F',
'race' => 'required',
'class' => 'required'
];
public function mount()
{
$this->races = (new Races)->alliance();
$rules['race'] = 'required|in:' . $this->races->keys()->join(',');
$this->race = $this->races->keys()->first();
$this->updatedRace($this->race);
}
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
public function updatedRace($race)
{
// Update classes list for this race.
$this->classes = (new Classes)->race($this->race);
// Update validation rules
$rules['class'] = 'required|in:' . $this->classes->keys()->join(',');
// if this race can not be the selected class.
// select the first one.
if (!$this->classes->has($this->class)) {
$this->class = $this->classes->keys()->first();
}
}
/**
* Save the character
*/
public function save()
{
$this->authorize('create', Character::class);
$data = $this->validate();
$user = auth()->user();
$character = $user->characters()->create($data);
// Livewire redirect() does not have "with" method.
// so we call session()->flash() directly instead.
session()->flash('success', "<strong>{$character->name}</strong> was created!");
return redirect()->route('user.index');
}
public function render()
{
return view('livewire.form.character');
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace App\Http\Livewire;
use App\Models\Profession;
use App\Models\Recipe;
use Illuminate\Support\Str;
use Livewire\Component;
class Recipes extends Component
{
use Traits\WithPagination;
protected $queryString = [
'name' => ['except' => ''],
'crafter' => ['except' => ''],
'profession' => ['except' => '']
];
/**
* Filter by name
*/
public string $name = '';
/**
* Filter by profession
*/
public string $profession = '';
/**
* Filter by crafter
*/
public string $crafter = '';
/**
* List of all professions.
*/
public array $profession_options = [];
public function mount()
{
// Build professions select list.
$options = Profession::all()->mapWithKeys(function ($item) {
return [Str::lower($item['name']) => $item['name']];
});
$this->profession_options = collect(['' => '-- Profession --'])
->merge($options)
->toArray();
}
public function render()
{
$query = Recipe::select('recipes.*')
->join('items', 'items.id', '=', 'recipes.item_id')
->orderBy('items.name');
// Filter by name
if (strlen($this->name) >= 3) {
$query->whereHas('craft', function ($q) {
$q->where('name', 'LIKE', '%' . $this->name . '%');
});
}
// Filter by profession
if (strlen($this->profession)) {
$query->whereHas('profession', function ($q) {
$q->where('name', Str::ucfirst($this->profession));
});
}
// Filter by crafter
if (strlen($this->crafter) >= 3) {
$query->whereHas('crafters', function ($q) {
$q->where('name', 'LIKE', '%' . $this->crafter . '%');
});
}
return view('livewire.recipes', [
'recipes' => $query->paginate($this->perPage)
]);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Http\Livewire\Traits;
trait WithPagination
{
use \Livewire\WithPagination;
public $perPage = 15;
public function paginationView()
{
return 'pagination.default';
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('auth.login');
}
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards
* @return mixed
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,95 @@
<?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.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'username' => 'required|string',
'password' => 'required|string',
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate()
{
$this->ensureIsNotRateLimited();
$credentials = $this->only('username', 'password');
$remember = $this->boolean('remember');
if (!Auth::attempt($credentials, $remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'username' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited()
{
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.
*
* @return string
*/
public function throttleKey()
{
return Str::lower($this->input('email')).'|'.$this->ip();
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CharacterProfessionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'data' => 'required|json',
];
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user() !== null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'current_password' => 'required|current_password',
'password' => 'required|min:8|confirmed',
];
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace App\Jobs;
use App\Models\Character;
use App\Models\Profession;
use App\Models\CharacterProfession;
use App\Models\Recipe;
use App\Models\RecipeCategory;
use App\Models\Item;
use App\ProfessionImport\Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
class ImportProfession implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Character profession the operation should act upon,
*/
protected CharacterProfession $ch_prof;
/**
* Array of recipe data to the imported.
*/
protected array $recipes;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Character $character, $data)
{
// Validate character name first.
if ($character->name !== $data->player) {
$message = sprintf('Wrong character: %s expected %s', $data->player, $character->name);
throw new Exception($message);
}
// Validate profession
$profession = Profession::slug($data->profession->name)->first();
if (!$profession) {
$message = sprintf('Invalid profession: %s', $data->profession->name);
throw new Exception($message);
}
// Create/update profession for player.
$this->ch_prof = $character->professions()->updateOrCreate(['profession_id' => $profession->id], [
'skill' => $data->profession->level,
]);
$this->recipes = $data->profession->recipes;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
DB::transaction(function () {
$this->processItems();
$profession = $this->ch_prof->profession;
$recipes = [];
// Create recipes for character
foreach($this->recipes as $data) {
$item = Item::where('name', $data->name)->firstOrFail();
$recipes[] = $this->getRecipe($item, $profession, $data)->id;
}
// Update attached recipes.
$this->ch_prof->recipes()->sync($recipes);
});
}
protected function processItems()
{
$items = collect($this->recipes);
$nested = $items->pluck('items')->flatten()->unique('name');
$items = $items->except('items')
->merge($nested)
->map(function ($item, $key) {
return [
'name' => $item->name,
'slug' => Str::slug($item->name),
'external_id' => $item->id,
'texture' => isset($item->texture) ? $item->texture : null,
'color' => isset($item->color) ? (string) Str::of($item->color)->replace('#', '')->limit(8) : null,
];
})
->sort('name')
->toArray();
Item::upsert($items, [ 'external_id', 'name', 'slug' ], [ 'external_id', 'texture', 'color' ]);
}
protected function getRecipe(Item $item, Profession $profession, $data)
{
$recipe = $profession->recipes()
->where('item_id', $item->id)
->first();
// Create if not found.
if ($recipe === null) {
$category = RecipeCategory::firstOrCreate([ 'name' => $data->categorie ]);
$recipe = $profession->recipes()->create([
'item_id' => $item->id,
'category_id' => $category->id
]);
// Reagents
foreach($data->items as $reagent) {
$item = Item::where('name', $reagent->name)->firstOrFail();
$recipe->reagents()->attach($item, [ 'quantity' => $reagent->num ]);
}
$recipe->push();
}
return $recipe;
}
}

86
app/Models/Character.php Normal file
View file

@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Character extends Model
{
use HasFactory;
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'level',
'race',
'gender',
'class'
];
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
return $this->slug($value)->firstOrFail();
}
/**
* Set the character's name.
*
* @param string $value
* @return void
*/
public function setNameAttribute($value)
{
$this->attributes['name'] = Str::ucfirst($value);
}
/**
* Retrieve the user that owns this character.
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Retrieve the character's professions.
*/
public function professions()
{
return $this->hasMany(CharacterProfession::class);
}
public function scopeSlug($q, $name)
{
return $q->where('name', Str::ucfirst($name));
}
public function getSlugAttribute()
{
return Str::lower($this->name);
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class CharacterProfession extends Model
{
use HasFactory;
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'character_id',
'profession_id',
'skill',
];
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = [
'profession'
];
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = [ 'name' ];
/**
* Relation to the actual profession data.
*/
public function profession()
{
return $this->belongsTo(Profession::class);
}
/**
* Get the character that has this profession.
*/
public function character()
{
return $this->belongsTo(Character::class);
}
/**
* Get the character's recipes for this profession.
*/
public function recipes()
{
return $this->belongsToMany(Recipe::class,
'character_profession_recipe', 'ch_prof_id');
}
/**
* Learn a recipe for this character and profession.
*/
public function learn(Recipe $recipe)
{
return $this->recipes()->save($recipe);
}
/**
* Get profession name.
*/
public function getNameAttribute() : string
{
return $this->profession->name;
}
public function getSlugAttribute() : string
{
return $this->profession->slug;
}
}

47
app/Models/Item.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
use HasFactory;
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'external_id',
'texture',
'color'
];
protected static function boot() {
parent::boot();
static::creating(function ($item) {
$item->slug = Str::slug($item->name);
});
}
public function recipe()
{
return $this->belongsTo(Recipe::class, 'id');
}
public function getQuantityAttribute()
{
if ($this->pivot) {
return $this->pivot->quantity;
}
return null;
}
}

62
app/Models/Profession.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Profession extends Model
{
use HasFactory;
public $timestamps = false;
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
return $this->slug($value)->firstOrFail();
}
/**
* Get all characters with this profession.
*/
public function characters()
{
return $this->belongsToMany(Character::class);
}
/**
* What recipes this profession has
*/
public function recipes()
{
return $this->hasMany(Recipe::class);
}
public function scopeSlug($q, $name)
{
return $q->where('name', Str::ucfirst($name));
}
public function getSlugAttribute()
{
return Str::lower($this->name);
}
}

131
app/Models/Recipe.php Normal file
View file

@ -0,0 +1,131 @@
<?php
namespace App\Models;
use Staudenmeir\EloquentHasManyDeep\HasRelationships as HasDeepRelation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
use Traits\HasSlug;
use HasDeepRelation;
use HasFactory;
public $timestamps = false;
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = [
'profession',
'craft'
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'profession_id',
'category_id',
'item_id'
];
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
return $this->whereHas('craft', function ($q) use ($value) {
$q->where('slug', $value);
})->firstOrFail();
}
/**
* What profession this item belongs to.
*/
public function profession()
{
return $this->belongsTo(Profession::class);
}
/**
* What category this recipe belongs to.
*/
public function category()
{
return $this->belongsTo(RecipeCategory::class, 'category_id');
}
/**
* What item this recipe crafts.
*/
public function craft()
{
return $this->belongsTo(Item::class, 'item_id');
}
/**
* Get all character professions.
*/
public function character_profession()
{
return $this->belongsToMany(CharacterProfession::class,
'character_profession_recipe', null, 'ch_prof_id');
}
/**
* Get all character that can craft this recipe.
*/
public function crafters()
{
return $this->hasManyDeep(Character::class,
['character_profession_recipe', CharacterProfession::class],
[ null, 'id', 'id'],
[ null,'ch_prof_id', 'character_id']
);
}
/**
* What reagents is needed to craft this recipe.
*/
public function reagents()
{
return $this->belongsToMany(Item::class, 'reagents')
->withPivot('quantity');
}
/**
* Get recipe name from crafted item.
*/
public function getNameAttribute()
{
return $this->craft->name;
}
/**
* Get recipe slug from crafted item.
*/
public function getSlugAttribute()
{
return $this->craft->slug;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class RecipeCategory extends Model
{
use HasFactory;
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
];
public function recipes()
{
return $this->hasMany(Recipe::class, 'category_id');
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Models\Traits;
use Illuminate\Support\Str;
trait HasSlug
{
public function setSlugAttribute($value)
{
$this->attributes['slug'] = Str::slug($value);
}
public function scopeSlug($q, $value)
{
return $q->where('slug', Str::slug($value));
}
}

48
app/Models/User.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'username',
'password',
'role'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function characters()
{
return $this->hasMany(Character::class);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Character;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
class CharacterPolicy
{
use HandlesAuthorization;
public function create(User $user)
{
$limit = 8;
if ($user->characters->count() < $limit) {
return Response::allow();
}
$message = __('authorization.character.limit', [ 'limit' => $limit ]);
return Response::deny($message);
}
public function update(User $user, Character $character)
{
return $user->id === $character->user_id;
}
public function delete(User $user, Character $character)
{
return $user->id === $character->user_id;
}
/**
* Check if use can import profession for this character.
*/
public function import_profession(User $user, Character $character)
{
return $user->id === $character->user_id;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Character;
use App\Models\CharacterProfession;
use Illuminate\Auth\Access\HandlesAuthorization;
class CharacterProfessionPolicy
{
use HandlesAuthorization;
public function delete(User $user, CharacterProfession $ch_prof)
{
return $user->id === $ch_prof->character->user_id;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\ProfessionImport;
class Exception extends \Exception
{
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Models\Character' => \App\Policies\CharacterPolicy::class,
'App\Models\CharacterProfession' => \App\Policies\CharacterProfessionPolicy::class,
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
class BladeServiceProvider extends ServiceProvider
{
protected $components = [
// Form
'input' => \App\View\Components\Form\Input::class,
'input-password' => \App\View\Components\Form\Password::class,
'textarea' => \App\View\Components\Form\Textarea::class,
'select' => \App\View\Components\Form\Select::class,
];
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Register components
foreach($this->components as $alias => $class) {
Blade::component($alias, $class);
}
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
//
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use App\Models\Character;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* This is used by Laravel authentication to redirect users after login.
*
* @var string
*/
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
$this->routes(function () {
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class ClassIcon extends Icon
{
protected $prefix = 'classes';
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Form extends Component
{
public string $method;
public $spoofMethod = false;
public function __construct($method = 'POST')
{
if (in_array($method, ['DELETE'])) {
$this->spoofMethod = $method;
$method = 'POST';
}
$this->method = $method;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.form.form');
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\View\Components\Form;
use Illuminate\View\Component;
class Input extends Component
{
public string $type;
public ?string $id;
public string $name;
public ?string $value;
public string $disabled;
public function __construct($name, ?string $id = null, string $type = 'text', ?string $value = null, bool $disabled = false)
{
$this->id = $id;
$this->type = $type;
$this->name = $name;
$this->value = old($name, $value);
$this->disabled = $disabled ? 'disabled=disabled' : '';
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.form.inputs.input');
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\View\Components\Form;
class Password extends Input
{
public function __construct($name = 'password', ?string $id = null, ?string $value = null, bool $disabled = false)
{
parent::__construct($name, $id, 'password', $value, $disabled);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\View\Components\Form;
use Illuminate\View\Component;
class Select extends Component
{
public ?string $id;
public string $name;
public ?string $value;
public string $disabled;
public $options;
public function __construct($name, $options, ?string $id = null, ?string $value = null, bool $disabled = false)
{
$this->id = $id;
$this->name = $name;
$this->value = old($name, $value);
$this->disabled = $disabled ? 'disabled=disabled' : '';
$this->options = $options;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.form.inputs.select');
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\View\Components\Form;
use Illuminate\View\Component;
class Textarea extends Component
{
public ?string $id;
public string $name;
public ?string $value;
public string $disabled;
public function __construct($name, ?string $id = null, ?string $value = null, bool $disabled = false)
{
$this->id = $id;
$this->name = $name;
$this->value = old($name, $value);
$this->disabled = $disabled ? 'disabled=disabled' : '';
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.form.inputs.textarea');
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\View\Components;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\Component;
abstract class Icon extends Component
{
protected $prefix = '';
public $url;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct(string $name)
{
$file = sprintf('%s/%s.jpg', $this->prefix, Str::lower($name));
$this->url = Storage::disk('images')->url($file);
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.class-icon');
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Layout extends Component
{
/**
* Layout script
*/
protected string $name;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct($name = 'default')
{
$this->name = $name;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view("layouts.{$this->name}");
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\View\Components;
use Illuminate\Support\Facades\Session;
use Illuminate\View\Component;
class Notifications extends Component
{
/**
* Types of messages.
*
* @var array
*/
protected $types = [ 'info', 'success', 'warning', 'error' ];
/**
* Position (left,center or right)
*
* @var string
*/
public string $position = 'center';
/**
* Type and css class mapping.
*/
protected $cssClasses = [
'info' => 'info',
'success' => 'success',
'warning' => 'warning',
'error' => 'danger'
];
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|string
*/
public function render()
{
$messages = [];
foreach($this->types as $type) {
if (Session::has($type)) {
$class = $this->cssClasses[$type];
$messages[$class] = Session::get($type);
}
}
return view('components.notifications', [ 'messages' => $messages ]);
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\View\Components;
class ProfessionIcon extends Icon
{
protected $prefix = 'professions';
}

81
app/Warcraft/Classes.php Normal file
View file

@ -0,0 +1,81 @@
<?php
namespace App\Warcraft;
use Illuminate\Support\Collection;
class Classes extends Collection
{
/**
* Enum constants
*/
const WARRIOR = 'warrior';
const PALADIN = 'paladin';
const HUNTER = 'hunter';
const ROGUE = 'rogue';
const PRIEST = 'priest';
const SHAMAN = 'shaman';
const MAGE = 'mage';
const WARLOCK = 'warlock';
const DRUID = 'druid';
public function __construct($items = null)
{
if ($items === null) {
$this->items = [
self::WARRIOR => 'Warrior',
self::PALADIN => 'Paladin',
self::HUNTER => 'Hunter',
self::ROGUE => 'Rogue',
self::PRIEST => 'Priest',
self::SHAMAN => 'Shaman',
self::MAGE => 'Mage',
self::WARLOCK => 'Warlock',
self::DRUID => 'Druid'
];
} else {
parent::__construct($items);
}
}
/**
* Filter classes by race.
*/
public function race($race) : self
{
switch($race) {
case Races::HUMAN :
return $this->only(self::WARRIOR, self::PALADIN, self::ROGUE,
self::PRIEST, self::MAGE, self::WARLOCK);
case Races::DWARF :
return $this->only(self::WARRIOR, self::HUNTER, self::PALADIN,
self::ROGUE, self::PRIEST);
case Races::GNOME :
return $this->only(self::WARRIOR, self::ROGUE, self::MAGE,
self::WARLOCK);
case Races::NIGHTELF :
return $this->only(self::WARRIOR, self::HUNTER, self::ROGUE,
self::PRIEST, self::DRUID);
case Races::DRAENEI :
return $this->only(self::WARRIOR, self::HUNTER, self::PALADIN,
self::SHAMAN, self::PRIEST, self::MAGE);
case Races::ORC :
return $this->only(self::WARRIOR, self::HUNTER, self::ROGUE,
self::SHAMAN, self::WARLOCK);
case Races::TROLL :
return $this->only(self::WARRIOR, self::HUNTER, self::ROGUE,
self::PRIEST, self::SHAMAN, self::MAGE);
case Races::TAUREN :
return $this->only(self::WARRIOR, self::HUNTER, self::SHAMAN,
self::DRUID);
case Races::UNDEAD :
return $this->only(self::WARRIOR, self::ROGUE, self::PRIEST,
self::MAGE, self::WARLOCK);
case Races::BLOODELF :
return $this->only(self::PALADIN, self::HUNTER, self::ROGUE,
self::PRIEST, self::MAGE, self::WARLOCK);
}
return $this;
}
}

90
app/Warcraft/Races.php Normal file
View file

@ -0,0 +1,90 @@
<?php
namespace App\Warcraft;
use Illuminate\Support\Collection;
class Races extends Collection
{
/**
* Enum constants
*/
const HUMAN = 'human';
const DWARF = 'dwarf';
const GNOME = 'gnome';
const NIGHTELF = 'night elf';
const DRAENEI = 'draenei';
const ORC = 'orc';
const TROLL = 'troll';
const TAUREN = 'tauren';
const UNDEAD = 'undead';
const BLOODELF = 'blood elf';
public function __construct($items = null)
{
if ($items === null) {
$this->items = [
self::HUMAN => 'Human',
self::DWARF => 'Dwarf',
self::GNOME => 'Gnome',
self::NIGHTELF => 'Night elf',
self::DRAENEI => 'Draenei',
self::ORC => 'Orc',
self::TROLL => 'Troll',
self::TAUREN => 'Tauren',
self::UNDEAD => 'Undead',
self::BLOODELF => 'Blood elf'
];
} else {
parent::__construct($items);
}
}
/**
* Filter only alliance races
*/
public function alliance() : self
{
return $this->only(self::HUMAN, self::DWARF, self::GNOME, self::NIGHTELF, self::DRAENEI);
}
/**
* Filter only alliance races
*/
public function horde() : self
{
return $this->only(self::ORC, self::TROLL, self::TAUREN, self::UNDEAD, self::BLOODELF);
}
/**
* Filter races by class.
*/
public function class($class) : self
{
switch($class) {
case Classes::WARRIOR :
return $this->except(self::BLOODELF);
case Classes::PALADIN :
return $this->only(self::HUMAN, self::DWARF, self::DRAENEI, self::BLOODELF);
case Classes::HUNTER :
return $this->only(self::HUMAN, self::DWARF, self::NIGHTELF, self::DRAENEI,
self::ORC, self::TROLL, self::TAUREN, self::BLOODELF);
case Classes::ROGUE :
return $this->only(self::HUMAN, self::DWARF, self::NIGHTELF, self::GNOME,
self::ORC, self::TROLL, self::UNDEAD, self::BLOODELF);
case Classes::PRIEST :
return $this->only(self::HUMAN, self::DWARF, self::NIGHTELF, self::DRAENEI,
self::TROLL, self::UNDEAD, self::BLOODELF);
case Classes::SHAMAN :
return $this->only(self::DRAENEI, self::ORC, self::TROLL, self::TAUREN);
case Classes::MAGE :
return $this->only(self::HUMAN, self::GNOME, self::TROLL, self::UNDEAD, self::BLOODELF);
case Classes::WARLOCK :
return $this->only(self::HUMAN, self::GNOME, self::ORC, self::UNDEAD, self::BLOODELF);
case Classes::DRUID :
return $this->only(self::NIGHTELF, self::TAUREN);
}
return $this;
}
}