Compare commits

..

No commits in common. "8c595aa3c6f6398b24c6c557b314f4f9fae651f9" and "12d8f3913c087709f2c6a2861da897da58daaf70" have entirely different histories.

39 changed files with 158 additions and 1309 deletions

View File

@ -1,55 +0,0 @@
<?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.');
}
}
}
}

View File

@ -14,16 +14,10 @@ class BookRecommendationController extends Controller
public function index()
{
$relations = [];
$bookRecommendations = BookRecommendation::query();
if (request()->has('with')) {
$relations = explode(',', request()->with);
$bookRecommendations->with($relations);
}
if(request()->has('status')) {
$bookRecommendations->where('status', request()->status);
}
return $bookRecommendations->get();
return BookRecommendation::with($relations)->get();
}
/**
@ -56,7 +50,7 @@ class BookRecommendationController extends Controller
$data['cover_image'] = $imagePath;
}
$bookRecommendation = BookRecommendation::create([...$data, 'recommended_by' => auth()->id()]);
$bookRecommendation = BookRecommendation::create([...$request->all(), 'recommended_by' => auth()->id()]);
return response()->json($bookRecommendation, 201);
}
@ -130,9 +124,6 @@ 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);
}

View File

@ -1,104 +0,0 @@
<?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.']);
}
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers;
use App\Models\BookRecommendation;
use App\Models\User;
use App\Models\Vote;
use Illuminate\Http\Request;
@ -30,22 +29,4 @@ 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.']);
}
}

View File

@ -17,7 +17,6 @@ class BookRecommendation extends Model
'recommended_by',
'cover_image',
'status',
'published_at',
];
/**
@ -32,9 +31,4 @@ class BookRecommendation extends Model
{
return $this->hasMany(Vote::class);
}
public function deadlines()
{
return $this->hasMany(Deadline::class);
}
}

View File

@ -1,27 +0,0 @@
<?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);
}
}

View File

@ -63,11 +63,6 @@ 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();

View File

@ -1,26 +0,0 @@
<?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);
}
}

View File

@ -21,7 +21,6 @@ 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');

View File

@ -1,31 +0,0 @@
<?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');
}
};

View File

@ -1,30 +0,0 @@
<?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');
}
};

View File

@ -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,107 +24,6 @@ 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'
]);
}
}
}

View File

@ -25,8 +25,8 @@ class DatabaseSeeder extends Seeder
// create admin user
$user = \App\Models\User::factory()->create([
'name' => 'Philipp',
'email' => 'hi@flycro.me',
'name' => 'Super',
'email' => 'admin@admin.com',
'password' => Hash::make('password'),
]);
@ -36,40 +36,6 @@ 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);
}
}

View File

@ -30,13 +30,11 @@ 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: {

View File

@ -1,7 +1,18 @@
<script setup lang="ts">
import ColorPicker from '~/components/color-picker/ColorPicker.vue'
const links = []
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',
}]
</script>
<template>
@ -11,7 +22,6 @@ const links = []
</template>
<template #right>
<ColorPicker />
<UColorModeButton />
<UserDropdown />
</template>

View File

@ -1,5 +1,5 @@
<template>
<div>
<span class="text-primary">Chapter</span> Stack
<span class="text-primary">Nuxt</span> Breeze
</div>
</template>

View File

@ -1,14 +1,5 @@
<script setup lang="ts">
import type { NavigationTree } from '#ui-pro/types'
const authStore = useAuthStore()
const links: NavigationTree[] = [
{
label: 'Dashboard',
to: '/',
icon: 'i-heroicons-home',
},
const links = [
{
label: 'Bücher',
icon: 'i-heroicons-book-open',
@ -20,27 +11,12 @@ const links: NavigationTree[] = [
},
],
},
{
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>

View File

@ -1,23 +1,15 @@
<script setup lang="ts">
const authStore = useAuthStore()
const links = [{
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>

View File

@ -1,48 +0,0 @@
<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>

View File

@ -19,10 +19,6 @@ const columns = [
key: 'author',
label: 'Autor',
},
{
key: 'published_at',
label: 'Erstveröffentlichung',
},
{
key: 'description',
label: 'Beschreibung',
@ -46,46 +42,27 @@ 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 :sort="sort" :loading="bookRecommendationStore.fetchRecommendationsStatus === 'pending'" :columns="columns" :rows="bookRecommendationStore.recommendations">
<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 #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" />

View File

@ -1,56 +0,0 @@
<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>

View File

@ -1,25 +0,0 @@
<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>

View File

@ -1,89 +0,0 @@
<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>

View File

@ -1,58 +0,0 @@
<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>

View File

@ -1,12 +1,10 @@
<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
}
}>()
@ -18,10 +16,6 @@ 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`, {
@ -45,14 +39,14 @@ const { refresh: onVote, status } = useFetch<any>(`vote`, {
</script>
<template>
<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" />
<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="Für Buch abstimmen"
:description="`Bist du dir sicher das du für die Buchempfehlung &quot;${row.book_name}&quot; abstimmen möchtest?`"
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-primary-500 dark:text-primary-400' } as any,
icon: { base: 'text-green-500 dark:text-green-400' } as any,
footer: { base: 'ml-16' } as any,
}"
>

View File

@ -1,45 +0,0 @@
<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>

View File

@ -5,19 +5,13 @@ 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,
@ -37,7 +31,7 @@ const { refresh: onDelete, status } = useFetch<any>(() => `book-recommendations/
</script>
<template>
<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" />
<UButton icon="i-heroicons-trash" size="sm" color="red" variant="solid" square @click="isOpen = true" />
<UDashboardModal
v-model="isOpen"
title="Buch Empfehlung löschen"

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { useBookRecommendationStore } from '~/stores/book-recommendations'
import type { FormSubmitEvent } from '#ui/types'
const props = defineProps<{
row: {
@ -10,39 +9,16 @@ const props = defineProps<{
description: string
isbn: string
pages: number
cover_image?: string | File
cover_image?: string
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()
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({
const state = reactive({
book_name: props.row.book_name,
author: props.row.author,
description: props.row.description,
@ -50,55 +26,16 @@ const state: 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()
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}`, {
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations/${props.row.id}`, {
method: 'PUT',
body: formData,
body: state,
immediate: false,
watch: false,
async onResponse({ response }) {
loading.value = false
if (response?.status === 422) {
form.value.setErrors(response._data?.errors)
}
@ -108,17 +45,16 @@ async function onSubmit(event: FormSubmitEvent<any>) {
title: 'Buchempfehlung wurde erfolgreich aktualisiert.',
color: 'emerald',
})
await bookRecommendationStore.fetchRecommendations()
isOpen.value = false
}
},
})
await bookRecommendationStore.fetchRecommendations()
}
</script>
<template>
<div>
<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" />
<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' }">
@ -131,23 +67,16 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</div>
</template>
<UForm ref="form" :state="state" class="space-y-4" enctype="multipart/form-data" @submit="onSubmit">
<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="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>
@ -157,7 +86,7 @@ async function onSubmit(event: FormSubmitEvent<any>) {
<UFormGroup label="Status" name="status">
<USelect v-model="state.status" :options="bookRecommendationStore.statusOptions" option-attribute="name" />
</UFormGroup>
<UButton size="md" type="submit">
<UButton size="md" type="submit" :loading="status === 'pending'">
Speichern
</UButton>
<UButton size="md" class="mx-4" color="white" label="Abbrechen" @click="isOpen = false" />

View File

@ -2,95 +2,50 @@
import { useBookRecommendationStore } from '~/stores/book-recommendations'
const isOpen = ref(false)
const loading = ref(false)
const dayjs = useDayjs()
const form = ref()
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 state = reactive({
book_name: null,
author: null,
description: null,
isbn: null,
pages: null,
cover_image: null,
status: null,
})
const bookRecommendationStore = useBookRecommendationStore()
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`, {
const { refresh: onSubmit, status } = useFetch<any>(`book-recommendations`, {
method: 'POST',
body: formData,
body: state,
immediate: false,
watch: false,
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 angelegt.',
title: 'Buchempfehlung wurde erfolgreich aktualisiert.',
color: 'emerald',
})
await bookRecommendationStore.fetchRecommendations()
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')
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>
@ -117,15 +72,9 @@ async function onSubmit() {
<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>

View File

@ -1,84 +0,0 @@
<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>

View File

@ -1,13 +0,0 @@
<script setup lang="ts">
definePageMeta({ middleware: ['role-admin'] })
</script>
<template>
<div>
Admin
</div>
</template>
<style scoped>
</style>

View File

@ -1,22 +0,0 @@
<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>

View File

@ -1,7 +1,5 @@
<script setup lang="ts">
import BookRecommendationTable from '~/components/book-recommendations/BookRecommendationTable.vue'
definePageMeta({ middleware: ['auth'] })
</script>
<template>

View File

@ -1,15 +1,13 @@
<script setup lang="ts">
import BookInfoCard from '~/components/dashboard/BookInfoCard.vue'
definePageMeta({ middleware: ['auth'] })
const bookRecommendationStore = useBookRecommendationStore()
bookRecommendationStore.fetchActiveRecommendations()
const modal = useModal();
const router = useRouter();
const auth = useAuthStore();
</script>
<template>
<div v-if="bookRecommendationStore.fetchActiveRecommendationsStatus !== 'pending'">
<BookInfoCard v-for="book in bookRecommendationStore.recommendations" :key="book.id" :book="book" />
<div>
This is the Page Content
</div>
</template>

View File

@ -1,60 +1,58 @@
<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();
interface Provider {
name: string
icon: string
color: string
loading?: boolean
}
type 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
form.value.setErrors(response._data?.errors);
} else if (response._data?.ok) {
auth.token = response._data.token;
await auth.fetchUser()
await router.push('/')
await auth.fetchUser();
await router.push("/");
}
},
})
}
});
const providers = ref<{ [key: string]: Provider }>(config.public.providers)
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>

View File

@ -1,49 +0,0 @@
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,
},
],
})
}
},
})

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
export enum BookRecommendationStatusEnum {
enum BookRecommendationStatusEnum {
PENDING = 'PENDING',
REJECTED = 'REJECTED',
ACTIVE = 'ACTIVE',
@ -23,7 +23,6 @@ export interface BookRecommendation {
}
status: BookRecommendationStatusEnum
cover_image?: string
published_at?: string
}
export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
@ -31,25 +30,20 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
const statusOptions = [
{
name: 'Ausstehend',
name: 'Pending',
value: BookRecommendationStatusEnum.PENDING,
color: 'orange',
},
{
name: 'Abgelehnt',
name: 'Rejected',
value: BookRecommendationStatusEnum.REJECTED,
color: 'red',
},
{
name: 'Aktiv',
name: 'Active',
value: BookRecommendationStatusEnum.ACTIVE,
color: 'green',
},
{
name: 'Abgeschlossen',
name: 'Completed',
value: BookRecommendationStatusEnum.COMPLETED,
color: 'primary',
},
]
@ -63,30 +57,29 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
},
})
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
}
},
const deleteRecommendation = async (id: number) => {
try {
const { error } = await useFetch(`book-recommendations/${id}`, {
method: 'DELETE',
})
function resetRecommendations() {
recommendations.value = []
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,
resetRecommendations,
statusOptions,
fetchRecommendations,
fetchRecommendationsStatus,
fetchActiveRecommendations,
fetchActiveRecommendationsStatus,
deleteRecommendation,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useBookRecommendationStore, import.meta.hot))
}

View File

@ -3,7 +3,6 @@
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;
@ -29,16 +28,8 @@ 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');
@ -46,3 +37,4 @@ Route::prefix('api/v1')->group(function () {
Route::post('upload', [UploadController::class, 'image'])->name('upload.image');
});
});
});

View File

@ -1,12 +0,0 @@
import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>>{
theme: {
extend: {
fontFamily: {
sans: ['DM Sans', ...defaultTheme.fontFamily.sans],
},
},
},
}