Models refactor + Basic functionnalities
This commit is contained in:
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\StoreResumeComponentRequest;
|
|
||||||
use App\Http\Requests\UpdateResumeComponentRequest;
|
|
||||||
use App\Models\ResumeComponent;
|
|
||||||
|
|
||||||
class ResumeComponentController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for creating a new resource.
|
|
||||||
*/
|
|
||||||
public function create()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(StoreResumeComponentRequest $request)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(ResumeComponent $resumeComponent)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified resource.
|
|
||||||
*/
|
|
||||||
public function edit(ResumeComponent $resumeComponent)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(UpdateResumeComponentRequest $request, ResumeComponent $resumeComponent)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(ResumeComponent $resumeComponent)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\StoreResumeComponentDataTypeRequest;
|
|
||||||
use App\Http\Requests\UpdateResumeComponentDataTypeRequest;
|
|
||||||
use App\Models\ResumeComponentDataType;
|
|
||||||
|
|
||||||
class ResumeComponentDataTypeController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for creating a new resource.
|
|
||||||
*/
|
|
||||||
public function create()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(StoreResumeComponentDataTypeRequest $request)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(ResumeComponentDataType $resumeComponentDataType)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified resource.
|
|
||||||
*/
|
|
||||||
public function edit(ResumeComponentDataType $resumeComponentDataType)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(UpdateResumeComponentDataTypeRequest $request, ResumeComponentDataType $resumeComponentDataType)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(ResumeComponentDataType $resumeComponentDataType)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
91
app/Http/Controllers/ResumeComponentPlacementController.php
Normal file
91
app/Http/Controllers/ResumeComponentPlacementController.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreResumeComponentPlacementRequest;
|
||||||
|
use App\Http\Requests\UpdateResumeComponentPlacementRequest;
|
||||||
|
use App\Models\ResumeComponentPlacement;
|
||||||
|
use Log;
|
||||||
|
|
||||||
|
class ResumeComponentPlacementController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
dd('hello');
|
||||||
|
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(StoreResumeComponentPlacementRequest $request)
|
||||||
|
{
|
||||||
|
dd('hello');
|
||||||
|
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(ResumeComponentPlacement $resumeComponentPlacement)
|
||||||
|
{
|
||||||
|
dd('hello');
|
||||||
|
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(UpdateResumeComponentPlacementRequest $request, ResumeComponentPlacement $resumeComponentPlacement)
|
||||||
|
{
|
||||||
|
$resumeComponentPlacement->load('componentData.component', 'componentData.inputData');
|
||||||
|
|
||||||
|
$data = $request->validated();
|
||||||
|
|
||||||
|
// Update component placement
|
||||||
|
$componentPlacementData = collect($data)->except('component_data')->toArray();
|
||||||
|
$componentPlacementData['resume_component_data_id'] = $data['component_data']['id'] ?? null;
|
||||||
|
$resumeComponentPlacement->update($componentPlacementData);
|
||||||
|
|
||||||
|
// Update component data
|
||||||
|
$componentData = collect($data['component_data'])->except(['input_data', 'component'])->toArray();
|
||||||
|
$componentData['resume_component_id'] = $data['component_data']['component']['id'] ?? null;
|
||||||
|
$componentData['resume_component_placement_id'] = $resumeComponentPlacement->id;
|
||||||
|
$resumeComponentPlacement->componentData()->update($componentData);
|
||||||
|
|
||||||
|
// Update input data
|
||||||
|
$inputData = collect($data['component_data']['input_data'] ?? []);
|
||||||
|
foreach ($inputData as $inputDatum) {
|
||||||
|
$componentInput = $inputDatum['component_input'];
|
||||||
|
$inputDatum = collect($inputDatum)->except('component_input')->toArray();
|
||||||
|
$inputDatum['resume_component_input_id'] = $componentInput['id'] ?? null;
|
||||||
|
$inputDatum['resume_component_data_id'] = $componentData['id'] ?? null;
|
||||||
|
|
||||||
|
$resumeComponentPlacement->componentData->inputData()->updateOrCreate(
|
||||||
|
['id' => $inputDatum['id'] ?? null],
|
||||||
|
$inputDatum
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resumeComponentPlacement->push();
|
||||||
|
$resumeComponentPlacement->refresh();
|
||||||
|
|
||||||
|
return response()->json($resumeComponentPlacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(ResumeComponentPlacement $resumeComponentPlacement)
|
||||||
|
{
|
||||||
|
dd('hello');
|
||||||
|
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ namespace App\Http\Controllers;
|
|||||||
use App\Http\Requests\StoreResumeRequest;
|
use App\Http\Requests\StoreResumeRequest;
|
||||||
use App\Http\Requests\UpdateResumeRequest;
|
use App\Http\Requests\UpdateResumeRequest;
|
||||||
use App\Models\Resume;
|
use App\Models\Resume;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ResumeController extends Controller
|
class ResumeController extends Controller
|
||||||
{
|
{
|
||||||
@@ -13,15 +16,24 @@ class ResumeController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
//
|
$resumes = Resume::all();
|
||||||
|
return new JsonResponse($resumes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for creating a new resource.
|
* Show the form for creating a new resource.
|
||||||
*/
|
*/
|
||||||
public function create()
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
//
|
// Check if the user can create the resume
|
||||||
|
if ($request->user()->cannot('create', Resume::class)) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newResume = new Resume();
|
||||||
|
$newResume->save();
|
||||||
|
// Redirect to the edit page for the new resume
|
||||||
|
return redirect()->route('resumes.edit', $newResume);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,15 +49,22 @@ class ResumeController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(Resume $resume)
|
public function show(Resume $resume)
|
||||||
{
|
{
|
||||||
//
|
return redirect()->route('resumes.edit', $resume);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for editing the specified resource.
|
* Show the form for editing the specified resource.
|
||||||
*/
|
*/
|
||||||
public function edit(Resume $resume)
|
public function edit(Request $request, Resume $resume)
|
||||||
{
|
{
|
||||||
//
|
// Check if the user can edit the resume
|
||||||
|
if ($request->user()->cannot('update', $resume)) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('resumes/Edit', [
|
||||||
|
'resume' => $resume->load('componentsPlacements.componentData.component', 'componentsPlacements.componentData.inputData.componentInput.dataType')->toArray()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
28
app/Http/Requests/StoreResumeComponentPlacementRequest.php
Normal file
28
app/Http/Requests/StoreResumeComponentPlacementRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreResumeComponentPlacementRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Http/Requests/UpdateResumeComponentPlacementRequest.php
Normal file
35
app/Http/Requests/UpdateResumeComponentPlacementRequest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateResumeComponentPlacementRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()->can('update', $this->resume_component_placement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => 'required|exists:resume_component_placements,id',
|
||||||
|
'order' => 'required|integer',
|
||||||
|
'component_data.id' => 'required|exists:resume_component_data,id',
|
||||||
|
'component_data.component.id' => 'required|exists:resume_components,id',
|
||||||
|
'component_data.input_data' => 'required|array',
|
||||||
|
'component_data.input_data.*.id' => 'required|exists:resume_component_input_data,id',
|
||||||
|
'component_data.input_data.*.value' => 'required|string',
|
||||||
|
'component_data.input_data.*.component_input.id' => 'required|exists:resume_component_inputs,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,15 +26,13 @@ class Resume extends Model
|
|||||||
return $this->belongsTo(User::class, 'creator_id');
|
return $this->belongsTo(User::class, 'creator_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function slots(): HasMany
|
public function componentsPlacements(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ResumeSlot::class);
|
return $this->hasMany(ResumeComponentPlacement::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function components(): BelongsToMany
|
public function componentsData()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(ResumeComponent::class)
|
return $this->hasManyThrough(ResumeComponentData::class, ResumeComponentPlacement::class);
|
||||||
->using(ResumeSlot::class)
|
|
||||||
->orderBy('resume_slots.order');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,30 +16,24 @@ class ResumeComponent extends Model
|
|||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
'vue_component_name',
|
'vue_component_name',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function resumes(): BelongsToMany
|
public function resumes(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Resume::class, 'resume_resume_component')
|
return $this->belongsToMany(Resume::class)
|
||||||
->using(ResumeSlot::class)
|
->using(ResumeComponentData::class);
|
||||||
->withTimestamps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function slots(): HasMany
|
public function data(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ResumeSlot::class);
|
return $this->hasMany(ResumeComponentData::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function inputs(): HasMany
|
public function placements(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ResumeComponentInput::class);
|
return $this->belongsToMany(ResumeComponentPlacement::class)
|
||||||
}
|
->using(ResumeComponentData::class);
|
||||||
|
|
||||||
public function dataTypes(): BelongsToMany
|
|
||||||
{
|
|
||||||
return $this->belongsToMany(ResumeComponentDataType::class)
|
|
||||||
->using(ResumeComponentInput::class)
|
|
||||||
->withTimestamps();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
app/Models/ResumeComponentData.php
Normal file
25
app/Models/ResumeComponentData.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
class ResumeComponentData extends Pivot
|
||||||
|
{
|
||||||
|
protected $table = 'resume_component_data';
|
||||||
|
|
||||||
|
public function component()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ResumeComponent::class, 'resume_component_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function componentPlacements()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ResumeComponentPlacement::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputData()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ResumeComponentInputData::class, 'resume_component_data_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ class ResumeComponentInput extends Pivot
|
|||||||
/** @use HasFactory<\Database\Factories\ResumeComponentFactory> */
|
/** @use HasFactory<\Database\Factories\ResumeComponentFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'resume_component_inputs';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'resume_component_id',
|
'resume_component_id',
|
||||||
'resume_component_data_type_id',
|
'resume_component_data_type_id',
|
||||||
@@ -34,6 +36,6 @@ class ResumeComponentInput extends Pivot
|
|||||||
|
|
||||||
public function dataType(): BelongsTo
|
public function dataType(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(ResumeComponentDataType::class);
|
return $this->belongsTo(ResumeComponentDataType::class, 'resume_component_data_type_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/Models/ResumeComponentInputData.php
Normal file
21
app/Models/ResumeComponentInputData.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
class ResumeComponentInputData extends Pivot
|
||||||
|
{
|
||||||
|
protected $table = 'resume_component_input_data';
|
||||||
|
|
||||||
|
|
||||||
|
public function componentData()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ResumeComponentData::class, 'resume_component_data_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function componentInput()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ResumeComponentInput::class, 'resume_component_input_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Models/ResumeComponentPlacement.php
Normal file
29
app/Models/ResumeComponentPlacement.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Policies\ResumeComponentPlacementPolicy;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
#[UsePolicy(ResumeComponentPlacementPolicy::class)]
|
||||||
|
class ResumeComponentPlacement extends Pivot
|
||||||
|
{
|
||||||
|
protected $table = 'resume_component_placements';
|
||||||
|
|
||||||
|
public function componentData(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ResumeComponentData::class, 'resume_component_data_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resume(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Resume::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function component(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->componentData->component();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use App\Policies\ResumeComponentPolicy;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
|
||||||
|
|
||||||
class ResumeSlot extends Pivot
|
|
||||||
{
|
|
||||||
/** @use HasFactory<\Database\Factories\ResumeComponentFactory> */
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'resume_id',
|
|
||||||
'resume_component_id',
|
|
||||||
'order',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function resume()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Resume::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function component()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(ResumeComponent::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use App\Policies\ResumeComponentPolicy;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
|
||||||
|
|
||||||
class ResumeSlotValue extends Pivot
|
|
||||||
{
|
|
||||||
/** @use HasFactory<\Database\Factories\ResumeComponentFactory> */
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'resume_slot_id',
|
|
||||||
'resume_component_input_id',
|
|
||||||
'value'
|
|
||||||
];
|
|
||||||
|
|
||||||
public function resume(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Resume::class)
|
|
||||||
->through(ResumeSlot::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function component(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(ResumeComponent::class)
|
|
||||||
->through(ResumeSlot::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dataType(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(ResumeComponentDataType::class)
|
|
||||||
->through(ResumeComponentInput::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function slot(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(ResumeSlot::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function componentInput(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(ResumeComponentInput::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,15 @@ namespace App\Models;
|
|||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -45,4 +47,9 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resumes(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Resume::class, 'creator_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
app/Policies/ResumeComponentPlacementPolicy.php
Normal file
58
app/Policies/ResumeComponentPlacementPolicy.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Resume;
|
||||||
|
use App\Models\ResumeComponentPlacement;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class ResumeComponentPlacementPolicy
|
||||||
|
{
|
||||||
|
private function isCreator(User $user, ResumeComponentPlacement $componentPlacement): Response
|
||||||
|
{
|
||||||
|
return $user->id === $componentPlacement->load('resume')->resume->creator_id
|
||||||
|
? Response::allow()
|
||||||
|
: Response::deny('You do not own the resume of the component placement.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, ResumeComponentPlacement $componentPlacement): Response
|
||||||
|
{
|
||||||
|
return $this->isCreator($user, $componentPlacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, ResumeComponentPlacement $componentPlacement): Response
|
||||||
|
{
|
||||||
|
return $this->isCreator($user, $componentPlacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, ResumeComponentPlacement $componentPlacement): Response
|
||||||
|
{
|
||||||
|
return $this->isCreator($user, $componentPlacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, ResumeComponentPlacement $componentPlacement): Response
|
||||||
|
{
|
||||||
|
return $this->isCreator($user, $componentPlacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
|||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
@@ -21,6 +22,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
AddLinkHeadersForPreloadedAssets::class,
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$middleware->api([
|
||||||
|
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"inertiajs/inertia-laravel": "^2.0",
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"tightenco/ziggy": "^2.4"
|
"tightenco/ziggy": "^2.4"
|
||||||
},
|
},
|
||||||
@@ -82,4 +83,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|||||||
66
composer.lock
generated
66
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "238758776cef75e40b61f11e6f2e00e2",
|
"content-hash": "d8582846588d6533069ca5f9a52ee9f7",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -1399,6 +1399,70 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-07-07T14:17:42+00:00"
|
"time": "2025-07-07T14:17:42+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/sanctum",
|
||||||
|
"version": "v4.2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/sanctum.git",
|
||||||
|
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
|
||||||
|
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.0|^12.0",
|
||||||
|
"illuminate/contracts": "^11.0|^12.0",
|
||||||
|
"illuminate/database": "^11.0|^12.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/console": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench": "^9.0|^10.0",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"phpunit/phpunit": "^11.3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Laravel\\Sanctum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"laravel",
|
||||||
|
"sanctum"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel/sanctum/issues",
|
||||||
|
"source": "https://github.com/laravel/sanctum"
|
||||||
|
},
|
||||||
|
"time": "2025-07-09T19:45:24+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.4",
|
"version": "v2.0.4",
|
||||||
|
|||||||
84
config/sanctum.php
Normal file
84
config/sanctum.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,localhost:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort(),
|
||||||
|
// Sanctum::currentRequestHost(),
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -15,7 +15,7 @@ return new class extends Migration
|
|||||||
Schema::create('resumes', function (Blueprint $table) {
|
Schema::create('resumes', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
|
||||||
$table->string('name', 255);
|
$table->string('name', 255)->nullable();
|
||||||
$table->foreignIdFor(User::class, "creator_id")->constrained()->onDelete('cascade');
|
$table->foreignIdFor(User::class, "creator_id")->constrained()->onDelete('cascade');
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ return new class extends Migration
|
|||||||
Schema::create('resume_components', function (Blueprint $table) {
|
Schema::create('resume_components', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
|
||||||
|
$table->string('name');
|
||||||
$table->string('vue_component_name')->unique();
|
$table->string('vue_component_name')->unique();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ return new class extends Migration
|
|||||||
$table->foreignIdFor(ResumeComponentDataType::class)->constrained()->onDelete('cascade');
|
$table->foreignIdFor(ResumeComponentDataType::class)->constrained()->onDelete('cascade');
|
||||||
|
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->string("placeholder");
|
$table->string('label')->nullable();
|
||||||
|
$table->string("placeholder")->nullable();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\Resume;
|
use App\Models\Resume;
|
||||||
use App\Models\ResumeComponent;
|
use App\Models\ResumeComponentData;
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -13,11 +13,11 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('resume_slots', function (Blueprint $table) {
|
Schema::create('resume_component_placements', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
|
||||||
$table->foreignIdFor(Resume::class)->constrained()->onDelete('cascade');
|
$table->foreignIdFor(Resume::class, 'resume_id')->constrained()->onDelete('cascade');
|
||||||
$table->foreignIdFor(ResumeComponent::class)->constrained()->onDelete('cascade');
|
$table->foreignIdFor(ResumeComponentData::class, 'resume_component_data_id')->constrained()->onDelete('cascade');
|
||||||
|
|
||||||
$table->integer('order')->default(0);
|
$table->integer('order')->default(0);
|
||||||
|
|
||||||
@@ -30,6 +30,6 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('resume_slots');
|
Schema::dropIfExists('resume_component_placements');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ResumeComponent;
|
||||||
|
use App\Models\ResumeComponentPlacement;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('resume_component_data', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignIdFor(ResumeComponent::class, 'resume_component_id')->constrained()->onDelete('cascade');
|
||||||
|
// $table->foreignIdFor(ResumeComponentPlacement::class, 'resume_component_placement_id')->constrained()->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('resume_component_data');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ResumeComponentData;
|
||||||
use App\Models\ResumeComponentInput;
|
use App\Models\ResumeComponentInput;
|
||||||
use App\Models\ResumeSlot;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -13,13 +13,13 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('resume_slot_values', function (Blueprint $table) {
|
Schema::create('resume_component_input_data', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
|
||||||
$table->foreignIdFor(ResumeSlot::class, 'resume_slot_id')->constrained()->onDelete('cascade');
|
$table->foreignIdFor(ResumeComponentData::class, 'resume_component_data_id')->constrained()->onDelete('cascade');
|
||||||
$table->foreignIdFor(ResumeComponentInput::class, 'resume_component_input_id')->constrained()->onDelete('cascade');
|
$table->foreignIdFor(ResumeComponentInput::class, 'resume_component_input_id')->constrained()->onDelete('cascade');
|
||||||
|
|
||||||
$table->string('value');
|
$table->string('value')->nullable();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
@@ -30,6 +30,6 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('resume_slot_values');
|
Schema::dropIfExists('resume_component_input_data');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -7,6 +7,6 @@ import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
|||||||
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1 grid flex-1 text-left text-sm">
|
<div class="ml-1 grid flex-1 text-left text-sm">
|
||||||
<span class="mb-0.5 truncate leading-tight font-semibold">Laravel Starter Kit</span>
|
<span class="mb-0.5 truncate leading-tight font-semibold">CVAtron</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import { type NavItem } from '@/types';
|
|||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import { LayoutGrid } from 'lucide-vue-next';
|
import { LayoutGrid } from 'lucide-vue-next';
|
||||||
import AppLogo from './AppLogo.vue';
|
import AppLogo from './AppLogo.vue';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
|
||||||
{
|
|
||||||
title: 'Dashboard',
|
|
||||||
href: '/dashboard',
|
|
||||||
icon: LayoutGrid,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,7 +24,7 @@ const mainNavItems: NavItem[] = [
|
|||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain :items="mainNavItems" />
|
<slot name="sidebar-content" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
|||||||
42
resources/js/components/SidebarResumeList.vue
Normal file
42
resources/js/components/SidebarResumeList.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||||
|
import { httpApi } from '@/lib/utils';
|
||||||
|
import { type NavItem } from '@/types';
|
||||||
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { Resume } from '@/types/resume';
|
||||||
|
|
||||||
|
const items = ref<NavItem[]>([]);
|
||||||
|
const page = usePage();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const { data: resumes, error } = await httpApi<Resume[]>(route("resumes.index"));
|
||||||
|
if (error || !resumes) {
|
||||||
|
console.error('Failed to fetch resumes:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.value = resumes.map((resume: Resume) => ({
|
||||||
|
title: resume.name,
|
||||||
|
href: route("resumes.edit", resume),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch resumes:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarGroup class="px-2 py-0">
|
||||||
|
<SidebarGroupLabel>Mes CV</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem v-for="item in items" :key="item.title">
|
||||||
|
<SidebarMenuButton as-child :is-active="item.href === page.url" :tooltip="item.title">
|
||||||
|
<Link :href="item.href">
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
</template>
|
||||||
20
resources/js/components/resume/ResumeComponent.vue
Normal file
20
resources/js/components/resume/ResumeComponent.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
componentPlacement: ResumeComponentPlacement | null
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const componentFile = defineAsyncComponent(
|
||||||
|
() => import(
|
||||||
|
/* @vite-ignore */
|
||||||
|
`./resumeComponents/${props.componentPlacement?.component_data?.component?.vue_component_name}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="componentFile" :componentPlacement="props.componentPlacement" />
|
||||||
|
</template>
|
||||||
74
resources/js/components/resume/ResumeComponentEdit.vue
Normal file
74
resources/js/components/resume/ResumeComponentEdit.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Resume, ResumeComponentPlacement, ResumeInputData } from '@/types/resume';
|
||||||
|
import Button from '../ui/button/Button.vue';
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next';
|
||||||
|
import { SidebarGroup, SidebarGroupLabel } from '@/components/ui/sidebar';
|
||||||
|
import ResumeComponentEditForm from './ResumeComponentEditForm.vue';
|
||||||
|
import { httpApi } from '@/lib/utils';
|
||||||
|
|
||||||
|
const SEND_CHANGED_DATA_DELAY = 500;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
resume: Resume
|
||||||
|
selectedComponent: ResumeComponentPlacement | null
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['selected-component-change']);
|
||||||
|
|
||||||
|
let delayedSendTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
async function sendChangedData(newData: ResumeInputData[]) {
|
||||||
|
const newSelectedComponent = {
|
||||||
|
...props.selectedComponent,
|
||||||
|
component_data: {
|
||||||
|
...props.selectedComponent?.component_data,
|
||||||
|
input_data: newData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let data, error;
|
||||||
|
|
||||||
|
if (delayedSendTimeout) {
|
||||||
|
clearTimeout(delayedSendTimeout);
|
||||||
|
}
|
||||||
|
delayedSendTimeout = setTimeout(async () => {
|
||||||
|
const { data, error } = await httpApi(route('resume-component-placements.update', newSelectedComponent.id), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...newSelectedComponent, _method: 'PUT' })
|
||||||
|
}, {immediate: true});
|
||||||
|
// Handle error
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to update component placement:', error, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, SEND_CHANGED_DATA_DELAY);
|
||||||
|
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to update component placement:', error, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('selected-component-change', newSelectedComponent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full w-full">
|
||||||
|
<Button @click="emit('selected-component-change', null)" variant="outline" size="icon" class="cursor-pointer"><ChevronLeft class="w-4 h-4" /></Button>
|
||||||
|
<SidebarGroup class="w-full p-0">
|
||||||
|
<SidebarGroupLabel>{{ props.selectedComponent?.component_data?.component?.name }}</SidebarGroupLabel>
|
||||||
|
<ResumeComponentEditForm
|
||||||
|
v-if="props.selectedComponent?.component_data?.input_data"
|
||||||
|
:data="props.selectedComponent?.component_data?.input_data!"
|
||||||
|
@data-changed="sendChangedData($event)"
|
||||||
|
/>
|
||||||
|
<p v-else class="text-destructive">No component input data : {{ props.selectedComponent?.component_data }}</p>
|
||||||
|
</SidebarGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
28
resources/js/components/resume/ResumeComponentEditForm.vue
Normal file
28
resources/js/components/resume/ResumeComponentEditForm.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeInputData } from '@/types/resume';
|
||||||
|
import ResumeComponentEditFormInput from './ResumeComponentEditFormInput.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: ResumeInputData[]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['data-changed']);
|
||||||
|
|
||||||
|
function handleDataChanged(updatedData: ResumeInputData) {
|
||||||
|
const index = props.data.findIndex(input => input.id === updatedData.id);
|
||||||
|
const dataCopy = [...props.data];
|
||||||
|
if (index !== -1) {
|
||||||
|
dataCopy[index] = updatedData;
|
||||||
|
emit('data-changed', dataCopy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
@submit.prevent="emit('data-changed', props.data)"
|
||||||
|
class="w-full space-y-4"
|
||||||
|
>
|
||||||
|
<ResumeComponentEditFormInput v-for="input in props.data" :model="input" v-bind:key="input.id" @data-changed="handleDataChanged" />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeInputData } from '@/types/resume';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import Label from '../ui/label/Label.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
model: ResumeInputData
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const componentFile = defineAsyncComponent(
|
||||||
|
() => import(
|
||||||
|
/* @vite-ignore */
|
||||||
|
`./resumeComponentsFormInput/${props.model.component_input.data_type.vue_component_name}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits(['data-changed']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<Label>{{ props.model.component_input?.label }}</Label>
|
||||||
|
<component
|
||||||
|
class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground
|
||||||
|
dark:bg-primary/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-primary px-3 py-1 text-base shadow-xs
|
||||||
|
transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-primary file:text-sm
|
||||||
|
file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm
|
||||||
|
focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]
|
||||||
|
aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
||||||
|
:name="props.model.component_input?.name"
|
||||||
|
:is="componentFile"
|
||||||
|
:model="props.model"
|
||||||
|
@data-changed="emit('data-changed', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
33
resources/js/components/resume/ResumeComponentsList.vue
Normal file
33
resources/js/components/resume/ResumeComponentsList.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Resume, ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||||
|
import { Link } from '@inertiajs/vue3';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
resume: Resume;
|
||||||
|
selectedComponent: ResumeComponentPlacement | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const orderedComponentsPlacements = computed(() => {
|
||||||
|
console.log('Ordered Components Placements:', props.resume.components_placements);
|
||||||
|
return props.resume.components_placements ? [...props.resume.components_placements].sort((a, b) => a.order - b.order) : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['selected-component-change']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarGroup class="w-full p-0">
|
||||||
|
<SidebarGroupLabel>Composants du CV</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem v-for="component in orderedComponentsPlacements" :key="component.id">
|
||||||
|
<SidebarMenuButton as-child class="cursor-pointer" :tooltip="component.component_data?.component?.name">
|
||||||
|
<div @click="emit('selected-component-change', component)">
|
||||||
|
<span>{{ component.component_data?.component?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
</template>
|
||||||
33
resources/js/components/resume/ResumeEditPanel.vue
Normal file
33
resources/js/components/resume/ResumeEditPanel.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Resume, ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
import ResumeComponentEdit from './ResumeComponentEdit.vue';
|
||||||
|
import ResumeComponentsList from './ResumeComponentsList.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
resume: Resume
|
||||||
|
selectedComponent: ResumeComponentPlacement | null
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['selected-component-change']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-1 gap-4 rounded-xl p-4 overflow-x-auto max-w-[25%] bg-accent relative">
|
||||||
|
<Transition mode="out-in" appear>
|
||||||
|
<ResumeComponentEdit v-if="selectedComponent != null" :resume="props.resume" :selectedComponent="props.selectedComponent" @selected-component-change="emit('selected-component-change', $event)" :key="selectedComponent ? selectedComponent.id : 'form'" />
|
||||||
|
<ResumeComponentsList v-else :resume="props.resume" :selectedComponent="props.selectedComponent" @selected-component-change="emit('selected-component-change', $event)" />
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
resources/js/components/resume/ResumePreviewPanel.vue
Normal file
19
resources/js/components/resume/ResumePreviewPanel.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Resume, ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
import ResumeComponent from './ResumeComponent.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
resume: Resume,
|
||||||
|
selectedComponent: ResumeComponentPlacement | null
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['selected-component-change']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-2 w-full p-6">
|
||||||
|
<div id="resume" class="aspect-[0.707317073] w-full max-w-[84.1cm] bg-white text-black">
|
||||||
|
<ResumeComponent v-for="componentPlacement in resume.components_placements" :key="componentPlacement.id" :componentPlacement="componentPlacement" @click="emit('selected-component-change', componentPlacement)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
15
resources/js/components/resume/resumeComponents/email.vue
Normal file
15
resources/js/components/resume/resumeComponents/email.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
componentPlacement: ResumeComponentPlacement | null
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
I'm an email component : {{ props.componentPlacement?.component_data?.input_data[0].value }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
15
resources/js/components/resume/resumeComponents/name.vue
Normal file
15
resources/js/components/resume/resumeComponents/name.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
componentPlacement: ResumeComponentPlacement | null
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
I'm an name component : {{ props.componentPlacement?.component_data?.input_data[0].value }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeInputData } from '@/types/resume';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
model: ResumeInputData
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['data-changed']);
|
||||||
|
|
||||||
|
console.log('model value ', props.model);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
:value="props.model.value"
|
||||||
|
@input="emit('data-changed', { ...props.model, value: $event.target.value })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ResumeInputData } from '@/types/resume';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
model: ResumeInputData
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['data-changed']);
|
||||||
|
|
||||||
|
console.log('model value ', props.model);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="props.model.value"
|
||||||
|
@input="emit('data-changed', { ...props.model, value: $event.target.value })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,7 @@ import AppContent from '@/components/AppContent.vue';
|
|||||||
import AppShell from '@/components/AppShell.vue';
|
import AppShell from '@/components/AppShell.vue';
|
||||||
import AppSidebar from '@/components/AppSidebar.vue';
|
import AppSidebar from '@/components/AppSidebar.vue';
|
||||||
import AppSidebarHeader from '@/components/AppSidebarHeader.vue';
|
import AppSidebarHeader from '@/components/AppSidebarHeader.vue';
|
||||||
|
import SidebarResumeList from '@/components/SidebarResumeList.vue';
|
||||||
import type { BreadcrumbItemType } from '@/types';
|
import type { BreadcrumbItemType } from '@/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -16,7 +17,11 @@ withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppShell variant="sidebar">
|
<AppShell variant="sidebar">
|
||||||
<AppSidebar />
|
<AppSidebar>
|
||||||
|
<template #sidebar-content>
|
||||||
|
<SidebarResumeList />
|
||||||
|
</template>
|
||||||
|
</AppSidebar>
|
||||||
<AppContent variant="sidebar" class="overflow-x-hidden">
|
<AppContent variant="sidebar" class="overflow-x-hidden">
|
||||||
<AppSidebarHeader :breadcrumbs="breadcrumbs" />
|
<AppSidebarHeader :breadcrumbs="breadcrumbs" />
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,6 +1,36 @@
|
|||||||
|
import { useFetch, UseFetchOptions } from '@vueuse/core';
|
||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let csrfInitialized = false
|
||||||
|
|
||||||
|
async function ensureCsrf() {
|
||||||
|
if (!csrfInitialized) {
|
||||||
|
await useFetch('/sanctum/csrf-cookie', {
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
csrfInitialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function httpApi<T>(url: string, options?: RequestInit, useFetchOptions?: UseFetchOptions): Promise<{data: T | null, error: any}> {
|
||||||
|
await ensureCsrf();
|
||||||
|
|
||||||
|
const { data, error } = await useFetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
useFetchOptions
|
||||||
|
).json();
|
||||||
|
|
||||||
|
if (!error.value && data.value) {
|
||||||
|
return { data: data.value, error: null };
|
||||||
|
} else {
|
||||||
|
return { data: data.value, error: error.value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
44
resources/js/pages/resumes/Edit.vue
Normal file
44
resources/js/pages/resumes/Edit.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
|
import { type BreadcrumbItem } from '@/types';
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { Resume, ResumeComponent, ResumeComponentPlacement } from '@/types/resume';
|
||||||
|
import ResumeEditPanel from '@/components/resume/ResumeEditPanel.vue';
|
||||||
|
import ResumePreviewPanel from '@/components/resume/ResumePreviewPanel.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
resume: Resume
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedComponent = ref<ResumeComponentPlacement | null>(null);
|
||||||
|
|
||||||
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
title: props.resume?.name ?? 'Sans titre',
|
||||||
|
href: '/resumes/edit',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function changeSelectedComponent(newComponent: ResumeComponentPlacement) {
|
||||||
|
selectedComponent.value = newComponent;
|
||||||
|
// Update the resume
|
||||||
|
props.resume.components_placements! = props.resume.components_placements!.map(component =>
|
||||||
|
component.id === newComponent.id ? newComponent : component
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Resume : ', props.resume);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
|
<div class="flex h-full flex-1 gap-4 rounded-xl p-4 overflow-x-auto">
|
||||||
|
<ResumeEditPanel :resume="props.resume" :selected-component="selectedComponent" @selected-component-change="changeSelectedComponent" />
|
||||||
|
<ResumePreviewPanel :resume="props.resume" :selected-component="selectedComponent" @selected-component-change="changeSelectedComponent" />
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
55
resources/js/types/resume.d.ts
vendored
Normal file
55
resources/js/types/resume.d.ts
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
export type Resume = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
components_placements: ResumeComponentPlacement[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResumeComponentPlacement = {
|
||||||
|
id: number;
|
||||||
|
resume_component_data_id: number;
|
||||||
|
resume_id: number;
|
||||||
|
order: number;
|
||||||
|
component_data: ResumeComponentData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResumeComponentData = {
|
||||||
|
id: number;
|
||||||
|
resume_component_id: number;
|
||||||
|
component: ResumeComponent | null;
|
||||||
|
input_data: ResumeInputData[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResumeComponent = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
vue_component_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResumeInputData = {
|
||||||
|
id: number;
|
||||||
|
resume_component_data_id: number;
|
||||||
|
resume_component_input_id: number;
|
||||||
|
|
||||||
|
value: any;
|
||||||
|
|
||||||
|
component_input: ResumeComponentInput | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResumeComponentInput = {
|
||||||
|
id: number;
|
||||||
|
resume_component_data_type_id: number;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
label: string | null;
|
||||||
|
placeholder: string | null;
|
||||||
|
|
||||||
|
data_type: ResumeComponentDataType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResumeComponentDataType = {
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
data_structure: string;
|
||||||
|
vue_component_name: string;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
|
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
10
routes/api.php
Normal file
10
routes/api.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\ResumeComponentPlacementController;
|
||||||
|
use App\Http\Controllers\ResumeController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::apiResource('resume-component-placements', ResumeComponentPlacementController::class)->middleware(['auth:sanctum', 'verified']);
|
||||||
|
Route::apiResource('resumes', ResumeController::class)->middleware(['auth:sanctum', 'verified']);
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Route::get('dashboard', function () {
|
|||||||
return Inertia::render('Dashboard');
|
return Inertia::render('Dashboard');
|
||||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||||
|
|
||||||
Route::resource('resumes', ResumeController::class);
|
Route::resource('resumes', ResumeController::class)->middleware(['auth', 'verified']);
|
||||||
|
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user