generated from Flycro/laravel-nuxt
Compare commits
21 Commits
12d8f3913c
...
8c595aa3c6
| Author | SHA1 | Date |
|---|---|---|
|
|
8c595aa3c6 | |
|
|
b001ea937f | |
|
|
fc63da0a3a | |
|
|
48a87c9b7b | |
|
|
837441ecb8 | |
|
|
bd8225374a | |
|
|
726c86dbcd | |
|
|
e393ba5943 | |
|
|
1d7f41e812 | |
|
|
2953acd630 | |
|
|
74d6f3efaa | |
|
|
95ece0f614 | |
|
|
fffe1b4717 | |
|
|
923e41b396 | |
|
|
cbc54210f0 | |
|
|
1a836b71f0 | |
|
|
3857ff70d2 | |
|
|
b97f8c785f | |
|
|
5ab2c46335 | |
|
|
096698a8e1 | |
|
|
daa80f4ff3 |
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BookRecommendation;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class OpenLibraryFetchCoverArt extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'book:open-library-fetch-cover-art';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Fetch cover art for books from Open Library API.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$books = BookRecommendation::whereNull('cover_image')->get();
|
||||
$this->info('Fetching cover art for '.$books->count().' books.');
|
||||
foreach ($books as $book) {
|
||||
$this->info('Fetching cover art for '.$book->book_name);
|
||||
$isbn = str_replace('-', '', $book->isbn);
|
||||
$url = 'https://covers.openlibrary.org/b/isbn/'.$isbn.'-L.jpg';
|
||||
$response = Http::get($url);
|
||||
if ($response->status() === 200) {
|
||||
$image = getimagesizefromstring($response->body());
|
||||
if ($image[0] <= 1 || $image[1] <= 1) {
|
||||
$this->error('Failed to fetch cover art.');
|
||||
continue;
|
||||
}
|
||||
$this->info('Cover art fetched.');
|
||||
$imagePath = 'cover_images/'.$isbn.'.jpg';
|
||||
Storage::disk('public')->put($imagePath, $response->body());
|
||||
$book->cover_image = $imagePath;
|
||||
$book->save();
|
||||
$this->info('Cover art fetched and saved.');
|
||||
} else {
|
||||
$this->error('Failed to fetch cover art.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,10 +14,16 @@ class BookRecommendationController extends Controller
|
|||
public function index()
|
||||
{
|
||||
$relations = [];
|
||||
$bookRecommendations = BookRecommendation::query();
|
||||
if (request()->has('with')) {
|
||||
$relations = explode(',', request()->with);
|
||||
$bookRecommendations->with($relations);
|
||||
}
|
||||
return BookRecommendation::with($relations)->get();
|
||||
if(request()->has('status')) {
|
||||
$bookRecommendations->where('status', request()->status);
|
||||
}
|
||||
|
||||
return $bookRecommendations->get();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,7 +56,7 @@ class BookRecommendationController extends Controller
|
|||
$data['cover_image'] = $imagePath;
|
||||
}
|
||||
|
||||
$bookRecommendation = BookRecommendation::create([...$request->all(), 'recommended_by' => auth()->id()]);
|
||||
$bookRecommendation = BookRecommendation::create([...$data, 'recommended_by' => auth()->id()]);
|
||||
return response()->json($bookRecommendation, 201);
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +130,9 @@ class BookRecommendationController extends Controller
|
|||
if ($bookRecommendation->recommended_by !== auth()->id() && !(auth()->user()->hasRole('admin')) ) {
|
||||
return response()->json(['message' => 'Keine Berechtigung.'], 403);
|
||||
}
|
||||
if ($bookRecommendation->cover_image) {
|
||||
Storage::delete($bookRecommendation->cover_image);
|
||||
}
|
||||
$bookRecommendation->delete();
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Deadline;
|
||||
use App\Models\UserDeadline;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DeadlineController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
$deadlines = Deadline::with(['bookRecommendation' => function ($query) {
|
||||
$query->where('status', 'active');
|
||||
}, 'userDeadlines'])
|
||||
->get();
|
||||
|
||||
|
||||
$deadlines->each(function ($deadline) use ($userId) {
|
||||
UserDeadline::firstOrCreate([
|
||||
'user_id' => $userId,
|
||||
'deadline_id' => $deadline->id,
|
||||
], [
|
||||
'completed_at' => null, // Default value, indicating not completed
|
||||
]);
|
||||
});
|
||||
|
||||
$deadlinesWithAllUserProgress = Deadline::with(['bookRecommendation', 'userDeadlines.user'])
|
||||
->get();
|
||||
|
||||
return response()->json($deadlinesWithAllUserProgress);
|
||||
}
|
||||
|
||||
public function deadlinesByBookRecommendation($bookRecommendationId)
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$refetch = false;
|
||||
|
||||
$deadlines = Deadline::with(['userDeadlines' => function ($query) use ($userId) {
|
||||
$query->where('user_id', $userId);
|
||||
}, 'userDeadlines.user'])
|
||||
->where('book_recommendation_id', $bookRecommendationId)
|
||||
->get();
|
||||
|
||||
$deadlines->each(function ($deadline) use ($userId, &$refetch) {
|
||||
UserDeadline::firstOrCreate([
|
||||
'user_id' => $userId,
|
||||
'deadline_id' => $deadline->id,
|
||||
], [
|
||||
'completed_at' => null, // Default to not completed.
|
||||
]);
|
||||
$refetch = true;
|
||||
});
|
||||
|
||||
if ($refetch) {
|
||||
$deadlines = Deadline::with(['userDeadlines' => function ($query) use ($userId) {
|
||||
$query->where('user_id', $userId);
|
||||
}, 'userDeadlines.user'])
|
||||
->where('book_recommendation_id', $bookRecommendationId)
|
||||
->get();
|
||||
}
|
||||
|
||||
return response()->json($deadlines);
|
||||
}
|
||||
|
||||
public function createDeadline(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'book_recommendation_id' => 'required|integer|exists:book_recommendations,id',
|
||||
'deadline' => 'required|date',
|
||||
'target_page' => 'nullable|integer',
|
||||
'target_chapter' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
if (!(auth()->user()->hasRole('admin')) ) {
|
||||
return response()->json(['message' => 'Keine Berechtigung.'], 403);
|
||||
}
|
||||
|
||||
$deadline = new Deadline($validated);
|
||||
$deadline->save();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Deadline created successfully.',
|
||||
'deadline' => $deadline,
|
||||
], 201);
|
||||
}
|
||||
|
||||
|
||||
public function updateUserDeadline(Request $request, $deadlineId)
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$userDeadline = UserDeadline::where('deadline_id', $deadlineId)
|
||||
->where('user_id', $userId)
|
||||
->firstOrFail();
|
||||
|
||||
$userDeadline->completed_at = now(); // Mark as completed
|
||||
$userDeadline->save();
|
||||
|
||||
return response()->json(['message' => 'Deadline marked as completed successfully.']);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\BookRecommendation;
|
||||
use App\Models\User;
|
||||
use App\Models\Vote;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
|
@ -29,4 +30,22 @@ class VoteController extends Controller
|
|||
|
||||
return response()->json(['message' => 'No remaining votes.'], 403);
|
||||
}
|
||||
|
||||
public function addTotalVotesAll(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'total_votes' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
if (!(auth()->user()->hasRole('admin')) ) {
|
||||
return response()->json(['message' => 'Keine Berechtigung.'], 403);
|
||||
}
|
||||
|
||||
$users = User::all();
|
||||
foreach ($users as $user) {
|
||||
$user->increment('total_votes', $request->total_votes);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Total votes added to all book recommendations.']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class BookRecommendation extends Model
|
|||
'recommended_by',
|
||||
'cover_image',
|
||||
'status',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -31,4 +32,9 @@ class BookRecommendation extends Model
|
|||
{
|
||||
return $this->hasMany(Vote::class);
|
||||
}
|
||||
|
||||
public function deadlines()
|
||||
{
|
||||
return $this->hasMany(Deadline::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Deadline extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'book_recommendation_id',
|
||||
'deadline',
|
||||
'target_page',
|
||||
'target_chapter',
|
||||
];
|
||||
|
||||
public function bookRecommendation()
|
||||
{
|
||||
return $this->belongsTo(BookRecommendation::class);
|
||||
}
|
||||
public function userDeadlines()
|
||||
{
|
||||
return $this->hasMany(UserDeadline::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
return $this->hasMany(Vote::class);
|
||||
}
|
||||
|
||||
public function deadlinesProgress()
|
||||
{
|
||||
return $this->hasMany(UserDeadline::class);
|
||||
}
|
||||
|
||||
public function mustVerifyEmail(): bool
|
||||
{
|
||||
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 UserDeadline extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'deadline_id',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
public function deadline()
|
||||
{
|
||||
return $this->belongsTo(Deadline::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ return new class extends Migration
|
|||
$table->string('cover_image')->nullable();
|
||||
$table->unsignedBigInteger('recommended_by');
|
||||
$table->enum('status', ['PENDING', 'COMPLETED', 'REJECTED','ACTIVE'])->default('PENDING');
|
||||
$table->dateTime('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('recommended_by')->references('id')->on('users')->onDelete('cascade');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?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('deadlines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('book_recommendation_id')->constrained()->onDelete('cascade');
|
||||
$table->date('deadline');
|
||||
$table->unsignedInteger('target_page')->nullable();
|
||||
$table->string('target_chapter')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('deadlines');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?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('user_deadlines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('deadline_id')->constrained('deadlines')->onDelete('cascade');
|
||||
$table->dateTime('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_deadlines');
|
||||
}
|
||||
};
|
||||
|
|
@ -14,7 +14,7 @@ class BookRecommendationsTableSeeder extends Seeder
|
|||
public function run(): void
|
||||
{
|
||||
// Let's say we want to create 50 fake book recommendations
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
/*for ($i = 0; $i < 50; $i++) {
|
||||
BookRecommendation::create([
|
||||
'book_name' => fake()->sentence($nbWords = 3, $variableNbWords = true),
|
||||
'author' => fake()->name,
|
||||
|
|
@ -24,6 +24,107 @@ class BookRecommendationsTableSeeder extends Seeder
|
|||
'recommended_by' => 1, // Adjust the range as necessary
|
||||
'cover_image' => null, // You could also simulate image paths if needed
|
||||
]);
|
||||
}
|
||||
}*/
|
||||
|
||||
BookRecommendation::create(
|
||||
[
|
||||
'book_name' => '100 Jahre Einsamkeit',
|
||||
'author' => 'Gabriel García Márquez',
|
||||
'description' => 'Die Geschichte der Familie Buendía und ihres Dorfes Macondo, das sie gründeten, ist ein Epos voller Leidenschaft, Tragik und Komik. Die Geschichte der Familie Buendía und ihres Dorfes Macondo, das sie gründeten, ist ein Epos voller Leidenschaft, Tragik und Komik.',
|
||||
'isbn' => '978-3-462-05021-9',
|
||||
'pages' => 528,
|
||||
'recommended_by' => 1,
|
||||
'cover_image' => null,
|
||||
'published_at' => '1967-05-30'
|
||||
]
|
||||
);
|
||||
BookRecommendation::create(
|
||||
[
|
||||
'book_name' => 'Die Mitternachtsbibliothek',
|
||||
'author' => 'Matt Haig',
|
||||
'description' => 'Nora Seed erkennt, dass sie viele Leben gelebt hat. Nicht die, die sie sich erhofft hatte. In der Mitternachtsbibliothek erhält sie die Möglichkeit, diese zu ändern.',
|
||||
'isbn' => '978-3-426-28256-4',
|
||||
'pages' => 320,
|
||||
'recommended_by' => 1,
|
||||
'cover_image' => null,
|
||||
'published_at' => '2020-08-13'
|
||||
]
|
||||
);
|
||||
BookRecommendation::create(
|
||||
[
|
||||
'book_name' => 'Die Schwarze Königin',
|
||||
'author' => 'Markus Heitz',
|
||||
'description' => 'Blutig, actionreich, dramatisch: Bestseller-Autor Markus Heitz kehrt mit seinem Dark-Fantasy-Roman »Die Schwarze Königin« zu den Vampiren zurück!',
|
||||
'isbn' => '978-3-426-22781-7',
|
||||
'pages' => 544,
|
||||
'recommended_by' => 4,
|
||||
'cover_image' => null,
|
||||
'published_at' => '2023-08-21'
|
||||
]
|
||||
);
|
||||
BookRecommendation::create(
|
||||
[
|
||||
'book_name' => 'The Stand - Das letzte Gefecht',
|
||||
'author' => 'Stephen King',
|
||||
'description' => 'In einem entvölkerten Amerika versucht eine Handvoll Überlebende die Zivilisation zu retten. Ihr Gegenspieler ist eine mythische Gestalt, die man den Dunklen Mann nennt, eine Verkörperung des absolut Bösen. In der Wüste Nevada kommt es zum Entscheidungskampf um das Schicksal der Menschheit.',
|
||||
'isbn' => '978-3-45343818-7',
|
||||
'pages' => 1712,
|
||||
'recommended_by' => 4,
|
||||
'cover_image' => null,
|
||||
'published_at' => '1978-09-01'
|
||||
]
|
||||
);
|
||||
BookRecommendation::create([
|
||||
'book_name' => 'Der Fremde',
|
||||
'author' => 'Albert Camus',
|
||||
'description' => 'Die Geschichte eines jungen Franzosen in Algerien, den ein lächerlicher Zufall zum Mörder macht, wurde 1942 im besetzten Frankreich zu einer literarischen Sensation. Der Roman bedeutete den schriftstellerischen Durchbruch für Albert Camus und gilt heute als einer der Haupttexte des Existenzialismus.',
|
||||
'isbn' => '978-3-49922189-7',
|
||||
'pages' => 160,
|
||||
'recommended_by' => 2,
|
||||
'cover_image' => null,
|
||||
'published_at' => '1942-06-01'
|
||||
]);
|
||||
BookRecommendation::create([
|
||||
'book_name' => 'No Longer Human',
|
||||
'author' => 'Osamu Dazai',
|
||||
'description' => 'No Longer Human is a Japanese novel by Osamu Dazai. It is considered Dazai\'s masterpiece and ranks as the second-best selling novel in Japan, behind Natsume Sōseki\'s Kokoro.',
|
||||
'isbn' => '978-0-81120481-1',
|
||||
'pages' => 177,
|
||||
'recommended_by' => 2,
|
||||
'cover_image' => null,
|
||||
'published_at' => '1948-06-01'
|
||||
]);
|
||||
BookRecommendation::create([
|
||||
'book_name' => 'After Dark',
|
||||
'author' => 'Haruki Murakami',
|
||||
'description' => 'Geschichten zwischen Mitternacht und Morgengrauen: Murakami begleitet seine Helden eine Nacht lang und zeichnet ein eindrucksvolles, geheimnisvoll schillerndes Großstadtporträt: Die 19-jährige Mari, ihre schöne, aber unglückliche Schwester Eri, der unscheinbare Posaunist, die Prostituierte aus einem Love Hotel und ein gewissenloser Freier streifen durch das nächtliche Tokyo.',
|
||||
'isbn' => '978-3-44273564-8',
|
||||
'pages' => 191,
|
||||
'recommended_by' => 2,
|
||||
'cover_image' => null,
|
||||
'published_at' => '2004-01-01'
|
||||
]);
|
||||
BookRecommendation::create([
|
||||
'book_name' => 'Futu.re',
|
||||
'author' => 'Dmitry Glukhovsky',
|
||||
'description' => 'Moskau 2038: Der junge Student Innokenti wächst in einer Welt auf, die von Technologie und künstlicher Intelligenz bestimmt wird. Doch er ist ein Träumer und glaubt an die Kraft der Literatur. Als er eines Tages auf die geheimnisvolle Chiski trifft, die in einer Parallelwelt lebt, gerät sein Leben aus den Fugen. Chiski ist auf der Flucht vor einer mächtigen Organisation, die die Welt beherrscht und die Literatur vernichten will. Innokenti wird in einen Strudel aus Gewalt und Intrigen gezogen und muss sich entscheiden: Wird er zum Helden oder zum Verräter?',
|
||||
'isbn' => '978-3-45331758-1',
|
||||
'pages' => 928,
|
||||
'recommended_by' => 3,
|
||||
'cover_image' => null,
|
||||
'published_at' => '2017-02-06'
|
||||
]);
|
||||
BookRecommendation::create([
|
||||
'book_name' => 'Die Seele des Königs',
|
||||
'author' => 'Brandon Sanderson',
|
||||
'description' => 'Die Seele des Königs ist der erste Band der neuen Serie von Bestsellerautor Brandon Sanderson. Die Serie spielt in einer Welt, in der die Magie auf dem Konzept der Metalle basiert. Die Serie spielt in einer Welt, in der die Magie auf dem Konzept der Metalle basiert.',
|
||||
'isbn' => '978-3-45331524-2',
|
||||
'pages' => 448,
|
||||
'recommended_by' => 3,
|
||||
'cover_image' => null,
|
||||
'published_at' => '2012-10-10'
|
||||
]);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ class DatabaseSeeder extends Seeder
|
|||
|
||||
// create admin user
|
||||
$user = \App\Models\User::factory()->create([
|
||||
'name' => 'Super',
|
||||
'email' => 'admin@admin.com',
|
||||
'name' => 'Philipp',
|
||||
'email' => 'hi@flycro.me',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
|
|
@ -36,6 +36,40 @@ class DatabaseSeeder extends Seeder
|
|||
|
||||
$user->assignRole($role1);
|
||||
|
||||
$user = \App\Models\User::factory()->create([
|
||||
'name' => 'Marcel',
|
||||
'email' => 'marcel@test.de',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
$user->ulid = Str::ulid()->toBase32();
|
||||
$user->email_verified_at = now();
|
||||
$user->save(['timestamps' => false]);
|
||||
|
||||
$user->assignRole($role1);
|
||||
|
||||
$user = \App\Models\User::factory()->create([
|
||||
'name' => 'Robert',
|
||||
'email' => 'robert@test.de',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
$user->ulid = Str::ulid()->toBase32();
|
||||
$user->email_verified_at = now();
|
||||
$user->save(['timestamps' => false]);
|
||||
|
||||
$user->assignRole($role2);
|
||||
|
||||
$user = \App\Models\User::factory()->create([
|
||||
'name' => 'Lukas',
|
||||
'email' => 'lukas@test.de',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
$user->ulid = Str::ulid()->toBase32();
|
||||
$user->email_verified_at = now();
|
||||
$user->save(['timestamps' => false]);
|
||||
|
||||
$user->assignRole($role2);
|
||||
|
||||
|
||||
$this->call(BookRecommendationsTableSeeder::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,13 @@ export default defineNuxtConfig({
|
|||
* @see https://v3.nuxtjs.org/api/configuration/nuxt.config#modules
|
||||
*/
|
||||
extends: ['@nuxt/ui-pro'],
|
||||
|
||||
modules: [
|
||||
'@nuxt/ui',
|
||||
'@nuxt/image',
|
||||
'@pinia/nuxt',
|
||||
'dayjs-nuxt',
|
||||
'nuxt-security',
|
||||
],
|
||||
|
||||
ui: {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
const links = [{
|
||||
label: 'Documentation',
|
||||
icon: 'i-heroicons-book-open',
|
||||
to: 'https://ui.nuxt.com/getting-started',
|
||||
}, {
|
||||
label: 'Pro',
|
||||
icon: 'i-heroicons-square-3-stack-3d',
|
||||
to: 'https://ui.nuxt.com/pro',
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-heroicons-rocket-launch',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank',
|
||||
}]
|
||||
import ColorPicker from '~/components/color-picker/ColorPicker.vue'
|
||||
|
||||
const links = []
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -22,6 +11,7 @@ const links = [{
|
|||
</template>
|
||||
|
||||
<template #right>
|
||||
<ColorPicker />
|
||||
<UColorModeButton />
|
||||
<UserDropdown />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<span class="text-primary">Nuxt</span> Breeze
|
||||
<span class="text-primary">Chapter</span> Stack
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
const links = [
|
||||
import type { NavigationTree } from '#ui-pro/types'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const links: NavigationTree[] = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
to: '/',
|
||||
icon: 'i-heroicons-home',
|
||||
},
|
||||
{
|
||||
label: 'Bücher',
|
||||
icon: 'i-heroicons-book-open',
|
||||
|
|
@ -11,12 +20,27 @@ const links = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'People',
|
||||
to: '/login',
|
||||
icon: 'i-heroicons-user-group',
|
||||
},
|
||||
]
|
||||
|
||||
if (authStore.user?.roles.includes('admin')) {
|
||||
links.push({
|
||||
label: 'Admin',
|
||||
to: '/admin',
|
||||
icon: 'i-heroicons-cog-solid',
|
||||
children: [
|
||||
{
|
||||
label: 'Übersicht',
|
||||
to: '/admin',
|
||||
icon: 'i-heroicons-eye',
|
||||
},
|
||||
{
|
||||
label: 'Votes',
|
||||
to: '/admin/votes',
|
||||
icon: 'i-heroicons-star',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
const links = [
|
||||
{
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const links = [{
|
||||
label: 'Account',
|
||||
to: '/account',
|
||||
icon: 'i-heroicons-user-solid',
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Logout',
|
||||
to: '/logout',
|
||||
icon: 'i-heroicons-arrow-left-on-rectangle',
|
||||
}]
|
||||
|
||||
if (authStore.user?.roles.includes('admin')) {
|
||||
links.push({
|
||||
label: 'Admin',
|
||||
to: '/admin',
|
||||
icon: 'i-heroicons-cog-solid',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
|
||||
const state = reactive({
|
||||
total_votes: 2,
|
||||
})
|
||||
|
||||
const { refresh: onClick, status } = useFetch<any>(`admin/add-total-votes-all`, {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Es wurden allen Nutzern 2 Votes hinzugefügt.',
|
||||
color: 'emerald',
|
||||
})
|
||||
}
|
||||
|
||||
isOpen.value = false
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton icon="i-heroicons-star" solid label="Nutzern Votes hinzufügen" @click="isOpen = true" />
|
||||
<UDashboardModal
|
||||
v-model="isOpen"
|
||||
title="Votes hinzufügen"
|
||||
description="Bist du dir sicher das du jedem Benutzer 2 Votes geben möchtest?"
|
||||
icon="i-heroicons-star"
|
||||
:ui="{
|
||||
icon: { base: 'text-primary dark:text-primary-400' } as any,
|
||||
footer: { base: 'ml-16' } as any,
|
||||
}"
|
||||
>
|
||||
<template #footer>
|
||||
<UButton color="primary" label="Bestätigen" :loading="status === 'pending'" @click="onClick" />
|
||||
<UButton color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
</template>
|
||||
</UDashboardModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -19,6 +19,10 @@ const columns = [
|
|||
key: 'author',
|
||||
label: 'Autor',
|
||||
},
|
||||
{
|
||||
key: 'published_at',
|
||||
label: 'Erstveröffentlichung',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Beschreibung',
|
||||
|
|
@ -42,27 +46,46 @@ const columns = [
|
|||
{
|
||||
key: 'votes',
|
||||
label: 'Votes',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
},
|
||||
]
|
||||
const sort = ref({
|
||||
column: 'votes',
|
||||
direction: 'desc',
|
||||
})
|
||||
|
||||
function resolveStatus(status: string) {
|
||||
return bookRecommendationStore.statusOptions.find(option => option.value === status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NewBookRecommendation />
|
||||
<UTable :loading="bookRecommendationStore.fetchRecommendationsStatus === 'pending'" :columns="columns" :rows="bookRecommendationStore.recommendations">
|
||||
<UTable :sort="sort" :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 #published_at-data="{ row }">
|
||||
<div>{{ dayjs(row.published_at).format('DD.MM.YYYY') }}</div>
|
||||
</template>
|
||||
<template #description-data="{ row }">
|
||||
<div v-if="row.description">
|
||||
{{ `${row.description.substring(0, 50)}...` }}
|
||||
</div>
|
||||
</template>
|
||||
<template #votes-data="{ row }">
|
||||
{{ row.votes.length }}
|
||||
</template>
|
||||
<template #status-data="{ row }">
|
||||
<UBadge :color="resolveStatus(row.status)?.color">
|
||||
{{ resolveStatus(row.status)?.name }}
|
||||
</UBadge>
|
||||
</template>
|
||||
<template #actions-data="{ row }">
|
||||
<div class="flex space-x-2">
|
||||
<CastVote :row="row" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
<script setup lang="ts">
|
||||
import colors from '#tailwind-config/theme/colors'
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
// Computed
|
||||
|
||||
const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
||||
const primary = computed({
|
||||
get() {
|
||||
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
|
||||
},
|
||||
set(option) {
|
||||
appConfig.ui.primary = option.value
|
||||
|
||||
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
|
||||
},
|
||||
})
|
||||
|
||||
const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
|
||||
const gray = computed({
|
||||
get() {
|
||||
return grayColors.value.find(option => option.value === appConfig.ui.gray)
|
||||
},
|
||||
set(option) {
|
||||
appConfig.ui.gray = option.value
|
||||
|
||||
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPopover mode="hover" :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
|
||||
<template #default="{ open }">
|
||||
<UButton color="gray" variant="ghost" square :class="[open && 'bg-gray-50 dark:bg-gray-800']" aria-label="Color picker">
|
||||
<UIcon name="i-heroicons-swatch-20-solid" class="text-primary-500 dark:text-primary-400 size-5" />
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<template #panel>
|
||||
<div class="p-2">
|
||||
<div class="grid grid-cols-5 gap-px">
|
||||
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
|
||||
</div>
|
||||
|
||||
<hr class="my-2 border-gray-200 dark:border-gray-800">
|
||||
|
||||
<div class="grid grid-cols-5 gap-px">
|
||||
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{ color: { value: string, hex: string }, selected: { value: string } }>()
|
||||
defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
|
||||
<UButton
|
||||
color="white"
|
||||
square
|
||||
:ui="{
|
||||
color: {
|
||||
white: {
|
||||
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
||||
},
|
||||
},
|
||||
}"
|
||||
:variant="color.value === selected.value ? 'solid' : 'ghost'"
|
||||
@click.stop.prevent="$emit('select')"
|
||||
>
|
||||
<span class="inline-block size-3 rounded-full" :style="{ backgroundColor: color.hex }" />
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<script setup lang="ts">
|
||||
import DeadlineTable from '~/components/deadline/DeadlineTable.vue'
|
||||
import NewDeadline from '~/components/modal/NewDeadline.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
book: BookRecommendation
|
||||
}>()
|
||||
|
||||
const auth = useAuthStore()
|
||||
const { $storage } = useNuxtApp()
|
||||
const dayjs = useDayjs()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard class="my-4">
|
||||
<div class="flex w-full justify-between md:space-x-8 lg:space-x-4">
|
||||
<div class="hidden w-1/5 md:block">
|
||||
<img :src="$storage(props.book.cover_image)" :alt="props.book.book_name" class="rounded-lg">
|
||||
</div>
|
||||
<div class="w-4/5 space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h1 class="font-sans text-3xl font-bold">
|
||||
{{ props.book.book_name }}
|
||||
</h1>
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<UBadge>
|
||||
{{ props.book.author }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<NewDeadline v-if="auth.user?.roles.includes('admin')" :book-recommendation-id="props.book.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UDivider />
|
||||
<div class="space-y-2">
|
||||
<p class="font-sans text-xl font-bold ">
|
||||
Beschreibung
|
||||
</p>
|
||||
<p class="w-full text-lg md:w-3/5">
|
||||
{{ props.book.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col flex-wrap justify-start gap-x-32 gap-y-4 pt-4 md:flex-row">
|
||||
<div>
|
||||
<p class="font-sans text-lg font-bold ">
|
||||
Seiten
|
||||
</p>
|
||||
<p class="text-lg">
|
||||
{{ props.book.pages }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-sans text-lg font-bold">
|
||||
Erstveröffentlichung
|
||||
</p>
|
||||
<p class="text-lg">
|
||||
{{ dayjs(props.book.published_at).format('MMM YYYY') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-sans text-lg font-bold">
|
||||
Empfohlen von
|
||||
</p>
|
||||
<p class="text-lg">
|
||||
{{ props.book.recommender.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-sans text-lg font-bold">
|
||||
Votes
|
||||
</p>
|
||||
<p class="text-lg">
|
||||
{{ props.book.votes.length }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DeadlineTable :book-recommendation-id="props.book.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<script setup lang="ts">
|
||||
import ConfirmUserDeadline from '~/components/modal/ConfirmUserDeadline.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
bookRecommendationId: number
|
||||
}>()
|
||||
|
||||
const { refresh: deadlineRefresh, status: deadlineStatus, data: deadlines } = useFetch(() => `book-recommendations/${props.bookRecommendationId}/deadlines`)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
label: 'Deadline',
|
||||
key: 'deadline',
|
||||
},
|
||||
{
|
||||
label: 'Ziel Seite',
|
||||
key: 'target_page',
|
||||
},
|
||||
{
|
||||
label: 'Ziel Kapitel',
|
||||
key: 'target_chapter',
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
key: 'actions',
|
||||
},
|
||||
]
|
||||
|
||||
const dayjs = useDayjs()
|
||||
|
||||
const rows = computed(() => {
|
||||
return deadlines.value?.map((deadline: any) => {
|
||||
return {
|
||||
id: deadline.id,
|
||||
deadline: dayjs(deadline.deadline).format('DD.MM.YYYY'),
|
||||
target_page: deadline.target_page,
|
||||
target_chapter: deadline.target_chapter,
|
||||
user_deadline: deadline.user_deadlines[0],
|
||||
actions: null,
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :columns="columns" :rows="rows" :empty-state="{ icon: 'i-heroicons-calendar-solid', label: 'Keine Deadlines.' }">
|
||||
<template #actions-data="{ row }">
|
||||
<UBadge v-if="row.user_deadline.completed_at !== null">
|
||||
Abgeschlossen - {{ dayjs(row.user_deadline.completed_at).format('DD.MM.YYYY') }}
|
||||
</UBadge>
|
||||
<ConfirmUserDeadline v-if="row.user_deadline.completed_at === null" :user-deadline-id="row.id" @update="deadlineRefresh" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { BookRecommendationStatusEnum } from '~/stores/book-recommendations'
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
|
||||
const props = defineProps<{
|
||||
row: {
|
||||
id: number
|
||||
book_name: string
|
||||
status: BookRecommendationStatusEnum
|
||||
}
|
||||
}>()
|
||||
|
||||
|
|
@ -16,6 +18,10 @@ const state = reactive({
|
|||
book_recommendation_id: props.row.id,
|
||||
})
|
||||
|
||||
watch(() => props.row, (newRow) => {
|
||||
state.book_recommendation_id = newRow.id
|
||||
})
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onVote, status } = useFetch<any>(`vote`, {
|
||||
|
|
@ -39,14 +45,14 @@ const { refresh: onVote, status } = useFetch<any>(`vote`, {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<UButton icon="i-heroicons-star" size="sm" color="green" variant="solid" square :disabled="authStore.user.total_votes === 0" @click="isOpen = true" />
|
||||
<UButton v-if="props.row.status === 'PENDING'" class="transition-150 transform-gpu hover:scale-110" 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?`"
|
||||
title="Für Buch abstimmen"
|
||||
: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,
|
||||
icon: { base: 'text-primary-500 dark:text-primary-400' } as any,
|
||||
footer: { base: 'ml-16' } as any,
|
||||
}"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
userDeadlineId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const { refresh: onConfirmDeadline, status } = useFetch<any>(() => `user-deadlines/${props.userDeadlineId}`, {
|
||||
method: 'PUT',
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Deadline erfolgreich Abgeschlossen.',
|
||||
color: 'emerald',
|
||||
})
|
||||
emit('update')
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton class="transition-150 transform-gpu hover:scale-110" icon="i-heroicons-book-open" size="sm" variant="solid" color="primary" square @click="isOpen = true" />
|
||||
<UDashboardModal
|
||||
v-model="isOpen"
|
||||
title="Deadline abschließen"
|
||||
description="Bist du dir sicher das du die Deadline abschließen möchtest?"
|
||||
icon="i-heroicons-book-open"
|
||||
:ui="{
|
||||
icon: { base: 'text-primary dark:text-primary-400' } as any,
|
||||
footer: { base: 'ml-16' } as any,
|
||||
}"
|
||||
>
|
||||
<template #footer>
|
||||
<UButton color="primary" label="Abschließen" :loading="status === 'pending'" @click="onConfirmDeadline" />
|
||||
<UButton color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
</template>
|
||||
</UDashboardModal>
|
||||
</template>
|
||||
|
|
@ -5,13 +5,19 @@ const props = defineProps<{
|
|||
row: {
|
||||
id: number
|
||||
book_name: string
|
||||
recommender: {
|
||||
ulid: string
|
||||
}
|
||||
}
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onDelete, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
|
||||
const { refresh: onDelete, status } = useFetch<any>(() => `book-recommendations/${props.row.id}`, {
|
||||
method: 'DELETE',
|
||||
immediate: false,
|
||||
watch: false,
|
||||
|
|
@ -31,7 +37,7 @@ const { refresh: onDelete, status } = useFetch<any>(`book-recommendations/${prop
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<UButton icon="i-heroicons-trash" size="sm" color="red" variant="solid" square @click="isOpen = true" />
|
||||
<UButton v-if="authStore.user.roles.includes('admin')" class="transition-150 transform-gpu hover:scale-110" icon="i-heroicons-trash" size="sm" color="red" variant="solid" square @click="isOpen = true" />
|
||||
<UDashboardModal
|
||||
v-model="isOpen"
|
||||
title="Buch Empfehlung löschen"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
const props = defineProps<{
|
||||
row: {
|
||||
|
|
@ -9,16 +10,39 @@ const props = defineProps<{
|
|||
description: string
|
||||
isbn: string
|
||||
pages: number
|
||||
cover_image?: string
|
||||
cover_image?: string | File
|
||||
status: string
|
||||
published_at: string
|
||||
recommender: {
|
||||
ulid: string
|
||||
}
|
||||
}
|
||||
}>()
|
||||
|
||||
const dayjs = useDayjs()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const { $storage } = useNuxtApp()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref()
|
||||
|
||||
const state = reactive({
|
||||
interface State {
|
||||
book_name: string
|
||||
author: string
|
||||
description: string
|
||||
isbn: string
|
||||
pages: number
|
||||
cover_image?: File | string
|
||||
status: string
|
||||
published_at: string
|
||||
// Index signature
|
||||
[key: string]: string | number | File | Date | undefined
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
book_name: props.row.book_name,
|
||||
author: props.row.author,
|
||||
description: props.row.description,
|
||||
|
|
@ -26,16 +50,55 @@ const state = reactive({
|
|||
pages: props.row.pages,
|
||||
cover_image: props.row.cover_image,
|
||||
status: props.row.status,
|
||||
published_at: dayjs(props.row.published_at).format('YYYY-MM-DD'),
|
||||
})
|
||||
|
||||
watch(() => props.row, (newRow) => {
|
||||
state.book_name = newRow.book_name
|
||||
state.author = newRow.author
|
||||
state.description = newRow.description
|
||||
state.isbn = newRow.isbn
|
||||
state.pages = newRow.pages
|
||||
state.cover_image = newRow.cover_image
|
||||
state.status = newRow.status
|
||||
state.published_at = dayjs(newRow.published_at).format('YYYY-MM-DD')
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
function handleCoverImageInput(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
// Update the state with the selected file
|
||||
state.cover_image = file
|
||||
}
|
||||
}
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
form.value.clear()
|
||||
loading.value = true
|
||||
const formData = new FormData()
|
||||
for (const key in state) {
|
||||
const item = state[key]
|
||||
if (item === undefined) {
|
||||
continue
|
||||
}
|
||||
if (key === 'cover_image' && state[key] instanceof File) {
|
||||
formData.append(key, item as Blob, (state[key] as File).name)
|
||||
}
|
||||
else {
|
||||
const value = typeof item === 'string' ? item : String(item)
|
||||
if (key === 'cover_image') {
|
||||
continue
|
||||
}
|
||||
formData.append(key, value)
|
||||
}
|
||||
}
|
||||
const response = await $fetch<any>(`book-recommendations/${props.row.id}`, {
|
||||
method: 'PUT',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
body: formData,
|
||||
async onResponse({ response }) {
|
||||
loading.value = false
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
|
|
@ -45,16 +108,17 @@ const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${prop
|
|||
title: 'Buchempfehlung wurde erfolgreich aktualisiert.',
|
||||
color: 'emerald',
|
||||
})
|
||||
await bookRecommendationStore.fetchRecommendations()
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
await bookRecommendationStore.fetchRecommendations()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton icon="i-heroicons-pencil-square" size="sm" variant="solid" square @click="isOpen = true" />
|
||||
<UButton v-if="authStore.user.ulid === props.row.recommender.ulid || authStore.user.roles.includes('admin')" class="transition-150 transform-gpu hover:scale-110" 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' }">
|
||||
|
|
@ -67,16 +131,23 @@ const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${prop
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UForm ref="form" :state="state" class="space-y-4" enctype="multipart/form-data" @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="Erstveröffentlichung">
|
||||
<UInput v-model="state.published_at" type="date" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Beschreibung" name="description">
|
||||
<UTextarea v-model="state.description" />
|
||||
</UFormGroup>
|
||||
<img v-if="state.cover_image" :src="$storage(state.cover_image)" alt="Cover" class="size-1/3 content-center rounded-lg">
|
||||
<UFormGroup label="Cover" name="cover_image">
|
||||
<UInput type="file" @change="handleCoverImageInput" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="ISBN" name="isbn">
|
||||
<UInput v-model="state.isbn" />
|
||||
</UFormGroup>
|
||||
|
|
@ -86,7 +157,7 @@ const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${prop
|
|||
<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'">
|
||||
<UButton size="md" type="submit">
|
||||
Speichern
|
||||
</UButton>
|
||||
<UButton size="md" class="mx-4" color="white" label="Abbrechen" @click="isOpen = false" />
|
||||
|
|
|
|||
|
|
@ -2,50 +2,95 @@
|
|||
import { useBookRecommendationStore } from '~/stores/book-recommendations'
|
||||
|
||||
const isOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const dayjs = useDayjs()
|
||||
|
||||
const form = ref()
|
||||
|
||||
const state = reactive({
|
||||
book_name: null,
|
||||
author: null,
|
||||
description: null,
|
||||
isbn: null,
|
||||
pages: null,
|
||||
cover_image: null,
|
||||
status: null,
|
||||
interface State {
|
||||
book_name: string
|
||||
author: string
|
||||
description: string
|
||||
isbn: string
|
||||
pages: number
|
||||
cover_image?: File | string
|
||||
status: string
|
||||
published_at: string
|
||||
// Index signature
|
||||
[key: string]: string | number | File | undefined
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
book_name: '',
|
||||
author: '',
|
||||
description: '',
|
||||
isbn: '',
|
||||
pages: 0,
|
||||
cover_image: '',
|
||||
status: 'PENDING',
|
||||
published_at: dayjs().format('YYYY-MM-DD'),
|
||||
})
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, {
|
||||
function handleCoverImageInput(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
// Update the state with the selected file
|
||||
state.cover_image = file
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
loading.value = true
|
||||
const formData = new FormData()
|
||||
for (const key in state) {
|
||||
const item = state[key]
|
||||
if (item === undefined) {
|
||||
continue
|
||||
}
|
||||
if (key === 'cover_image' && state[key] instanceof File) {
|
||||
formData.append(key, item as Blob, (state[key] as File).name)
|
||||
}
|
||||
else {
|
||||
if (key === 'cover_image') {
|
||||
continue
|
||||
}
|
||||
const value = typeof item === 'string' ? item : String(item)
|
||||
formData.append(key, value)
|
||||
}
|
||||
}
|
||||
await $fetch<any>(`book-recommendations`, {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
body: formData,
|
||||
async onResponse({ response }) {
|
||||
loading.value = false
|
||||
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.',
|
||||
title: 'Buchempfehlung wurde erfolgreich angelegt.',
|
||||
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
|
||||
state.book_name = ''
|
||||
state.author = ''
|
||||
state.description = ''
|
||||
state.isbn = ''
|
||||
state.pages = 0
|
||||
state.cover_image = ''
|
||||
state.status = 'PENDING'
|
||||
state.published_at = dayjs().format('YYYY-MM-DD')
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -72,9 +117,15 @@ const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, {
|
|||
<UFormGroup label="Autor" name="author">
|
||||
<UInput v-model="state.author" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Erstveröffentlichung">
|
||||
<UInput v-model="state.published_at" type="date" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Beschreibung" name="description">
|
||||
<UTextarea v-model="state.description" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Cover" name="cover_image">
|
||||
<UInput type="file" @change="handleCoverImageInput" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="ISBN" name="isbn">
|
||||
<UInput v-model="state.isbn" />
|
||||
</UFormGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
bookRecommendationId: number
|
||||
}>()
|
||||
const isOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
const form = ref()
|
||||
|
||||
interface NewDeadlineState {
|
||||
book_recommendation_id: number | null
|
||||
deadline: string
|
||||
target_page?: number
|
||||
target_chapter?: string
|
||||
}
|
||||
|
||||
const state: NewDeadlineState = reactive({
|
||||
book_recommendation_id: props.bookRecommendationId,
|
||||
deadline: '',
|
||||
target_page: undefined,
|
||||
target_chapter: undefined,
|
||||
})
|
||||
|
||||
async function onSubmit() {
|
||||
loading.value = true
|
||||
|
||||
await $fetch('deadlines', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
async onResponse({ response }) {
|
||||
loading.value = false
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Deadline erfolgreich erstellt.',
|
||||
color: 'emerald',
|
||||
})
|
||||
loading.value = false
|
||||
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UButton icon="i-heroicons-plus" label="Neue Deadline" size="sm" variant="solid" color="green" 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">
|
||||
Neue Deadline
|
||||
</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="Deadline" name="deadline">
|
||||
<UInput v-model="state.deadline" type="date" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Zielseite" name="target_page">
|
||||
<UInput v-model="state.target_page" type="number" />
|
||||
</UFormGroup>
|
||||
<UFormGroup label="Zielkapitel" name="target_chapter">
|
||||
<UInput v-model="state.target_chapter" />
|
||||
</UFormGroup>
|
||||
<UButton size="md" type="submit" :loading="loading">
|
||||
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">
|
||||
definePageMeta({ middleware: ['role-admin'] })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
Admin
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import AddVotes from '~/components/admin/AddVotes.vue'
|
||||
|
||||
definePageMeta({ middleware: ['role-admin'] })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UCard class="w-1/2">
|
||||
<template #header>
|
||||
<h1 class="font-sans text-3xl font-bold">
|
||||
Benutzer Aktionen
|
||||
</h1>
|
||||
</template>
|
||||
<AddVotes />
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import BookRecommendationTable from '~/components/book-recommendations/BookRecommendationTable.vue'
|
||||
|
||||
definePageMeta({ middleware: ['auth'] })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import BookInfoCard from '~/components/dashboard/BookInfoCard.vue'
|
||||
|
||||
definePageMeta({ middleware: ['auth'] })
|
||||
|
||||
const modal = useModal();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
|
||||
bookRecommendationStore.fetchActiveRecommendations()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
This is the Page Content
|
||||
<div v-if="bookRecommendationStore.fetchActiveRecommendationsStatus !== 'pending'">
|
||||
<BookInfoCard v-for="book in bookRecommendationStore.recommendations" :key="book.id" :book="book" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const form = ref();
|
||||
const config = useRuntimeConfig()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const form = ref()
|
||||
|
||||
type Provider = {
|
||||
name: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
loading?: boolean;
|
||||
};
|
||||
interface Provider {
|
||||
name: string
|
||||
icon: string
|
||||
color: string
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
email: "",
|
||||
password: "",
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: loginStatus } = useFetch<any>("login", {
|
||||
method: "POST",
|
||||
const { refresh: onSubmit, status: loginStatus } = useFetch<any>('login', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors);
|
||||
} else if (response._data?.ok) {
|
||||
auth.token = response._data.token;
|
||||
|
||||
await auth.fetchUser();
|
||||
await router.push("/");
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
}
|
||||
});
|
||||
else if (response._data?.ok) {
|
||||
auth.token = response._data.token
|
||||
|
||||
const providers = ref<{ [key: string]: Provider }>(config.public.providers);
|
||||
await auth.fetchUser()
|
||||
await router.push('/')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const providers = ref<{ [key: string]: Provider }>(config.public.providers)
|
||||
|
||||
async function handleMessage(event: { data: any }): Promise<void> {
|
||||
const provider = event.data.provider as string;
|
||||
const provider = event.data.provider as string
|
||||
|
||||
if (Object.keys(providers.value).includes(provider) && event.data.token) {
|
||||
providers.value[provider].loading = false;
|
||||
auth.token = event.data.token;
|
||||
providers.value[provider].loading = false
|
||||
auth.token = event.data.token
|
||||
|
||||
await auth.fetchUser();
|
||||
await router.push("/");
|
||||
} else if (event.data.message) {
|
||||
await auth.fetchUser()
|
||||
await router.push('/')
|
||||
}
|
||||
else if (event.data.message) {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-exclamation-circle-solid",
|
||||
color: "red",
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
color: 'red',
|
||||
title: event.data.message,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import colors from '#tailwind-config/theme/colors'
|
||||
import { hexToRgb } from '#ui/utils'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
enforce: 'post',
|
||||
setup() {
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const root = computed(() => {
|
||||
const primary: Record<string, string> | undefined = colors[appConfig.ui.primary]
|
||||
const gray: Record<string, string> | undefined = colors[appConfig.ui.gray]
|
||||
|
||||
return `:root {
|
||||
${Object.entries(primary || colors.green).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
|
||||
--color-primary-DEFAULT: var(--color-primary-500);
|
||||
|
||||
${Object.entries(gray || colors.cool).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-primary-DEFAULT: var(--color-primary-400);
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
if (process.client) {
|
||||
watch(root, () => {
|
||||
window.localStorage.setItem('nuxt-ui-root', root.value)
|
||||
})
|
||||
|
||||
appConfig.ui.primary = window.localStorage.getItem('nuxt-ui-primary') || appConfig.ui.primary
|
||||
appConfig.ui.gray = window.localStorage.getItem('nuxt-ui-gray') || appConfig.ui.gray
|
||||
}
|
||||
if (process.server) {
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
innerHTML: `
|
||||
if (localStorage.getItem('nuxt-ui-root')) {
|
||||
document.querySelector('style#nuxt-ui-colors').innerHTML = localStorage.getItem('nuxt-ui-root')
|
||||
}`.replace(/\s+/g, ' '),
|
||||
type: 'text/javascript',
|
||||
tagPriority: -1,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from 'pinia'
|
||||
|
||||
enum BookRecommendationStatusEnum {
|
||||
export enum BookRecommendationStatusEnum {
|
||||
PENDING = 'PENDING',
|
||||
REJECTED = 'REJECTED',
|
||||
ACTIVE = 'ACTIVE',
|
||||
|
|
@ -23,6 +23,7 @@ export interface BookRecommendation {
|
|||
}
|
||||
status: BookRecommendationStatusEnum
|
||||
cover_image?: string
|
||||
published_at?: string
|
||||
}
|
||||
|
||||
export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
|
||||
|
|
@ -30,20 +31,25 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
|
|||
|
||||
const statusOptions = [
|
||||
{
|
||||
name: 'Pending',
|
||||
name: 'Ausstehend',
|
||||
value: BookRecommendationStatusEnum.PENDING,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
name: 'Rejected',
|
||||
name: 'Abgelehnt',
|
||||
value: BookRecommendationStatusEnum.REJECTED,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
name: 'Active',
|
||||
name: 'Aktiv',
|
||||
value: BookRecommendationStatusEnum.ACTIVE,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
name: 'Completed',
|
||||
name: 'Abgeschlossen',
|
||||
value: BookRecommendationStatusEnum.COMPLETED,
|
||||
color: 'primary',
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -57,29 +63,30 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
|
|||
},
|
||||
})
|
||||
|
||||
const deleteRecommendation = async (id: number) => {
|
||||
try {
|
||||
const { error } = await useFetch(`book-recommendations/${id}`, {
|
||||
method: 'DELETE',
|
||||
const { refresh: fetchActiveRecommendations, status: fetchActiveRecommendationsStatus } = useFetch<BookRecommendation[]>('book-recommendations?with=recommender,votes&status=ACTIVE', {
|
||||
immediate: false,
|
||||
onResponse({ response }) {
|
||||
if (response.status === 200) {
|
||||
recommendations.value = response._data
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
function resetRecommendations() {
|
||||
recommendations.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
recommendations,
|
||||
resetRecommendations,
|
||||
statusOptions,
|
||||
fetchRecommendations,
|
||||
fetchRecommendationsStatus,
|
||||
deleteRecommendation,
|
||||
fetchActiveRecommendations,
|
||||
fetchActiveRecommendationsStatus,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useBookRecommendationStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Http\Controllers\AccountController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\BookRecommendationController;
|
||||
use App\Http\Controllers\DeadlineController;
|
||||
use App\Http\Controllers\UploadController;
|
||||
use App\Http\Controllers\VoteController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
|
@ -28,8 +29,16 @@ Route::prefix('api/v1')->group(function () {
|
|||
Route::get('user', [AuthController::class, 'user'])->name('user');
|
||||
|
||||
Route::resource('book-recommendations', BookRecommendationController::class);
|
||||
Route::get('/book-recommendations/{bookRecommendationId}/deadlines', [DeadlineController::class, 'deadlinesByBookRecommendation']);
|
||||
Route::post('/vote', [VoteController::class, 'castVote']);
|
||||
|
||||
Route::get('/deadlines', [DeadlineController::class, 'index']);
|
||||
Route::post('/deadlines', [DeadlineController::class, 'createDeadline']);
|
||||
Route::put('/user-deadlines/{deadlineId}', [DeadlineController::class, 'updateUserDeadline']);
|
||||
});
|
||||
|
||||
Route::post('/admin/add-total-votes-all', [VoteController::class, 'addTotalVotesAll']);
|
||||
|
||||
Route::post('account/update', [AccountController::class, 'update'])->name('account.update');
|
||||
Route::post('account/password', [AccountController::class, 'password'])->name('account.password');
|
||||
|
||||
|
|
@ -37,4 +46,3 @@ Route::prefix('api/v1')->group(function () {
|
|||
Route::post('upload', [UploadController::class, 'image'])->name('upload.image');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
|
||||
export default <Partial<Config>>{
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['DM Sans', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Loading…
Reference in New Issue