make CRUD for spacex#91
Conversation
alfinforwork
commented
Jan 28, 2026
- list of rockets (Show each rocket image, rocket name, and rocket description)
- filter the rockets
- add the new rocket
- the rocket detail (Show rocket image, rocket name, rocket description, cost per launch, country, first flight)
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive CRUD interface for SpaceX rockets, adding functionality to list, filter, view details, and create custom rockets. The implementation uses Vue 3 with Vuetify for UI components, Pinia for state management, and integrates with the SpaceX API.
Changes:
- Added complete state management with Pinia store for rocket data and custom rockets
- Implemented SpaceX API service layer with error handling for fetching rocket data
- Created responsive UI components for rocket list, detail views, and add rocket modal with Vuetify
Reviewed changes
Copilot reviewed 15 out of 19 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| src/stores/rocketStore.ts | Pinia store managing rocket state, filtering, and CRUD operations |
| src/services/spaceXApi.ts | API service layer for fetching rocket data from SpaceX API |
| src/router/index.ts | Updated router configuration to support auto-routes from unplugin-vue-router |
| src/plugins/index.ts | Added Pinia plugin registration |
| src/pages/index.vue | Updated home page to display RocketList component |
| src/pages/rocket/[id].vue | Dynamic route page for rocket detail view with error and loading states |
| src/components/RocketList/RocketList.vue | Main list component with filtering, grid layout, and navigation |
| src/components/RocketList/AddRocketModal.vue | Modal form for creating custom rockets |
| src/components/RocketDetail/RocketDetailHeader.vue | Header component displaying rocket image and basic info |
| src/components/RocketDetail/RocketDetailInfo.vue | Component displaying detailed rocket specifications |
| src/components/RocketDetail/RocketGallery.vue | Gallery component for displaying multiple rocket images |
| package.json | Added Pinia dependency |
| typed-router.d.ts | Generated TypeScript definitions for auto-routes |
| components.d.ts | Generated component type definitions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const rocket: Rocket = { | ||
| id: `custom-${Date.now()}`, | ||
| name: form.value.name, | ||
| type: form.value.type, | ||
| description: form.value.description, | ||
| cost_per_launch: form.value.cost_per_launch, | ||
| country: form.value.country, | ||
| first_flight: form.value.first_flight, | ||
| rocket_id: `custom-${Date.now()}`, | ||
| flickr_images: form.value.imageUrl ? [form.value.imageUrl] : [], | ||
| } |
There was a problem hiding this comment.
The id and rocket_id fields are being set in the modal (lines 105, 112) and then overwritten in the store's addRocket function (lines 92-93 of rocketStore.ts). This is redundant and creates unnecessary work. Either generate the IDs only in the store, or only in the modal, but not in both places.
| const rocket: Rocket = { | |
| id: `custom-${Date.now()}`, | |
| name: form.value.name, | |
| type: form.value.type, | |
| description: form.value.description, | |
| cost_per_launch: form.value.cost_per_launch, | |
| country: form.value.country, | |
| first_flight: form.value.first_flight, | |
| rocket_id: `custom-${Date.now()}`, | |
| flickr_images: form.value.imageUrl ? [form.value.imageUrl] : [], | |
| } | |
| const rocket = { | |
| name: form.value.name, | |
| type: form.value.type, | |
| description: form.value.description, | |
| cost_per_launch: form.value.cost_per_launch, | |
| country: form.value.country, | |
| first_flight: form.value.first_flight, | |
| flickr_images: form.value.imageUrl ? [form.value.imageUrl] : [], | |
| } as Rocket |
| watch(searchQuery, (newValue) => { | ||
| rocketStore.setFilterQuery(newValue) | ||
| }) |
There was a problem hiding this comment.
The watch function updates filterQuery immediately on every keystroke. For better performance, especially with large datasets, consider using a debounced watch or watchDebounced to avoid filtering the list on every character input. This will reduce unnecessary computations and improve the user experience.
|
|
||
| export const useRocketStore = defineStore('rocket', () => { | ||
| const rockets = ref<Rocket[]>([]) | ||
| const customRockets = ref<Rocket[]>([]) |
There was a problem hiding this comment.
Custom rockets added via the modal are stored only in memory and will be lost when the page is refreshed or the user navigates away. Consider persisting customRockets to localStorage or providing clear user feedback that these rockets are temporary. This could lead to user confusion when their added rockets disappear.
| <div | ||
| v-for="rocket in rocketStore.allRockets" | ||
| :key="rocket.id" | ||
| class="rocket-card" | ||
| @click="goToDetail(rocket.id)" | ||
| > | ||
| <div class="rocket-image"> | ||
| <img | ||
| v-if="rocket.flickr_images && rocket.flickr_images.length > 0" | ||
| :src="rocket.flickr_images[0]" | ||
| :alt="rocket.name" | ||
| class="image" | ||
| > | ||
| <div | ||
| v-else | ||
| class="placeholder" | ||
| > | ||
| <v-icon size="48"> | ||
| mdi-rocket | ||
| </v-icon> | ||
| </div> | ||
| </div> | ||
| <div class="rocket-info"> | ||
| <h2 class="rocket-name"> | ||
| {{ rocket.name }} | ||
| </h2> | ||
| <p class="rocket-description"> | ||
| {{ rocket.description }} | ||
| </p> | ||
| <div class="rocket-meta"> | ||
| <span class="badge">{{ rocket.type }}</span> | ||
| <span class="badge">{{ rocket.country }}</span> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The rocket cards are clickable but lack proper keyboard accessibility. Users navigating with keyboard cannot tab to these cards or activate them with Enter/Space keys. Consider making the cards proper interactive elements by using button elements or adding tabindex="0" and @keypress handlers to support keyboard navigation, which is important for accessibility compliance.
| customRockets.value.push({ | ||
| ...rocket, | ||
| id: `custom-${Date.now()}`, | ||
| rocket_id: `custom-${Date.now()}`, |
There was a problem hiding this comment.
The addRocket function generates duplicate IDs for 'id' and 'rocket_id' using Date.now() in the same call. While unlikely in normal usage, if the function executes quickly enough (which can happen in some JavaScript engines), both calls to Date.now() could return the same value. More importantly, when a rocket is passed in with these fields already set (lines 104-114 in AddRocketModal.vue), they get overwritten here, creating a discrepancy between what the modal created and what gets stored.
| customRockets.value.push({ | |
| ...rocket, | |
| id: `custom-${Date.now()}`, | |
| rocket_id: `custom-${Date.now()}`, | |
| const generatedId = `custom-${Date.now()}` | |
| const finalId = rocket.id || rocket.rocket_id || generatedId | |
| const finalRocketId = rocket.rocket_id || finalId | |
| customRockets.value.push({ | |
| ...rocket, | |
| id: finalId, | |
| rocket_id: finalRocketId, |
| <div | ||
| v-else-if="rocketStore.selectedRocket" | ||
| class="detail-content" | ||
| > | ||
| <RocketDetailHeader :rocket="rocketStore.selectedRocket" /> | ||
| <RocketDetailInfo :rocket="rocketStore.selectedRocket" /> | ||
| <RocketGallery :images="rocketStore.selectedRocket.flickr_images" /> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The component doesn't handle the case where rocketId is undefined or when the rocket is not found after loading (selectedRocket is null but no error). After loadRocketDetail completes successfully, if selectedRocket is still null, the UI will show nothing. Consider adding an else clause to handle this case with a "Rocket not found" message.
| height: 200px; | ||
| object-fit: cover; | ||
| border-radius: 8px; | ||
| cursor: pointer; |
There was a problem hiding this comment.
Gallery images are clickable (cursor: pointer) but have no click handler or keyboard interaction. This creates a misleading UX where users expect something to happen on click. Either remove the cursor pointer styling or implement functionality like opening images in a lightbox/modal for a better viewing experience.
| cursor: pointer; |
| <img | ||
| v-if="rocket.flickr_images && rocket.flickr_images.length > 0" | ||
| :src="rocket.flickr_images[0]" | ||
| :alt="rocket.name" | ||
| class="detail-image" | ||
| > |
There was a problem hiding this comment.
Images loaded from external URLs (Flickr) have no error handling. If an image fails to load, it will show a broken image icon. Consider adding an @error handler to the img tag to fall back to the placeholder icon when image loading fails, providing a better user experience.
| if (!form.value.name || !form.value.description) { | ||
| alert('Please fill in all required fields') | ||
| return | ||
| } |
There was a problem hiding this comment.
Using alert() for form validation feedback is not consistent with the rest of the application which uses Vuetify components like v-alert. Consider using a v-snackbar or inline validation error messages for better user experience and consistency with the UI framework.
| if (!form.value.name || !form.value.description) { | ||
| alert('Please fill in all required fields') | ||
| return | ||
| } | ||
|
|
||
| const rocket: Rocket = { | ||
| id: `custom-${Date.now()}`, | ||
| name: form.value.name, | ||
| type: form.value.type, | ||
| description: form.value.description, | ||
| cost_per_launch: form.value.cost_per_launch, | ||
| country: form.value.country, | ||
| first_flight: form.value.first_flight, | ||
| rocket_id: `custom-${Date.now()}`, | ||
| flickr_images: form.value.imageUrl ? [form.value.imageUrl] : [], |
There was a problem hiding this comment.
Form validation is incomplete. The validation only checks for name and description, but the form fields for type, country, and first_flight are marked as 'required' in the template (lines 14, 20, 40, 46). This creates an inconsistency between the HTML required attributes and the actual validation logic, which could confuse users and allow invalid data to be submitted.
| if (!form.value.name || !form.value.description) { | |
| alert('Please fill in all required fields') | |
| return | |
| } | |
| const rocket: Rocket = { | |
| id: `custom-${Date.now()}`, | |
| name: form.value.name, | |
| type: form.value.type, | |
| description: form.value.description, | |
| cost_per_launch: form.value.cost_per_launch, | |
| country: form.value.country, | |
| first_flight: form.value.first_flight, | |
| rocket_id: `custom-${Date.now()}`, | |
| flickr_images: form.value.imageUrl ? [form.value.imageUrl] : [], | |
| const { | |
| name, | |
| type, | |
| description, | |
| cost_per_launch, | |
| country, | |
| first_flight, | |
| imageUrl, | |
| } = form.value | |
| if ( | |
| !name || | |
| !type || | |
| !description || | |
| cost_per_launch === null || | |
| cost_per_launch === undefined || | |
| !country || | |
| !first_flight | |
| ) { | |
| alert('Please fill in all required fields') | |
| return | |
| } | |
| const rocket: Rocket = { | |
| id: `custom-${Date.now()}`, | |
| name, | |
| type, | |
| description, | |
| cost_per_launch, | |
| country, | |
| first_flight, | |
| rocket_id: `custom-${Date.now()}`, | |
| flickr_images: imageUrl ? [imageUrl] : [], |