Cursor-rules
PromptBeginner5 minmarkdown
Repo rules
- This provisioning code is designed to run on Manjaro Linux.
2
**Разрабатывает:** новичок + ИИ помощник
Sign in to like and favorite skills
- This provisioning code is designed to run on Manjaro Linux.
.llmrules
Code Style and Structure
Разрабатывает: новичок + ИИ помощник Поддерживает: тот же новичок Принцип: код должен быть понятен через 6 месяцев
fn(), а calculateTotalPrice()Платформа услуг массажа = дизайн Ozon + функционал Avito
Backend: Laravel 12, PHP 8.2+ Frontend: Vue 3 (Composition API), Inertia.js, Pinia Стили: Tailwind CSS Сборка: Vite
❌ ПЛОХО: MasterProfile.vue (2000 строк) ✅ ХОРОШО: - MasterProfile/index.vue (композиция) - MasterProfile/Header.vue (шапка) - MasterProfile/Gallery.vue (галерея) - MasterProfile/Services.vue (услуги) - MasterProfile/Reviews.vue (отзывы)
resources/js/ ├── Components/ │ ├── UI/ # Базовые элементы │ │ ├── Button/ │ │ │ ├── Button.vue (основной компонент) │ │ │ ├── ButtonIcon.vue (с иконкой) │ │ │ └── ButtonGroup.vue (группа кнопок) │ │ ├── Card/ │ │ │ ├── Card.vue │ │ │ ├── CardHeader.vue │ │ │ └── CardFooter.vue │ │ └── Form/ │ │ ├── Input.vue │ │ ├── Select.vue │ │ └── Textarea.vue │ │ │ ├── Master/ # Компоненты мастера │ │ ├── MasterCard/ │ │ │ ├── index.vue (главный) │ │ │ ├── MasterCardImage.vue │ │ │ ├── MasterCardInfo.vue │ │ │ ├── MasterCardPrice.vue │ │ │ └── MasterCardActions.vue │ │ ├── MasterProfile/ │ │ │ ├── index.vue │ │ │ ├── ProfileHeader.vue │ │ │ ├── ProfileGallery.vue │ │ │ ├── ProfileServices.vue │ │ │ └── ProfileReviews.vue │ │ └── MasterFilters/ │ │ ├── index.vue │ │ ├── PriceFilter.vue │ │ ├── LocationFilter.vue │ │ └── CategoryFilter.vue │ │ │ └── Common/ # Общие компоненты │ ├── Header/ │ ├── Footer/ │ └── Sidebar/ │ ├── Composables/ # Переиспользуемая логика │ ├── useMaster.js # Работа с мастерами │ ├── useBooking.js # Бронирование │ ├── useFilters.js # Фильтрация │ └── useAuth.js # Авторизация │ └── Pages/ # Страницы (тонкие) ├── Masters/ │ ├── Index.vue # Список (использует компоненты) │ └── Show.vue # Детальная (использует модули) └── Bookings/ └── Create.vue
<!-- ❌ ПЛОХО: Один большой компонент --> <template> <div class="master-card"> <!-- 500 строк кода --> </div> </template> <!-- ✅ ХОРОШО: Разбито на части --> <template> <Card class="master-card"> <MasterCardImage :images="master.images" /> <MasterCardInfo :master="master" /> <MasterCardPrice :price="master.price" /> <MasterCardActions :master-id="master.id" @favorite="toggleFavorite" @book="openBooking" /> </Card> </template>
// ❌ ПЛОХО: Вся логика в одном файле export default { data() { return { masters: [], filters: {}, favorites: [], booking: {}, // ... еще 20 свойств } }, methods: { loadMasters() {}, filterMasters() {}, addToFavorites() {}, createBooking() {}, // ... еще 30 методов } } // ✅ ХОРОШО: Композиции по функциям import { useMasters } from '@/Composables/useMasters' import { useFilters } from '@/Composables/useFilters' import { useFavorites } from '@/Composables/useFavorites' export default { setup() { const { masters, loadMasters } = useMasters() const { filters, applyFilters } = useFilters() const { favorites, toggleFavorite } = useFavorites() return { masters, filters, favorites } } }
Компонент: максимум 200 строк Composable: максимум 150 строк Страница: максимум 100 строк (только композиция) CSS блок: максимум 50 строк (остальное в отдельные файлы)
<!-- Components/Master/MasterCard/index.vue --> <template> <article class="master-card"> <MasterCardImage :src="master.avatar" :alt="master.name" :badges="master.badges" @favorite="$emit('favorite')" /> <MasterCardInfo :name="master.name" :rating="master.rating" :reviews-count="master.reviews_count" :specialization="master.specialization" /> <MasterCardPrice :price-from="master.price_from" :discount="master.discount" /> <MasterCardActions :master-id="master.id" :phone="master.phone" @book="$emit('book')" @call="$emit('call')" /> </article> </template> <script setup> // Только пропсы и эмиты - вся логика в дочерних компонентах defineProps({ master: { type: Object, required: true } }) defineEmits(['favorite', 'book', 'call']) </script>
// Composables/useMasters.js export function useMasters() { const masters = ref([]) const loading = ref(false) const error = ref(null) // Загрузка списка const loadMasters = async (filters = {}) => { loading.value = true error.value = null try { const { data } = await axios.get('/api/masters', { params: filters }) masters.value = data.data } catch (e) { error.value = 'Не удалось загрузить мастеров' console.error('Ошибка загрузки:', e) } finally { loading.value = false } } // Поиск по имени const searchMasters = (query) => { return masters.value.filter(master => master.name.toLowerCase().includes(query.toLowerCase()) ) } return { masters: readonly(masters), loading: readonly(loading), error: readonly(error), loadMasters, searchMasters } }
// ❌ ПЛОХО: Толстый контроллер class MasterController extends Controller { public function index(Request $request) { // 200 строк логики фильтрации // 100 строк форматирования // 50 строк кеширования } } // ✅ ХОРОШО: Модульный подход class MasterController extends Controller { public function __construct( private MasterService $masterService, private FilterService $filterService ) {} public function index(MasterFilterRequest $request) { $filters = $this->filterService->parse($request); $masters = $this->masterService->getFiltered($filters); return MasterResource::collection($masters); } }
// app/Services/MasterService.php class MasterService { public function getFiltered(array $filters): LengthAwarePaginator { return Master::query() ->active() ->withFilters($filters) ->withRelations() ->paginate(20); } } // app/Services/BookingService.php class BookingService { public function create(array $data): Booking { // Валидация времени $this->validateTimeSlot($data); // Создание брони $booking = Booking::create($data); // Уведомления $this->notificationService->bookingCreated($booking); return $booking; } }
<!-- Components/UI/Button/Button.vue --> <template> <button :class="buttonClasses" :disabled="disabled || loading" @click="$emit('click', $event)" > <SpinnerIcon v-if="loading" class="w-4 h-4 animate-spin" /> <slot v-else /> </button> </template> <script setup> import { computed } from 'vue' import { useButtonStyles } from './useButtonStyles' const props = defineProps({ variant: { type: String, default: 'primary', validator: (v) => ['primary', 'success', 'secondary'].includes(v) }, size: { type: String, default: 'md', validator: (v) => ['sm', 'md', 'lg'].includes(v) }, disabled: Boolean, loading: Boolean }) const buttonClasses = computed(() => useButtonStyles(props)) </script>
// Components/UI/Button/useButtonStyles.js export function useButtonStyles({ variant, size, disabled }) { const base = 'font-medium rounded-lg transition-all transform active:scale-95' const variants = { primary: 'bg-[#005BFF] hover:bg-[#0048CC] text-white', success: 'bg-[#00D46A] hover:bg-[#00B055] text-white', secondary: 'bg-white border border-gray-300 hover:border-[#005BFF]' } const sizes = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-3 text-lg' } const state = disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer' return [base, variants[variant], sizes[size], state].join(' ') }
<!-- Компонент автоматически адаптируется --> <template> <div class="master-grid"> <!-- Мобильная: 1 колонка, Планшет: 2, Десктоп: 3 --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <MasterCard v-for="master in masters" :key="master.id" :master="master" /> </div> </div> </template>
1. **Что делаем:** Создаем модуль карточки мастера 2. **Структура файлов:** Components/Master/MasterCard/ ├── index.vue (главный) ├── MasterCardImage.vue (картинка) └── MasterCardInfo.vue (информация) 3. **Почему так:** - Легче найти нужный код - Можно переиспользовать части - Проще тестировать 4. **Код каждого файла:** [с комментариями] 5. **Как подключить:** import MasterCard from '@/Components/Master/MasterCard' 6. **Что проверить:** - Работает на мобильном - Все части отображаются - Кнопки кликаются
1. **Название функции:** formatPrice (не fp или fmt) 2. **Что делает:** Форматирует цену как "1 500 ₽" 3. **Где разместить:** utils/formatters.js 4. **Код с примерами:** formatPrice(1500) // "1 500 ₽" formatPrice(0) // "Бесплатно"
# Создать компонент с папкой mkdir -p resources/js/Components/Master/MasterCard touch resources/js/Components/Master/MasterCard/index.vue # Создать сервис php artisan make:service MasterService # Проверить что работает npm run dev php artisan serve # Открыть http://localhost:8000
Перед коммитом проверь:
ПОМНИ: