feat: debut d'un front vue.js + ajout de cursorrules
This commit is contained in:
parent
ca9a74fe69
commit
bee8572dc5
@@ -0,0 +1,18 @@
|
||||
export class SearchMangas {
|
||||
constructor(mangaRepository) {
|
||||
this.mangaRepository = mangaRepository;
|
||||
}
|
||||
|
||||
async execute(query) {
|
||||
if (!query || query.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.mangaRepository.searchMangas(query);
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
assets/vue/app/domain/manga/application/store/mangaStore.js
Normal file
72
assets/vue/app/domain/manga/application/store/mangaStore.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
|
||||
|
||||
const mangaRepository = new ApiMangaRepository();
|
||||
|
||||
export const useMangaStore = defineStore('manga', () => {
|
||||
const collection = ref(null);
|
||||
const detailedMangas = ref({});
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const isBackgroundLoading = ref(false);
|
||||
|
||||
const loadCollection = async () => {
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
collection.value = await mangaRepository.getCollection();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
console.error('Failed to load collection:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCollectionInBackground = async () => {
|
||||
if (isBackgroundLoading.value) return;
|
||||
|
||||
isBackgroundLoading.value = true;
|
||||
|
||||
try {
|
||||
const updatedCollection = await mangaRepository.getCollection();
|
||||
collection.value = updatedCollection;
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh collection:', err);
|
||||
} finally {
|
||||
isBackgroundLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMangaDetail = async (slug) => {
|
||||
if (detailedMangas.value[slug]) return;
|
||||
|
||||
try {
|
||||
const manga = await mangaRepository.getMangaBySlug(slug);
|
||||
detailedMangas.value[slug] = manga;
|
||||
} catch (err) {
|
||||
console.error(`Failed to load manga details for ${slug}:`, err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getMangaFromCollection = (slug) => {
|
||||
return collection.value?.items.find(manga => manga.slug === slug);
|
||||
};
|
||||
|
||||
return {
|
||||
collection,
|
||||
detailedMangas,
|
||||
loading,
|
||||
error,
|
||||
isBackgroundLoading,
|
||||
loadCollection,
|
||||
refreshCollectionInBackground,
|
||||
loadMangaDetail,
|
||||
getMangaFromCollection
|
||||
};
|
||||
});
|
||||
42
assets/vue/app/domain/manga/domain/entities/manga.js
Normal file
42
assets/vue/app/domain/manga/domain/entities/manga.js
Normal file
@@ -0,0 +1,42 @@
|
||||
export class Manga {
|
||||
constructor({
|
||||
id,
|
||||
slug,
|
||||
title,
|
||||
description = null,
|
||||
authors = [],
|
||||
imageUrl = null,
|
||||
publicationYear = null,
|
||||
status = null,
|
||||
rating = null,
|
||||
genres = [],
|
||||
createdAt = new Date().toISOString()
|
||||
}) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.authors = authors;
|
||||
this.imageUrl = imageUrl;
|
||||
this.publicationYear = publicationYear;
|
||||
this.status = status;
|
||||
this.rating = rating;
|
||||
this.genres = genres;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
return new Manga(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class MangaCollection {
|
||||
constructor(items, total, page, limit, hasNextPage, hasPreviousPage) {
|
||||
this.items = items.map(item => Manga.create(item));
|
||||
this.total = total;
|
||||
this.page = page;
|
||||
this.limit = limit;
|
||||
this.hasNextPage = hasNextPage;
|
||||
this.hasPreviousPage = hasPreviousPage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { MangaCollection } from '../../domain/entities/manga';
|
||||
|
||||
export class ApiMangaRepository {
|
||||
async getCollection() {
|
||||
try {
|
||||
const response = await fetch('/api/mangas');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch manga collection');
|
||||
}
|
||||
const data = await response.json();
|
||||
return new MangaCollection(
|
||||
data.items,
|
||||
data.total,
|
||||
data.page,
|
||||
data.limit,
|
||||
data.hasNextPage,
|
||||
data.hasPreviousPage
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMangaBySlug(slug) {
|
||||
try {
|
||||
const response = await fetch(`/api/mangas/${slug}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch manga details');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchMangas(query) {
|
||||
try {
|
||||
const response = await fetch(`/api/mangas/search?q=${encodeURIComponent(query)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to search mangas');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="relative pb-[150%]">
|
||||
<img
|
||||
:src="manga.imageUrl || 'https://via.placeholder.com/300x400'"
|
||||
:alt="manga.title"
|
||||
class="absolute inset-0 w-full h-full object-contain bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ manga.title }}</h3>
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-500">{{ manga.publicationYear }}</span>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-500">
|
||||
Added: {{ formatDate(manga.createdAt) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
manga: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(`/manga/${props.manga.slug}`);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-6">
|
||||
<MangaCard
|
||||
v-for="manga in mangas"
|
||||
:key="manga.id"
|
||||
:manga="manga"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MangaCard from './MangaCard.vue';
|
||||
|
||||
defineProps({
|
||||
mangas: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
67
assets/vue/app/domain/manga/presentation/pages/HomePage.vue
Normal file
67
assets/vue/app/domain/manga/presentation/pages/HomePage.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<Layout @add-manga-click="handleAddMangaClick">
|
||||
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
|
||||
<div class="container mx-auto px-4">
|
||||
<MangaGrid :mangas="collection?.items || []" />
|
||||
<div v-if="isBackgroundLoading" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
Mise à jour en cours...
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMangaStore } from '../../application/store/mangaStore';
|
||||
import Layout from '../../../../shared/components/layout/Layout.vue';
|
||||
import MangaGrid from '../components/MangaGrid.vue';
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
MagnifyingGlassIcon,
|
||||
Cog6ToothIcon,
|
||||
EyeIcon,
|
||||
ArrowsUpDownIcon,
|
||||
FunnelIcon
|
||||
} from '@heroicons/vue/24/outline';
|
||||
|
||||
const router = useRouter();
|
||||
const mangaStore = useMangaStore();
|
||||
|
||||
const {
|
||||
collection,
|
||||
loading,
|
||||
error,
|
||||
isBackgroundLoading
|
||||
} = storeToRefs(mangaStore);
|
||||
|
||||
const { loadCollection, refreshCollectionInBackground } = mangaStore;
|
||||
|
||||
onMounted(() => {
|
||||
loadCollection();
|
||||
});
|
||||
|
||||
const handleAddMangaClick = (query = '') => {
|
||||
router.push(`/add${query ? `?q=${encodeURIComponent(query)}` : ''}`);
|
||||
};
|
||||
|
||||
const toolbarConfig = {
|
||||
leftSection: [
|
||||
{
|
||||
icon: ArrowPathIcon,
|
||||
label: 'Refresh',
|
||||
onClick: refreshCollectionInBackground,
|
||||
active: isBackgroundLoading
|
||||
},
|
||||
{ icon: MagnifyingGlassIcon, label: 'Search', onClick: () => {} }
|
||||
],
|
||||
rightSection: [
|
||||
{ icon: Cog6ToothIcon, onClick: () => {} },
|
||||
{ icon: EyeIcon, onClick: () => {} },
|
||||
{ icon: ArrowsUpDownIcon, onClick: () => {} },
|
||||
{ icon: FunnelIcon, onClick: () => {} }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user