generated from Flycro/laravel-nuxt
feat: Multiple Systems
parent
22ea1930c4
commit
12d8f3913c
42
.env.example
42
.env.example
|
|
@ -1,8 +1,9 @@
|
||||||
APP_NAME=LaravelNuxt
|
APP_NAME=LaravelNuxt
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=base64:nsRstTKasZXl9pyGadA3og==
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_TIMEZONE=UTC
|
APP_TIMEZONE=UTC
|
||||||
|
APP_PORT=8000
|
||||||
APP_URL=http://127.0.0.1:8000
|
APP_URL=http://127.0.0.1:8000
|
||||||
API_URL=http://127.0.0.1:8000
|
API_URL=http://127.0.0.1:8000
|
||||||
API_LOCAL_URL=http://127.0.0.1:8000
|
API_LOCAL_URL=http://127.0.0.1:8000
|
||||||
|
|
@ -22,12 +23,12 @@ LOG_STACK=single
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=pgsql
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=pgsql
|
||||||
# DB_PORT=3306
|
DB_PORT=5432
|
||||||
# DB_DATABASE=laravel
|
DB_DATABASE=laravel
|
||||||
# DB_USERNAME=root
|
DB_USERNAME=sail
|
||||||
# DB_PASSWORD=
|
DB_PASSWORD=password
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
|
@ -45,19 +46,27 @@ CACHE_PREFIX=
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
REDIS_CLIENT=phpredis
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=redis
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=log
|
MAIL_MAILER=smtp
|
||||||
MAIL_HOST=127.0.0.1
|
MAIL_HOST=mailpit
|
||||||
MAIL_PORT=2525
|
MAIL_PORT=1025
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
|
||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
PUSHER_APP_ID=Laravel
|
||||||
|
PUSHER_APP_KEY=Laravel
|
||||||
|
PUSHER_APP_SECRET=Laravel
|
||||||
|
PUSHER_HOST=soketi
|
||||||
|
PUSHER_PORT=6001
|
||||||
|
PUSHER_SCHEME=http
|
||||||
|
PUSHER_APP_CLUSTER=mt1
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
|
@ -70,4 +79,11 @@ GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
GOOGLE_REDIRECT_URI=https://127.0.0.1:8000/api/v1/login/google/callback
|
GOOGLE_REDIRECT_URI=https://127.0.0.1:8000/api/v1/login/google/callback
|
||||||
|
|
||||||
TELESCOPE_ENABLED=false
|
TELESCOPE_ENABLED=true
|
||||||
|
|
||||||
|
SCOUT_DRIVER=meilisearch
|
||||||
|
MEILISEARCH_HOST=http://meilisearch:7700
|
||||||
|
|
||||||
|
MEILISEARCH_NO_ANALYTICS=false
|
||||||
|
|
||||||
|
OCTANE_SERVER=frankenphp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\BookRecommendation;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class BookRecommendationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$relations = [];
|
||||||
|
if (request()->has('with')) {
|
||||||
|
$relations = explode(',', request()->with);
|
||||||
|
}
|
||||||
|
return BookRecommendation::with($relations)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'book_name' => 'required|string|max:255',
|
||||||
|
'author' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'isbn' => 'required|string|unique:book_recommendations,isbn',
|
||||||
|
'pages' => 'required|integer',
|
||||||
|
'status' => 'in:PENDING,COMPLETED,REJECTED,ACTIVE',
|
||||||
|
'cover_image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $request->all();
|
||||||
|
|
||||||
|
if ($request->hasFile('cover_image')) {
|
||||||
|
$imagePath = $request->file('cover_image')->store('cover_images', 'public');
|
||||||
|
$data['cover_image'] = $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookRecommendation = BookRecommendation::create([...$request->all(), 'recommended_by' => auth()->id()]);
|
||||||
|
return response()->json($bookRecommendation, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(string $id)
|
||||||
|
{
|
||||||
|
$relations = [];
|
||||||
|
if (request()->has('with')) {
|
||||||
|
$relations = explode(',', request()->with);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookRecommendation = BookRecommendation::with($relations)->findOrFail($id);
|
||||||
|
return response()->json($bookRecommendation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(string $id)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$bookRecommendation = BookRecommendation::findOrFail($id);
|
||||||
|
$request->validate([
|
||||||
|
'book_name' => 'string|max:255',
|
||||||
|
'author' => 'string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'isbn' => 'string|unique:book_recommendations,isbn,'.$bookRecommendation->id,
|
||||||
|
'pages' => 'integer',
|
||||||
|
'recommended_by' => 'exists:users,id',
|
||||||
|
'status' => 'in:PENDING,COMPLETED,REJECTED,ACTIVE',
|
||||||
|
'cover_image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
if ($bookRecommendation->recommended_by !== auth()->id() && !(auth()->user()->hasRole('admin')) ) {
|
||||||
|
return response()->json(['message' => 'Keine Berechtigung.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->all();
|
||||||
|
|
||||||
|
if ($request->hasFile('cover_image')) {
|
||||||
|
// Delete old image if exists
|
||||||
|
if ($bookRecommendation->cover_image) {
|
||||||
|
Storage::delete($bookRecommendation->cover_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
$imagePath = $request->file('cover_image')->store('cover_images', 'public');
|
||||||
|
$data['cover_image'] = $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookRecommendation->update($data);
|
||||||
|
|
||||||
|
return response()->json($bookRecommendation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
$bookRecommendation = BookRecommendation::findOrFail($id);
|
||||||
|
if ($bookRecommendation->recommended_by !== auth()->id() && !(auth()->user()->hasRole('admin')) ) {
|
||||||
|
return response()->json(['message' => 'Keine Berechtigung.'], 403);
|
||||||
|
}
|
||||||
|
$bookRecommendation->delete();
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\BookRecommendation;
|
||||||
|
use App\Models\Vote;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class VoteController extends Controller
|
||||||
|
{
|
||||||
|
public function castVote(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'book_recommendation_id' => 'required|exists:book_recommendations,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($user->total_votes > 0) {
|
||||||
|
$user->decrement('total_votes');
|
||||||
|
|
||||||
|
Vote::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'book_recommendation_id' => $request->book_recommendation_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Vote successfully cast.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['message' => 'No remaining votes.'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class BookRecommendation extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'book_recommendations';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'book_name',
|
||||||
|
'author',
|
||||||
|
'description',
|
||||||
|
'isbn',
|
||||||
|
'pages',
|
||||||
|
'recommended_by',
|
||||||
|
'cover_image',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that recommended the book.
|
||||||
|
*/
|
||||||
|
public function recommender()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'recommended_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function votes()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Vote::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||||
'email',
|
'email',
|
||||||
'avatar',
|
'avatar',
|
||||||
'password',
|
'password',
|
||||||
|
'total_votes',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,6 +58,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||||
return $this->hasMany(UserProvider::class);
|
return $this->hasMany(UserProvider::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function votes()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Vote::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function mustVerifyEmail(): bool
|
public function mustVerifyEmail(): bool
|
||||||
{
|
{
|
||||||
return $this instanceof MustVerifyEmail && !$this->hasVerifiedEmail();
|
return $this instanceof MustVerifyEmail && !$this->hasVerifiedEmail();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Vote extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'book_recommendation_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bookRecommendation()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(BookRecommendation::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"laravel/socialite": "^5.12",
|
"laravel/socialite": "^5.12",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
"league/flysystem-aws-s3-v3": "^3.24",
|
"league/flysystem-aws-s3-v3": "^3.24",
|
||||||
|
"predis/predis": "*",
|
||||||
"spatie/laravel-permission": "^6.4"
|
"spatie/laravel-permission": "^6.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
|
||||||
|
|
@ -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": "22693d68e1669d8584aae373e4619f23",
|
"content-hash": "ec31886b4063db8aa1efc22c187f614a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -3645,6 +3645,67 @@
|
||||||
],
|
],
|
||||||
"time": "2023-11-12T21:59:55+00:00"
|
"time": "2023-11-12T21:59:55+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "predis/predis",
|
||||||
|
"version": "v2.2.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/predis/predis.git",
|
||||||
|
"reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/predis/predis/zipball/b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1",
|
||||||
|
"reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.3",
|
||||||
|
"phpstan/phpstan": "^1.9",
|
||||||
|
"phpunit/phpunit": "^8.0 || ~9.4.4"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Predis\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Till Krüss",
|
||||||
|
"homepage": "https://till.im",
|
||||||
|
"role": "Maintainer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A flexible and feature-complete Redis client for PHP.",
|
||||||
|
"homepage": "http://github.com/predis/predis",
|
||||||
|
"keywords": [
|
||||||
|
"nosql",
|
||||||
|
"predis",
|
||||||
|
"redis"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/predis/predis/issues",
|
||||||
|
"source": "https://github.com/predis/predis/tree/v2.2.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/sponsors/tillkruss",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2023-09-13T16:42:03+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/clock",
|
"name": "psr/clock",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ return new class extends Migration
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
|
$table->integer('total_votes')->default(2);
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?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('book_recommendations', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('book_name');
|
||||||
|
$table->string('author');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('isbn')->unique();
|
||||||
|
$table->integer('pages')->unsigned();
|
||||||
|
$table->string('cover_image')->nullable();
|
||||||
|
$table->unsignedBigInteger('recommended_by');
|
||||||
|
$table->enum('status', ['PENDING', 'COMPLETED', 'REJECTED','ACTIVE'])->default('PENDING');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('recommended_by')->references('id')->on('users')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('book_recommendations');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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('votes', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('book_recommendation_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('votes');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\BookRecommendation;
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class BookRecommendationsTableSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Let's say we want to create 50 fake book recommendations
|
||||||
|
for ($i = 0; $i < 50; $i++) {
|
||||||
|
BookRecommendation::create([
|
||||||
|
'book_name' => fake()->sentence($nbWords = 3, $variableNbWords = true),
|
||||||
|
'author' => fake()->name,
|
||||||
|
'description' => fake()->paragraph($nbSentences = 3, $variableNbSentences = true),
|
||||||
|
'isbn' => fake()->isbn13(),
|
||||||
|
'pages' => fake()->numberBetween($min = 100, $max = 1000),
|
||||||
|
'recommended_by' => 1, // Adjust the range as necessary
|
||||||
|
'cover_image' => null, // You could also simulate image paths if needed
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,5 +35,7 @@ class DatabaseSeeder extends Seeder
|
||||||
$user->save(['timestamps' => false]);
|
$user->save(['timestamps' => false]);
|
||||||
|
|
||||||
$user->assignRole($role1);
|
$user->assignRole($role1);
|
||||||
|
|
||||||
|
$this->call(BookRecommendationsTableSeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ services:
|
||||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||||
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||||
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=frankenphp --host=0.0.0.0 --admin-port=2019 --port=8000"
|
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=frankenphp --host=0.0.0.0 --admin-port=2019 --port=8000 --watch"
|
||||||
XDG_CONFIG_HOME: /var/www/html/config
|
XDG_CONFIG_HOME: /var/www/html/config
|
||||||
XDG_DATA_HOME: /var/www/html/data
|
XDG_DATA_HOME: /var/www/html/data
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc'
|
||||||
|
|
||||||
|
const compat = new FlatCompat()
|
||||||
|
|
||||||
|
export default antfu({
|
||||||
|
rules: {
|
||||||
|
'curly': ['error', 'all'],
|
||||||
|
'node/prefer-global/process': ['error', 'always'],
|
||||||
|
},
|
||||||
|
}, ...compat.config({
|
||||||
|
extends: ['plugin:tailwindcss/recommended'],
|
||||||
|
rules: {
|
||||||
|
'tailwindcss/no-custom-classname': 'off',
|
||||||
|
'tailwindcss/migration-from-tailwind-2': 'off',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
@ -5,7 +5,7 @@ export default defineNuxtConfig({
|
||||||
$development: {
|
$development: {
|
||||||
ssr: true,
|
ssr: true,
|
||||||
devtools: {
|
devtools: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -23,13 +23,9 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'auth/verify': { ssr: false }
|
'auth/verify': { ssr: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
css: [
|
|
||||||
'@/assets/css/main.css',
|
|
||||||
],
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://v3.nuxtjs.org/api/configuration/nuxt.config#modules
|
* @see https://v3.nuxtjs.org/api/configuration/nuxt.config#modules
|
||||||
*/
|
*/
|
||||||
|
|
@ -39,7 +35,6 @@ export default defineNuxtConfig({
|
||||||
'@nuxt/image',
|
'@nuxt/image',
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
'dayjs-nuxt',
|
'dayjs-nuxt',
|
||||||
'nuxt-security',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
|
|
@ -48,28 +43,26 @@ export default defineNuxtConfig({
|
||||||
|
|
||||||
image: {
|
image: {
|
||||||
domains: [
|
domains: [
|
||||||
process.env.API_URL || 'http://127.0.0.1:8000'
|
process.env.API_URL || 'http://127.0.0.1:8000',
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
api: process.env.API_URL || 'http://127.0.0.1:8000'
|
api: process.env.API_URL || 'http://127.0.0.1:8000',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
security: {
|
security: {
|
||||||
headers: {
|
headers: {
|
||||||
crossOriginEmbedderPolicy: 'unsafe-none',
|
crossOriginEmbedderPolicy: 'unsafe-none',
|
||||||
crossOriginOpenerPolicy: 'same-origin-allow-popups',
|
crossOriginOpenerPolicy: 'same-origin-allow-popups',
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: false,
|
||||||
"img-src": ["'self'", "data:", "https://*", process.env.API_URL || 'http://127.0.0.1:8000'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dayjs: {
|
dayjs: {
|
||||||
locales: ['en'],
|
locales: ['en', 'de'],
|
||||||
plugins: ['relativeTime', 'utc', 'timezone'],
|
plugins: ['relativeTime', 'utc', 'timezone'],
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'de',
|
||||||
defaultTimezone: 'America/New_York',
|
defaultTimezone: 'Europe/Berlin',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -80,12 +73,12 @@ export default defineNuxtConfig({
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.API_URL,
|
apiBase: process.env.API_URL,
|
||||||
apiPrefix: '/api/v1',
|
apiPrefix: '/api/v1',
|
||||||
storageBase: process.env.API_URL + '/storage/',
|
storageBase: `${process.env.API_URL}/storage/`,
|
||||||
providers: {
|
providers: {
|
||||||
google: {
|
google: {
|
||||||
name: "Google",
|
name: 'Google',
|
||||||
icon: "",
|
icon: '',
|
||||||
color: "gray",
|
color: 'gray',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ export default defineAppConfig({
|
||||||
primary: 'sky',
|
primary: 'sky',
|
||||||
gray: 'cool',
|
gray: 'cool',
|
||||||
container: {
|
container: {
|
||||||
constrained: 'max-w-7xl w-full'
|
constrained: 'max-w-full w-full',
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
default: {
|
default: {
|
||||||
icon: 'i-heroicons-user',
|
icon: 'i-heroicons-user',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
label: 'Company',
|
label: 'Bücher',
|
||||||
icon: 'i-heroicons-building-office-2',
|
icon: 'i-heroicons-book-open',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: 'Overview',
|
label: 'Übersicht',
|
||||||
to: '/login',
|
to: '/book-recommendations',
|
||||||
icon: 'i-heroicons-eye',
|
icon: 'i-heroicons-eye',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Add Company',
|
|
||||||
to: '/login',
|
|
||||||
icon: 'i-heroicons-plus-circle',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||||
|
import DeleteBookRecommendation from '~/components/modal/DeleteBookRecommendation.vue'
|
||||||
|
import EditBookRecommendation from '~/components/modal/EditBookRecommendation.vue'
|
||||||
|
import NewBookRecommendation from '~/components/modal/NewBookRecommendation.vue'
|
||||||
|
import CastVote from '~/components/modal/CastVote.vue'
|
||||||
|
|
||||||
|
const dayjs = useDayjs()
|
||||||
|
|
||||||
|
const bookRecommendationStore = useBookRecommendationStore()
|
||||||
|
bookRecommendationStore.fetchRecommendations()
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: 'book_name',
|
||||||
|
label: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'author',
|
||||||
|
label: 'Autor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
label: 'Beschreibung',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pages',
|
||||||
|
label: 'Seiten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'recommender.name',
|
||||||
|
label: 'Empfohlen von',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
label: 'Erstellt am',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'votes',
|
||||||
|
label: 'Votes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NewBookRecommendation />
|
||||||
|
<UTable :loading="bookRecommendationStore.fetchRecommendationsStatus === 'pending'" :columns="columns" :rows="bookRecommendationStore.recommendations">
|
||||||
|
<template #created_at-data="{ row }">
|
||||||
|
<div>{{ dayjs(row.created_at).format('DD.MM.YYYY') }}</div>
|
||||||
|
</template>
|
||||||
|
<template #description-data="{ row }">
|
||||||
|
{{ `${row.description.substring(0, 50)}...` }}
|
||||||
|
</template>
|
||||||
|
<template #votes-data="{ row }">
|
||||||
|
{{ row.votes.length }}
|
||||||
|
</template>
|
||||||
|
<template #actions-data="{ row }">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<CastVote :row="row" />
|
||||||
|
<EditBookRecommendation :row="row" />
|
||||||
|
<DeleteBookRecommendation :row="row" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
row: {
|
||||||
|
id: number
|
||||||
|
book_name: string
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
book_recommendation_id: props.row.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const bookRecommendationStore = useBookRecommendationStore()
|
||||||
|
|
||||||
|
const { refresh: onVote, status } = useFetch<any>(`vote`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: state,
|
||||||
|
immediate: false,
|
||||||
|
watch: false,
|
||||||
|
async onResponse({ response }) {
|
||||||
|
if (response.ok) {
|
||||||
|
useToast().add({
|
||||||
|
icon: 'i-heroicons-check-circle-20-solid',
|
||||||
|
title: 'Abstimmung erfolgreich.',
|
||||||
|
color: 'emerald',
|
||||||
|
})
|
||||||
|
await bookRecommendationStore.fetchRecommendations()
|
||||||
|
await authStore.fetchUser()
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButton icon="i-heroicons-star" size="sm" color="green" variant="solid" square :disabled="authStore.user.total_votes === 0" @click="isOpen = true" />
|
||||||
|
<UDashboardModal
|
||||||
|
v-model="isOpen"
|
||||||
|
title="Buch Empfehlung löschen"
|
||||||
|
:description="`Bist du dir sicher das du für die Buchempfehlung ${row.book_name} abstimmen möchtest?`"
|
||||||
|
icon="i-heroicons-star"
|
||||||
|
:ui="{
|
||||||
|
icon: { base: 'text-green-500 dark:text-green-400' } as any,
|
||||||
|
footer: { base: 'ml-16' } as any,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #footer>
|
||||||
|
<UButton color="primary" label="Abstimmung" :loading="status === 'pending'" @click="onVote" />
|
||||||
|
<UButton color="white" label="Abbrechen" @click="isOpen = false" />
|
||||||
|
</template>
|
||||||
|
</UDashboardModal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
row: {
|
||||||
|
id: number
|
||||||
|
book_name: string
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const bookRecommendationStore = useBookRecommendationStore()
|
||||||
|
|
||||||
|
const { refresh: onDelete, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
immediate: false,
|
||||||
|
watch: false,
|
||||||
|
async onResponse({ response }) {
|
||||||
|
if (response.ok) {
|
||||||
|
useToast().add({
|
||||||
|
icon: 'i-heroicons-check-circle-20-solid',
|
||||||
|
title: 'Buchempfehlung wurde gelöscht.',
|
||||||
|
color: 'emerald',
|
||||||
|
})
|
||||||
|
await bookRecommendationStore.fetchRecommendations()
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen.value = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UButton icon="i-heroicons-trash" size="sm" color="red" variant="solid" square @click="isOpen = true" />
|
||||||
|
<UDashboardModal
|
||||||
|
v-model="isOpen"
|
||||||
|
title="Buch Empfehlung löschen"
|
||||||
|
:description="`Möchtest du die Buchempfehlung ${row.book_name} wirklich löschen?`"
|
||||||
|
icon="i-heroicons-exclamation-circle"
|
||||||
|
:ui="{
|
||||||
|
icon: { base: 'text-red-500 dark:text-red-400' } as any,
|
||||||
|
footer: { base: 'ml-16' } as any,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #footer>
|
||||||
|
<UButton color="red" label="Löschen" :loading="status === 'pending'" @click="onDelete" />
|
||||||
|
<UButton color="white" label="Abbrechen" @click="isOpen = false" />
|
||||||
|
</template>
|
||||||
|
</UDashboardModal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
row: {
|
||||||
|
id: number
|
||||||
|
book_name: string
|
||||||
|
author: string
|
||||||
|
description: string
|
||||||
|
isbn: string
|
||||||
|
pages: number
|
||||||
|
cover_image?: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const form = ref()
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
book_name: props.row.book_name,
|
||||||
|
author: props.row.author,
|
||||||
|
description: props.row.description,
|
||||||
|
isbn: props.row.isbn,
|
||||||
|
pages: props.row.pages,
|
||||||
|
cover_image: props.row.cover_image,
|
||||||
|
status: props.row.status,
|
||||||
|
})
|
||||||
|
|
||||||
|
const bookRecommendationStore = useBookRecommendationStore()
|
||||||
|
|
||||||
|
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: state,
|
||||||
|
immediate: false,
|
||||||
|
watch: false,
|
||||||
|
async onResponse({ response }) {
|
||||||
|
if (response?.status === 422) {
|
||||||
|
form.value.setErrors(response._data?.errors)
|
||||||
|
}
|
||||||
|
else if (response.ok) {
|
||||||
|
useToast().add({
|
||||||
|
icon: 'i-heroicons-check-circle-20-solid',
|
||||||
|
title: 'Buchempfehlung wurde erfolgreich aktualisiert.',
|
||||||
|
color: 'emerald',
|
||||||
|
})
|
||||||
|
await bookRecommendationStore.fetchRecommendations()
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UButton icon="i-heroicons-pencil-square" size="sm" variant="solid" square @click="isOpen = true" />
|
||||||
|
|
||||||
|
<UModal v-model="isOpen">
|
||||||
|
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
Bearbeiten
|
||||||
|
</h3>
|
||||||
|
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||||
|
<UFormGroup label="Name" name="book_name">
|
||||||
|
<UInput v-model="state.book_name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Autor" name="author">
|
||||||
|
<UInput v-model="state.author" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Beschreibung" name="description">
|
||||||
|
<UTextarea v-model="state.description" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="ISBN" name="isbn">
|
||||||
|
<UInput v-model="state.isbn" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Seiten" name="pages">
|
||||||
|
<UInput v-model="state.pages" type="number" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Status" name="status">
|
||||||
|
<USelect v-model="state.status" :options="bookRecommendationStore.statusOptions" option-attribute="name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UButton size="md" type="submit" :loading="status === 'pending'">
|
||||||
|
Speichern
|
||||||
|
</UButton>
|
||||||
|
<UButton size="md" class="mx-4" color="white" label="Abbrechen" @click="isOpen = false" />
|
||||||
|
</UForm>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const form = ref()
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
book_name: null,
|
||||||
|
author: null,
|
||||||
|
description: null,
|
||||||
|
isbn: null,
|
||||||
|
pages: null,
|
||||||
|
cover_image: null,
|
||||||
|
status: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const bookRecommendationStore = useBookRecommendationStore()
|
||||||
|
|
||||||
|
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: state,
|
||||||
|
immediate: false,
|
||||||
|
watch: false,
|
||||||
|
async onResponse({ response }) {
|
||||||
|
if (response?.status === 422) {
|
||||||
|
form.value.setErrors(response._data?.errors)
|
||||||
|
}
|
||||||
|
else if (response.ok) {
|
||||||
|
useToast().add({
|
||||||
|
icon: 'i-heroicons-check-circle-20-solid',
|
||||||
|
title: 'Buchempfehlung wurde erfolgreich aktualisiert.',
|
||||||
|
color: 'emerald',
|
||||||
|
})
|
||||||
|
await bookRecommendationStore.fetchRecommendations()
|
||||||
|
|
||||||
|
state.book_name = null
|
||||||
|
state.author = null
|
||||||
|
state.description = null
|
||||||
|
state.isbn = null
|
||||||
|
state.pages = null
|
||||||
|
state.cover_image = null
|
||||||
|
state.status = null
|
||||||
|
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<UButton icon="i-heroicons-plus" label="Neues Buch" size="sm" variant="solid" color="green" square @click="isOpen = true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UModal v-model="isOpen">
|
||||||
|
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
Neues Buch
|
||||||
|
</h3>
|
||||||
|
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||||
|
<UFormGroup label="Name" name="book_name">
|
||||||
|
<UInput v-model="state.book_name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Autor" name="author">
|
||||||
|
<UInput v-model="state.author" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Beschreibung" name="description">
|
||||||
|
<UTextarea v-model="state.description" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="ISBN" name="isbn">
|
||||||
|
<UInput v-model="state.isbn" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Seiten" name="pages">
|
||||||
|
<UInput v-model="state.pages" type="number" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Status" name="status">
|
||||||
|
<USelect v-model="state.status" :options="bookRecommendationStore.statusOptions" option-attribute="name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UButton size="md" type="submit" :loading="status === 'pending'">
|
||||||
|
Erstellen
|
||||||
|
</UButton>
|
||||||
|
<UButton size="md" class="mx-4" color="white" label="Abbrechen" @click="isOpen = false" />
|
||||||
|
</UForm>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BookRecommendationTable from '~/components/book-recommendations/BookRecommendationTable.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BookRecommendationTable />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,26 +1,27 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
export type User = {
|
export interface User {
|
||||||
ulid: string;
|
ulid: string
|
||||||
name: string;
|
name: string
|
||||||
email: string;
|
email: string
|
||||||
avatar: string;
|
avatar: string
|
||||||
must_verify_email: boolean;
|
must_verify_email: boolean
|
||||||
has_password: boolean;
|
has_password: boolean
|
||||||
roles: string[];
|
roles: string[]
|
||||||
providers: string[];
|
providers: string[]
|
||||||
|
total_votes: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
const user = ref(<User>{});
|
const user = ref(<User>{})
|
||||||
const token = useCookie('token', {
|
const token = useCookie('token', {
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: config.public.apiBase.startsWith('https://'),
|
secure: config.public.apiBase.startsWith('https://'),
|
||||||
maxAge: 60 * 60 * 24 * 365
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
})
|
})
|
||||||
const isLoggedIn = computed(() => !!token.value)
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
return navigateTo('/')
|
return navigateTo('/')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { refresh: fetchUser } = useFetch<any>('user', {
|
const { refresh: fetchUser } = useFetch<any>('user', {
|
||||||
|
|
@ -45,7 +46,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
user.value = response._data.user
|
user.value = response._data.user
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return { user, isLoggedIn, logout, fetchUser, token }
|
return { user, isLoggedIn, logout, fetchUser, token }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
enum BookRecommendationStatusEnum {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
REJECTED = 'REJECTED',
|
||||||
|
ACTIVE = 'ACTIVE',
|
||||||
|
COMPLETED = 'COMPLETED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookRecommendation {
|
||||||
|
id: number
|
||||||
|
book_name: string
|
||||||
|
author: string
|
||||||
|
description: string
|
||||||
|
isbn: string
|
||||||
|
pages: number
|
||||||
|
recommended_by?: number
|
||||||
|
recommender?: {
|
||||||
|
ulid: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
status: BookRecommendationStatusEnum
|
||||||
|
cover_image?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
|
||||||
|
const recommendations = ref<BookRecommendation[]>([])
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{
|
||||||
|
name: 'Pending',
|
||||||
|
value: BookRecommendationStatusEnum.PENDING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rejected',
|
||||||
|
value: BookRecommendationStatusEnum.REJECTED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Active',
|
||||||
|
value: BookRecommendationStatusEnum.ACTIVE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Completed',
|
||||||
|
value: BookRecommendationStatusEnum.COMPLETED,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Fetch all book recommendations
|
||||||
|
const { refresh: fetchRecommendations, status: fetchRecommendationsStatus } = useFetch<BookRecommendation[]>('book-recommendations?with=recommender,votes', {
|
||||||
|
immediate: false,
|
||||||
|
onResponse({ response }) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
recommendations.value = response._data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteRecommendation = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const { error } = await useFetch(`book-recommendations/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Failed to delete book recommendation:', error.value)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recommendations.value = recommendations.value.filter(rec => rec.id !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('An error occurred while deleting a book recommendation:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
recommendations,
|
||||||
|
statusOptions,
|
||||||
|
fetchRecommendations,
|
||||||
|
fetchRecommendationsStatus,
|
||||||
|
deleteRecommendation,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"api": "php artisan octane:start --watch --port=8000 --host=127.0.0.1"
|
"api": "php artisan octane:start --watch --port=8000 --host=127.0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^2.8.3",
|
||||||
"@iconify-json/heroicons": "^1.1.20",
|
"@iconify-json/heroicons": "^1.1.20",
|
||||||
"@iconify/vue": "^4.1.1",
|
"@iconify/vue": "^4.1.1",
|
||||||
"@nuxt/devtools": "^1.0.8",
|
"@nuxt/devtools": "^1.0.8",
|
||||||
|
|
@ -22,6 +23,8 @@
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dayjs-nuxt": "^2.1.9",
|
"dayjs-nuxt": "^2.1.9",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.15.1",
|
||||||
"nuxt": "^3.11.0",
|
"nuxt": "^3.11.0",
|
||||||
"nuxt-security": "^1.2.2",
|
"nuxt-security": "^1.2.2",
|
||||||
"vue": "3.4.21",
|
"vue": "3.4.21",
|
||||||
|
|
|
||||||
1631
pnpm-lock.yaml
1631
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
use App\Http\Controllers\AccountController;
|
use App\Http\Controllers\AccountController;
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
|
use App\Http\Controllers\BookRecommendationController;
|
||||||
use App\Http\Controllers\UploadController;
|
use App\Http\Controllers\UploadController;
|
||||||
|
use App\Http\Controllers\VoteController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
|
|
@ -25,6 +27,9 @@ Route::prefix('api/v1')->group(function () {
|
||||||
Route::get('devices', [AuthController::class, 'devices'])->name('devices');
|
Route::get('devices', [AuthController::class, 'devices'])->name('devices');
|
||||||
Route::get('user', [AuthController::class, 'user'])->name('user');
|
Route::get('user', [AuthController::class, 'user'])->name('user');
|
||||||
|
|
||||||
|
Route::resource('book-recommendations', BookRecommendationController::class);
|
||||||
|
Route::post('/vote', [VoteController::class, 'castVote']);
|
||||||
|
|
||||||
Route::post('account/update', [AccountController::class, 'update'])->name('account.update');
|
Route::post('account/update', [AccountController::class, 'update'])->name('account.update');
|
||||||
Route::post('account/password', [AccountController::class, 'password'])->name('account.password');
|
Route::post('account/password', [AccountController::class, 'password'])->name('account.password');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue