Vue Firebase Portfolio Integration
March 27, 2025 Β· AI Conversations
data will be
titile
description
url
and
image
create add/fetch operations
—
### β Prerequisites
1. Set up Firebase in your project:
– Go to [Firebase Console](https://console.firebase.google.com/)
– Create a project and enable Firestore & Storage
2. Install Firebase:
yarn add firebase
—
### π§ Step 1: Firebase Setup (`firebase.js`)
// src/firebase.js
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_PROJECT_ID.firebaseapp.com',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_PROJECT_ID.appspot.com',
messagingSenderId: 'YOUR_SENDER_ID',
appId: 'YOUR_APP_ID',
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const storage = getStorage(app);
—
### π€ Step 2: Add Portfolio Item
// src/composables/usePortfolio.js
import { db, storage } from '@/firebase';
import { collection, addDoc, getDocs } from 'firebase/firestore';
import { ref as storageRef, uploadBytes, getDownloadURL } from 'firebase/storage';
const portfolioCollection = collection(db, 'portfolio');
export const addPortfolioItem = async ({ title, description, url, imageFile }) => {
try {
// Upload image to storage
const imgRef = storageRef(storage, `portfolio/${imageFile.name}`);
await uploadBytes(imgRef, imageFile);
const imageUrl = await getDownloadURL(imgRef);
// Save data to Firestore
await addDoc(portfolioCollection, {
title,
description,
url,
image: imageUrl,
});
} catch (error) {
console.error('Error adding portfolio item:', error);
throw error;
}
};
—
### π₯ Step 3: Fetch Portfolio Items
export const fetchPortfolioItems = async () => {
try {
const snapshot = await getDocs(portfolioCollection);
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
} catch (error) {
console.error('Error fetching portfolio items:', error);
return [];
}
};
—
### πΌοΈ Step 4: Sample Vue Component
<!-- PortfolioForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="title" placeholder="Title" required />
<input v-model="description" placeholder="Description" required />
<input v-model="url" placeholder="Project URL" required />
<input type="file" @change="handleImageUpload" required />
<button type="submit">Add Portfolio Item</button>
</form>
<div v-for="item in portfolioItems" :key="item.id">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<a :href="item.url" target="_blank">Visit</a>
<img :src="item.image" alt="" width="200" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { addPortfolioItem, fetchPortfolioItems } from '@/composables/usePortfolio';
const title = ref('');
const description = ref('');
const url = ref('');
const imageFile = ref(null);
const portfolioItems = ref([]);
const handleImageUpload = (e) => {
imageFile.value = e.target.files[0];
};
const handleSubmit = async () => {
await addPortfolioItem({ title: title.value, description: description.value, url: url.value, imageFile: imageFile.value });
await loadItems();
title.value = description.value = url.value = '';
imageFile.value = null;
};
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems();
};
onMounted(loadItems);
</script>
—
Let me know if you'd like Firebase rules for securing read/write access or pagination support.
—
### β Updated Vue Component with Vuetify “
<!-- PortfolioForm.vue -->
<template>
<v-container>
<v-form ref="form" v-model="formValid" @submit.prevent="handleSubmit">
<v-text-field
v-model="title"
label="Title"
:rules="[v => !!v || 'Title is required']"
required
/>
<v-textarea
v-model="description"
label="Description"
:rules="[v => !!v || 'Description is required']"
required
/>
<v-text-field
v-model="url"
label="Project URL"
:rules="[v => !!v || 'URL is required']"
required
/>
<v-file-input
v-model="imageFile"
label="Upload Image"
:rules="[v => !!v || 'Image is required']"
accept="image/*"
required
/>
<v-btn type="submit" :disabled="!formValid" color="primary" class="mt-4">
Add Portfolio Item
</v-btn>
</v-form>
<v-divider class="my-6"></v-divider>
<v-row>
<v-col v-for="item in portfolioItems" :key="item.id" cols="12" md="6" lg="4">
<v-card>
<v-img :src="item.image" height="200px" />
<v-card-title>{{ item.title }}</v-card-title>
<v-card-text>
<p>{{ item.description }}</p>
<v-btn :href="item.url" target="_blank" text color="primary">Visit</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { addPortfolioItem, fetchPortfolioItems } from '@/composables/usePortfolio';
const title = ref('');
const description = ref('');
const url = ref('');
const imageFile = ref(null);
const portfolioItems = ref([]);
const form = ref(null);
const formValid = ref(false);
const handleSubmit = async () => {
if (form.value?.validate()) {
await addPortfolioItem({
title: title.value,
description: description.value,
url: url.value,
imageFile: imageFile.value,
});
await loadItems();
// Reset form
title.value = '';
description.value = '';
url.value = '';
imageFile.value = null;
form.value.resetValidation();
}
};
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems();
};
onMounted(loadItems);
</script>
—
### π§ Reminder: Supporting Firebase Code
Use the same composable (`usePortfolio.js`) I gave earlier β it works with this Vuetify form as-is. If you missed it, I can reattach the code for uploading and fetching.
—
Let me know if you want to:
– Add loading spinners/toasts
– Edit or delete items
– Protect uploads via Firebase Auth & Rules
I can help with all that too.
—
### π File: `src/composables/usePortfolio.js`
// src/composables/usePortfolio.js
import { db, storage } from '@/firebase'
import {
collection,
addDoc,
getDocs,
doc,
deleteDoc,
updateDoc,
} from 'firebase/firestore'
import {
ref as storageRef,
uploadBytes,
getDownloadURL,
deleteObject,
} from 'firebase/storage'
const portfolioCollection = collection(db, 'portfolio')
export const addPortfolioItem = async ({ title, description, url, imageFile }) => {
try {
const imgPath = `portfolio/${Date.now()}_${imageFile.name}`
const imgRef = storageRef(storage, imgPath)
await uploadBytes(imgRef, imageFile)
const imageUrl = await getDownloadURL(imgRef)
const docRef = await addDoc(portfolioCollection, {
title,
description,
url,
image: imageUrl,
imagePath: imgPath, // Store for deletion later if needed
createdAt: new Date(),
})
return docRef.id
} catch (error) {
console.error('Error adding portfolio item:', error)
throw error
}
}
export const fetchPortfolioItems = async () => {
try {
const snapshot = await getDocs(portfolioCollection)
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}))
} catch (error) {
console.error('Error fetching portfolio items:', error)
return []
}
}
export const deletePortfolioItem = async (id, imagePath) => {
try {
// Delete from Firestore
await deleteDoc(doc(db, 'portfolio', id))
// Delete image from Storage
if (imagePath) {
const imgRef = storageRef(storage, imagePath)
await deleteObject(imgRef)
}
} catch (error) {
console.error('Error deleting portfolio item:', error)
throw error
}
}
export const updatePortfolioItem = async (id, updatedData) => {
try {
const docRef = doc(db, 'portfolio', id)
await updateDoc(docRef, updatedData)
} catch (error) {
console.error('Error updating portfolio item:', error)
throw error
}
}
—
### π Available Methods
| Method | Description |
|——–|————-|
| `addPortfolioItem({ title, description, url, imageFile })` | Adds a new portfolio item and uploads image |
| `fetchPortfolioItems()` | Fetches all portfolio entries |
| `deletePortfolioItem(id, imagePath)` | Deletes a portfolio item and its image from storage |
| `updatePortfolioItem(id, updatedData)` | Updates title, description, etc. (image update not included here) |
—
Let me know if you want to add:
– Image update handling
– Timestamp-based ordering
– Pagination
– Filtering by tag or category
Happy to help!
Module '"@/firebase"' has no exported member 'storage'
—
### β Step 1: Fix Your `firebase.js`
// src/firebase.js
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
import { getStorage } from 'firebase/storage'
const firebaseConfig = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_PROJECT_ID.firebaseapp.com',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_PROJECT_ID.appspot.com',
messagingSenderId: 'YOUR_SENDER_ID',
appId: 'YOUR_APP_ID',
}
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
const storage = getStorage(app)
export { db, storage }
β This will allow you to use:
import { db, storage } from '@/firebase'
> π Make sure `firebase.js` is inside the `src/` directory so that `@/firebase` works correctly with your alias (check `vite.config.js` or `vue.config.js` if unsure).
—
### β Step 2: Restart Dev Server
After updating exports, restart the dev server to clear module cache:
yarn dev
# or
npm run dev
Let me know if you want to validate if the image uploads are working too, or if youβd like help enabling path aliases properly.
—
### β Example Vue Component Using the Common File
<!-- PortfolioManager.vue -->
<template>
<v-container>
<!-- Portfolio Form -->
<v-form ref="form" v-model="formValid" @submit.prevent="handleSubmit">
<v-text-field
v-model="title"
label="Title"
:rules="[v => !!v || 'Title is required']"
required
/>
<v-textarea
v-model="description"
label="Description"
:rules="[v => !!v || 'Description is required']"
required
/>
<v-text-field
v-model="url"
label="Project URL"
:rules="[v => !!v || 'URL is required']"
required
/>
<v-file-input
v-model="imageFile"
label="Upload Image"
:rules="[v => !!v || 'Image is required']"
accept="image/*"
required
/>
<v-btn type="submit" color="primary" class="mt-4" :disabled="!formValid || loading">
<v-progress-circular v-if="loading" indeterminate size="20" color="white" class="mr-2" />
Submit
</v-btn>
</v-form>
<!-- Portfolio List -->
<v-divider class="my-6" />
<v-row>
<v-col v-for="item in portfolioItems" :key="item.id" cols="12" md="6" lg="4">
<v-card>
<v-img :src="item.image" height="200px" />
<v-card-title>{{ item.title }}</v-card-title>
<v-card-text>
<p>{{ item.description }}</p>
<v-btn :href="item.url" target="_blank" text color="primary">Visit</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {
addPortfolioItem,
fetchPortfolioItems
} from '@/composables/usePortfolio'
const title = ref('')
const description = ref('')
const url = ref('')
const imageFile = ref(null)
const form = ref(null)
const formValid = ref(false)
const loading = ref(false)
const portfolioItems = ref([])
const handleSubmit = async () => {
if (form.value?.validate()) {
loading.value = true
try {
await addPortfolioItem({
title: title.value,
description: description.value,
url: url.value,
imageFile: imageFile.value,
})
await loadItems()
// Reset form
title.value = ''
description.value = ''
url.value = ''
imageFile.value = null
form.value.resetValidation()
} catch (err) {
console.error('Error submitting portfolio:', err)
} finally {
loading.value = false
}
}
}
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems()
}
onMounted(loadItems)
</script>
—
### π§© File Reference Recap
Make sure:
– `usePortfolio.js` is in `src/composables`
– `firebase.js` is in `src/`
– You're using Vuetify 3 properly set up in `main.js`
—
### π¦ Extra Enhancements (Optional)
You can later add:
– `v-alert` for success/error messages
– Edit/delete options
– Lazy loading with `v-skeleton-loader`
Want help with those? Let me know!
For a quick and Vuetify-friendly option, letβs go with **Vue 3 + Quill editor** using `vue3-quill`.
—
### β Step 1: Install Quill Editor
yarn add vue3-quill
# or
npm install vue3-quill
—
### β Step 2: Setup in Vue Component
Update the component to use `QuillEditor` in place of “:
<script setup>
import { ref, onMounted } from 'vue'
import { QuillEditor } from 'vue3-quill'
import 'quill/dist/quill.snow.css'
import {
addPortfolioItem,
fetchPortfolioItems,
} from '@/composables/usePortfolio'
const title = ref('')
const description = ref('') // will contain HTML
const url = ref('')
const imageFile = ref(null)
const form = ref(null)
const formValid = ref(false)
const loading = ref(false)
const portfolioItems = ref([])
const handleSubmit = async () => {
if (form.value?.validate()) {
loading.value = true
try {
await addPortfolioItem({
title: title.value,
description: description.value, // already HTML
url: url.value,
imageFile: imageFile.value,
})
await loadItems()
title.value = ''
description.value = ''
url.value = ''
imageFile.value = null
form.value.resetValidation()
} catch (err) {
console.error('Error submitting portfolio:', err)
} finally {
loading.value = false
}
}
}
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems()
}
onMounted(loadItems)
</script>
<template>
<v-container>
<v-form ref="form" v-model="formValid" @submit.prevent="handleSubmit">
<v-text-field
v-model="title"
label="Title"
:rules="[v => !!v || 'Title is required']"
required
/>
<!-- Rich Text Editor -->
<label class="text-subtitle-1 font-weight-medium mb-2 d-block">Description</label>
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 150px; margin-bottom: 16px;"
/>
<v-text-field
v-model="url"
label="Project URL"
:rules="[v => !!v || 'URL is required']"
required
/>
<v-file-input
v-model="imageFile"
label="Upload Image"
:rules="[v => !!v || 'Image is required']"
accept="image/*"
required
/>
<v-btn type="submit" color="primary" class="mt-4" :disabled="!formValid || loading">
<v-progress-circular v-if="loading" indeterminate size="20" color="white" class="mr-2" />
Submit
</v-btn>
</v-form>
<v-divider class="my-6" />
<v-row>
<v-col v-for="item in portfolioItems" :key="item.id" cols="12" md="6" lg="4">
<v-card>
<v-img :src="item.image" height="200px" />
<v-card-title>{{ item.title }}</v-card-title>
<v-card-text>
<!-- Render HTML content safely -->
<div v-html="item.description" />
<v-btn :href="item.url" target="_blank" text color="primary">Visit</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
—
### β
Result:
– You'll have a rich text editor (like Medium/Notion style)
– Form submission still works
– Rendered content will preserve formatting, links, bold, etc.
—
Let me know if you want:
– Custom toolbar options
– Validation for the editor
– Switch to Tiptap (more advanced)
Happy to help!
—
### β Step 1: Install the correct package
Make sure youβve installed `vue3-quill` and `quill`:
yarn add vue3-quill quill
# or
npm install vue3-quill quill
—
### β Step 2: Register QuillEditor globally (Optional but recommended)
If you're using it across multiple components, register it globally in `main.js` or `main.ts`:
// main.js or main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { QuillEditor } from 'vue3-quill'
import 'quill/dist/quill.snow.css'
const app = createApp(App)
app.component('QuillEditor', QuillEditor)
app.mount('#app')
Or if you prefer **local registration**, skip this and register it directly in your component.
—
### β Step 3: Use it in Your Component
<script setup>
import { ref, onMounted } from 'vue'
import { QuillEditor } from 'vue3-quill'
import 'quill/dist/quill.snow.css'
import {
addPortfolioItem,
fetchPortfolioItems,
} from '@/composables/usePortfolio'
const title = ref('')
const description = ref('') // HTML content
const url = ref('')
const imageFile = ref(null)
const form = ref(null)
const formValid = ref(false)
const loading = ref(false)
const portfolioItems = ref([])
const handleSubmit = async () => {
if (form.value?.validate()) {
loading.value = true
try {
await addPortfolioItem({
title: title.value,
description: description.value,
url: url.value,
imageFile: imageFile.value,
})
await loadItems()
title.value = ''
description.value = ''
url.value = ''
imageFile.value = null
form.value.resetValidation()
} catch (err) {
console.error('Submit error:', err)
} finally {
loading.value = false
}
}
}
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems()
}
onMounted(loadItems)
</script>
<template>
<v-container>
<v-form ref="form" v-model="formValid" @submit.prevent="handleSubmit">
<v-text-field v-model="title" label="Title" :rules="[v => !!v || 'Required']" required />
<!-- Rich Text Description -->
<label class="text-subtitle-1 d-block mb-1">Description</label>
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 150px; background: white; margin-bottom: 16px;"
/>
<v-text-field v-model="url" label="Project URL" :rules="[v => !!v || 'Required']" required />
<v-file-input v-model="imageFile" label="Upload Image" :rules="[v => !!v || 'Required']" accept="image/*" required />
<v-btn type="submit" color="primary" :disabled="!formValid || loading">
<v-progress-circular v-if="loading" indeterminate size="20" color="white" class="mr-2" />
Submit
</v-btn>
</v-form>
<v-divider class="my-6" />
<v-row>
<v-col v-for="item in portfolioItems" :key="item.id" cols="12" md="6" lg="4">
<v-card>
<v-img :src="item.image" height="200px" />
<v-card-title>{{ item.title }}</v-card-title>
<v-card-text>
<div v-html="item.description" />
<v-btn :href="item.url" target="_blank" text color="primary">Visit</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
—
### π Still not showing?
Double check:
1. You have `import 'quill/dist/quill.snow.css'`
2. Youβre applying a light background color (`background: white`) so Quillβs toolbar and text area are visible
3. No console errors about `Quill` or `QuillEditor`
—
If it's still not working, share your console logs or project structure and Iβll troubleshoot it directly with you. Want to try Tiptap instead?
—
### β Fix: Ensure Height and Container Visibility
Update your “ tag like this:
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 200px; background: white; border: 1px solid #ccc; padding: 10px;"
/>
### π Key Things to Check:
| β
| What to Check |
|—-|—————|
| π§± `min-height` | Ensures editor is visible |
| π¨ `background` | Vuetify cards/containers may have dark background β set white |
| π¦ `padding` | Helps make the text area usable |
| π Parent layout | Ensure itβs not in a `v-row` or `v-col` with 0 height |
| πͺ Inspector | Check styles in browser DevTools to see if height is 0 due to flex/collapse |
—
### π§ͺ Bonus: Wrap It in a Container
If you're still stuck, try wrapping it like this to debug:
<div style="border: 1px solid red; padding: 10px; min-height: 200px;">
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 180px; background: white;"
/>
</div>
You should see a red box and the editor inside it.
—
### β Confirm Quill CSS is Loaded
Make sure this line is present in your “ or global `main.js`:
import 'quill/dist/quill.snow.css'
> If not loaded, Quill wonβt render its toolbar or styles.
—
### Still Not Showing?
Drop your Vuetify layout structure here (especially around the form), and Iβll help you patch it up. Sometimes itβs about how Vuetify handles grid layout and flex height.
– π Toolbar is visible but not functional
– π You canβt type into the editor
– π The content area seems broken or read-only
β¦itβs usually one of the following common Vue 3 + Quill integration issues:
—
### β
Fix 1: Check `v-model` Binding
Ensure you're using **`v-model:content="…"`**, not `v-model` directly:
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 180px; background: white;"
/>
> β `v-model="description"` β **will NOT work**
> β
`v-model:content="description"` β **works with `vue3-quill`**
—
### β Fix 2: Wrap in Visible Container (again, just to be sure)
Sometimes Vuetify layout overrides Quill's default styles.
<v-card class="pa-4">
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 200px; background: white; border: 1px solid #ccc;"
/>
</v-card>
—
### β Fix 3: Use `modules` Prop (Optional toolbar setup)
Quill might fail to initialize if toolbar config is missing or stripped. Try explicitly passing modules:
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
:modules="modules"
style="min-height: 200px; background: white; border: 1px solid #ccc;"
/>
const modules = {
toolbar: [
['bold', 'italic', 'underline'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image'],
['clean'],
],
}
—
### β Fix 4: DevTools Check
Make sure thereβs no error like:
> `quill Cannot read properties of null (reading 'getBoundingClientRect')`
That usually means Quill was mounted on a hidden element or layout caused collapse.
Try forcing it visible:
<div style="display: block; visibility: visible;">
<QuillEditor ... />
</div>
—
### π Final Sanity Check Summary
| π Check | β
Should Be |
|——–|—————|
| `v-model` | `v-model:content="description"` |
| CSS | `min-height`, `background`, `border` |
| Toolbar | Use `modules` prop |
| Styles | `'quill/dist/quill.snow.css'` must be imported |
| Container | Must not collapse or be 0px tall |
—
Want me to make you a working minimal GitHub/Vite repo with Vuetify + vue3-quill? Or prefer switching to Tiptap which integrates more smoothly in Vue 3?
—
### β Update in `usePortfolio.js`
export const addPortfolioItem = async ({ title, description, url, imageFile }) => {
try {
const imgPath = `portfolio/${Date.now()}_${imageFile.name}`
const imgRef = storageRef(storage, imgPath)
// Upload image to Firebase Storage
await uploadBytes(imgRef, imageFile)
const imageUrl = await getDownloadURL(imgRef)
// Add document to Firestore
const docRef = await addDoc(portfolioCollection, {
title,
description,
url,
image: imageUrl,
imagePath: imgPath,
createdAt: new Date(),
})
// β
Log the new document ID to the console
console.log('New Portfolio Item ID:', docRef.id)
return docRef.id
} catch (error) {
console.error('Error adding portfolio item:', error)
throw error
}
}
—
### β
Then in your component (optional)
You can also log it when calling `addPortfolioItem`:
const handleSubmit = async () => {
if (form.value?.validate()) {
loading.value = true
try {
const docId = await addPortfolioItem({
title: title.value,
description: description.value,
url: url.value,
imageFile: imageFile.value,
})
console.log('Portfolio item successfully created with ID:', docId)
await loadItems()
// Clear form fields...
} catch (err) {
console.error('Submit error:', err)
} finally {
loading.value = false
}
}
}
—
Let me know if youβd like to also **display this ID in the UI** (like a toast/snackbar or confirmation message).
doTheRequest @ firebase_storage.js?v=12b17e52:452
(anonymous) @ firebase_storage.js?v=12b17e52:276
setTimeout
callWithDelay @ firebase_storage.js?v=12b17e52:274
responseHandler @ firebase_storage.js?v=12b17e52:310
(anonymous) @ firebase_storage.js?v=12b17e52:461
Promise.then
doTheRequest @ firebase_storage.js?v=12b17e52:452
(anonymous) @ firebase_storage.js?v=12b17e52:276
setTimeout
callWithDelay @ firebase_storage.js?v=12b17e52:274
responseHandler @ firebase_storage.js?v=12b17e52:310
(anonymous) @ firebase_storage.js?v=12b17e52:461
Promise.then
doTheRequest @ firebase_storage.js?v=12b17e52:452
(anonymous) @ firebase_storage.js?v=12b17e52:276
setTimeout
callWithDelay @ firebase_storage.js?v=12b17e52:274
responseHandler @ firebase_storage.js?v=12b17e52:310
(anonymous) @ firebase_storage.js?v=12b17e52:461
Promise.then
doTheRequest @ firebase_storage.js?v=12b17e52:452
(anonymous) @ firebase_storage.js?v=12b17e52:276
setTimeout
callWithDelay @ firebase_storage.js?v=12b17e52:274
start @ firebase_storage.js?v=12b17e52:334
start_ @ firebase_storage.js?v=12b17e52:506
(anonymous) @ firebase_storage.js?v=12b17e52:428
NetworkRequest @ firebase_storage.js?v=12b17e52:425
makeRequest @ firebase_storage.js?v=12b17e52:558
_makeRequest @ firebase_storage.js?v=12b17e52:2403
makeRequestWithTokens @ firebase_storage.js?v=12b17e52:2416
await in makeRequestWithTokens
uploadBytes$1 @ firebase_storage.js?v=12b17e52:2139
uploadBytes @ firebase_storage.js?v=12b17e52:2428
addPortfolioItem @ myPortfolio.ts:25
submit @ AddPortfolio.vue:46
await in submit
cache..cache. @ chunk-U3LI7FBV.js?v=12b17e52:12160
callWithErrorHandling @ chunk-U3LI7FBV.js?v=12b17e52:2263
callWithAsyncErrorHandling @ chunk-U3LI7FBV.js?v=12b17e52:2270
emit @ chunk-U3LI7FBV.js?v=12b17e52:8466
(anonymous) @ chunk-U3LI7FBV.js?v=12b17e52:10175
onSubmit @ vuetify_components.js?v=12b17e52:17318
callWithErrorHandling @ chunk-U3LI7FBV.js?v=12b17e52:2263
callWithAsyncErrorHandling @ chunk-U3LI7FBV.js?v=12b17e52:2270
invoker @ chunk-U3LI7FBV.js?v=12b17e52:11202Understand this errorAI
add:1 Access to XMLHttpRequest at 'https://firebasestorage.googleapis.com/v0/b/openeratech.firebasestorage.app/o?name=portfolio%2F1742542411774_tab-1.png' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
—
### 𧨠Error Summary:
Access to XMLHttpRequest at 'https://firebasestorage.googleapis.com/...'
from origin 'http://localhost:5173' has been blocked by CORS policy.
This happens when:
– Firebase Storage hasnβt been configured to allow your local domain
– The `OPTIONS` preflight request fails due to incorrect CORS headers
—
### β Solution: Update Firebase Storage CORS Configuration
You need to allow your dev origin (`http://localhost:5173`) using the Firebase CLI:
—
#### β Step 1: Install Firebase CLI (if you havenβt)
npm install -g firebase-tools
—
#### β Step 2: Login and initialize (if not done)
firebase login
firebase init
—
#### β Step 3: Create a CORS config file (`cors.json`)
Create a file named `cors.json`:
[
{
"origin": ["http://localhost:5173"],
"method": ["GET", "POST", "PUT", "DELETE"],
"maxAgeSeconds": 3600,
"responseHeader": ["Content-Type", "Authorization", "x-goog-meta-custom"]
}
]
> You can add other origins like `https://your-production-domain.com` when deploying.
—
#### β Step 4: Deploy CORS config to Firebase Storage
Run this command:
firebase storage:rules:set ./cors.json
If that doesn't work, try:
gsutil cors set cors.json gs://your-project-id.appspot.com
> Make sure you replace `your-project-id` with your actual Firebase project ID.
If `gsutil` is not available:
– Install it via [Google Cloud SDK](https://cloud.google.com/sdk/docs/install)
—
### π§ͺ Bonus: Temporarily use Firebase Hosting or `vite –host`
You can also temporarily test by hosting your Vue app with Firebase Hosting or using:
vite --host 0.0.0.0
β¦but the correct fix is the CORS config above.
—
Let me know if you want me to generate your exact `cors.json` with multiple environments or walk you through setting Firebase CLI up for your project.
—
### β Steps to Add `localhost` in Firebase Console:
1. **Go to Firebase Console**
π [https://console.firebase.google.com](https://console.firebase.google.com)
2. **Select your project**
3. In the left sidebar, click on **βοΈ Project Settings**
4. Scroll down to the **Authorized domains** section
5. Click the **"Add domain"** button
6. Enter:
localhost:5173
7. Click **"Add"**
β Done! Your local Vite dev server (`localhost:5173`) is now whitelisted and allowed to interact with Firebase services like **Auth**, **Storage**, and **Firestore**.
—
> π‘ If you also use `localhost:3000` (React), or `127.0.0.1`, you can add those too.
Let me know if you're also using Firebase Auth β we can configure the sign-in methods for localhost too.
Type '{ id: string; }' is not assignable to type 'never'.
const portfolioItems = ref([])
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems()
}
onMounted(loadItems)
—
### 𧨠Error Explained
Type '{ id: string; }[]' is not assignable to type 'never[]'
This happens because:
const portfolioItems = ref([]) // β no type info, defaults to never[]
Later you're trying to assign an array of `{ id: string; … }` to it, and TypeScript complains.
—
### β Solution: Explicitly Type the Ref
Update the `ref` like this (assuming your item has more than just `id`):
interface PortfolioItem {
id: string
title: string
description: string
url: string
image: string
}
const portfolioItems = ref<PortfolioItem[]>([])
Now `portfolioItems.value` is typed as an array of `PortfolioItem`, and your assignment will work perfectly:
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems()
}
—
### β Optionally: Use `ReturnType`
If you want to avoid defining the interface manually and you're using a typed backend or `fetchPortfolioItems()` is typed, you can also do:
const portfolioItems = ref<Awaited<ReturnType<typeof fetchPortfolioItems>>>([])
—
Let me know if you want help generating a full `PortfolioItem` interface from your Firestore document structure!
FirebaseError: Firebase Storage: User does not have permission to access 'portfolio/....' (storage/unauthorized)
—
### β Common Cause
Your Firebase Storage **security rules** are too restrictive and not allowing **unauthenticated** or **authenticated** users to upload files.
—
### π Step 1: Check and Update Firebase Storage Rules
Go to the Firebase Console:
1. Open your project β **Storage**
2. Click on the **"Rules"** tab
3. Youβll see something like:
// β Too restrictive (default)
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if false;
}
}
}
—
### β Development-Friendly Rules (for testing only)
Replace with this for development:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /portfolio/{allPaths=**} {
allow read, write: if true; // allow everyone for now
}
}
}
> π₯ WARNING: This makes the `/portfolio` folder public β good for **development**, not production.
—
### π‘οΈ Recommended Rules for Authenticated Users
If you're using Firebase Authentication, and want to allow only signed-in users to upload:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /portfolio/{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
Then make sure you're logged in when making the upload request using Firebase Auth in your Vue app.
—
### β Step 2: Save & Publish
After updating the rules, click **"Publish"** in Firebase Console.
—
### β Step 3: Try Upload Again
Once rules are updated:
– Restart your app (or at least refresh)
– Retry uploading the portfolio item
—
Let me know if:
– You're using Firebase Auth and want users to upload only their own files
– You want a secure, production-grade rule config
Iβll help you lock it down safely.
—
### β Step 1: Confirm Youβre Saving the Image URL
When uploading the image in your `addPortfolioItem()` method, make sure you're saving the **download URL** to Firestore:
const imageUrl = await getDownloadURL(imgRef)
const docRef = await addDoc(portfolioCollection, {
title,
description,
url,
image: imageUrl, // β
save this
imagePath: imgPath, // optional - for deleting later
createdAt: new Date(),
})
—
### β Step 2: Fetch and Display It
When you call `fetchPortfolioItems()`, you're getting the `image` URL like this:
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(), // includes `image`
}))
Then in your Vue template:
<v-img :src="item.image" height="200px" />
or even plain HTML:
<img :src="item.image" alt="Portfolio Image" width="300" />
—
### β Step 3: Preview Example
<v-card v-for="item in portfolioItems" :key="item.id">
<v-img :src="item.image" height="200px" />
<v-card-title>{{ item.title }}</v-card-title>
<v-card-text>
<div v-html="item.description" />
<v-btn :href="item.url" target="_blank" text color="primary">Visit</v-btn>
</v-card-text>
</v-card>
—
### π Bonus: Image URL Expiry?
Firebase image URLs **do not expire** by default unless you revoke tokens or use Firebase rules to block public reads.
You can:
– Keep it public (for most portfolio sites)
– Or add secure access via Firebase Auth
—
Let me know if you'd like:
– Lazy loading
– Image placeholder
– Full-size preview modal on click
Happy to help with that too!
—
## ποΈ Firestore Structure Proposal
### πΉ `categories` collection:
{
id: string,
name: string
}
### πΉ `portfolio` collection (updated):
{
title: string,
description: string,
url: string,
image: string,
categoryId: string, // reference to category
createdAt: Timestamp
}
—
## β Step 1: Add Category CRUD to `usePortfolio.js`
// src/composables/usePortfolio.js
import {
collection,
getDocs,
addDoc,
} from 'firebase/firestore'
import { db, storage } from '@/firebase'
const categoryCollection = collection(db, 'categories')
// π Add new category
export const addCategory = async (name) => {
const docRef = await addDoc(categoryCollection, { name })
console.log('New category ID:', docRef.id)
return docRef.id
}
// π§ Fetch all categories
export const fetchCategories = async () => {
const snapshot = await getDocs(categoryCollection)
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
}
—
## β Step 2: Update `addPortfolioItem` to accept a `categoryId`
export const addPortfolioItem = async ({ title, description, url, imageFile, categoryId }) => {
const imgPath = `portfolio/${Date.now()}_${imageFile.name}`
const imgRef = storageRef(storage, imgPath)
await uploadBytes(imgRef, imageFile)
const imageUrl = await getDownloadURL(imgRef)
const docRef = await addDoc(collection(db, 'portfolio'), {
title,
description,
url,
image: imageUrl,
imagePath: imgPath,
categoryId, // β
save selected category
createdAt: new Date(),
})
console.log('New Portfolio Item ID:', docRef.id)
return docRef.id
}
—
## β Step 3: Update Vue Form with Category Select
### π§ Inside your “
import { fetchCategories, addPortfolioItem } from '@/composables/usePortfolio'
const categories = ref([])
const selectedCategory = ref('')
// Fetch categories on mount
onMounted(async () => {
categories.value = await fetchCategories()
await loadItems()
})
—
### π§© Inside “
Add this to your form:
<v-select
v-model="selectedCategory"
:items="categories"
item-title="name"
item-value="id"
label="Select Category"
:rules="[v => !!v || 'Category is required']"
required
/>
Then pass it in your submit handler:
await addPortfolioItem({
title: title.value,
description: description.value,
url: url.value,
imageFile: imageFile.value,
categoryId: selectedCategory.value,
})
—
## β Step 4: Show Category with Portfolio Item
Assuming youβve fetched the categories already, you can map category names using a helper:
const getCategoryName = (id) => {
return categories.value.find(cat => cat.id === id)?.name || 'Uncategorized'
}
Then in your card:
<v-card-subtitle>Category: {{ getCategoryName(item.categoryId) }}</v-card-subtitle>
—
## β Optional: Category Management UI
If you'd like to build a simple form to **add new categories** (similar to portfolio), I can include that too.
Let me know if:
– You want multi-category support (array)
– You want category filtering for portfolio
– You want to use Firestore `documentReference` instead of just ID
Happy to extend this!
– Rich Text Editor for description (via `vue3-quill`)
– Image upload to Firebase Storage
– Category selection from Firestore
– Form validation
– Reusable composables from `usePortfolio.js`
—
## β Full `AddPortfolio.vue`
<template>
<v-container>
<v-card class="pa-6" elevation="2">
<v-card-title>Add Portfolio Item</v-card-title>
<v-form ref="form" v-model="formValid" @submit.prevent="handleSubmit">
<v-text-field
v-model="title"
label="Title"
:rules="[v => !!v || 'Title is required']"
required
/>
<label class="text-subtitle-1 d-block mb-1">Description</label>
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 200px; background: white; border: 1px solid #ccc;"
/>
<v-text-field
v-model="url"
label="Project URL"
:rules="[v => !!v || 'URL is required']"
required
/>
<v-select
v-model="selectedCategory"
:items="categories"
item-title="name"
item-value="id"
label="Select Category"
:rules="[v => !!v || 'Category is required']"
required
/>
<v-file-input
v-model="imageFile"
label="Upload Image"
accept="image/*"
:rules="[v => !!v || 'Image is required']"
required
/>
<v-btn
type="submit"
color="primary"
class="mt-4"
:disabled="!formValid || loading"
>
<v-progress-circular
v-if="loading"
indeterminate
size="20"
color="white"
class="mr-2"
/>
Submit
</v-btn>
</v-form>
</v-card>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { QuillEditor } from 'vue3-quill'
import 'quill/dist/quill.snow.css'
import {
addPortfolioItem,
fetchCategories,
} from '@/composables/usePortfolio'
const title = ref('')
const description = ref('')
const url = ref('')
const imageFile = ref(null)
const selectedCategory = ref('')
const categories = ref([])
const form = ref(null)
const formValid = ref(false)
const loading = ref(false)
onMounted(async () => {
categories.value = await fetchCategories()
})
const handleSubmit = async () => {
if (form.value?.validate()) {
loading.value = true
try {
const docId = await addPortfolioItem({
title: title.value,
description: description.value,
url: url.value,
imageFile: imageFile.value,
categoryId: selectedCategory.value,
})
console.log('Portfolio item created with ID:', docId)
// Reset form
title.value = ''
description.value = ''
url.value = ''
imageFile.value = null
selectedCategory.value = ''
form.value.resetValidation()
} catch (err) {
console.error('Error adding portfolio:', err)
} finally {
loading.value = false
}
}
}
</script>
—
## π¦ Dependencies
Ensure you've installed:
yarn add vue3-quill quill
And have registered the Quill CSS:
import 'quill/dist/quill.snow.css'
—
## π§© Folder Structure Suggestion
src/
composables/
usePortfolio.js
components/
AddPortfolio.vue
firebase.js
—
Let me know if you also want:
– Portfolio listing page (`PortfolioList.vue`)
– Filter by category
– Edit/delete functionality
Happy to build those with you!
More info and automated migrator: https://sass-lang.com/d/import
β·
5 β @import './layout/sidebar';
β ^^^^^^^^^^^^^^^^^^
β΅
../../../../src/scss/style.scss 5:9 root stylesheet
—
## β οΈ Your Code (Deprecated)
@import './layout/sidebar';
—
## β Modern Replacement (Recommended)
Update it to:
@use './layout/sidebar';
But this requires changes inside the `sidebar.scss` file too.
—
### π§ Step-by-Step Migration Guide
#### β 1. Use `@use` Instead of `@import`
Replace all `@import` lines in your main `style.scss` file like this:
@use './layout/sidebar';
@use './layout/header';
> β οΈ **Note:** You must reference variables, mixins, etc. with a namespace. Example:
.sidebar {
color: sidebar.$sidebar-text-color;
}
#### β 2. Export with `@forward` (in shared files)
If you're creating a central style system (like colors, mixins, etc.), use `@forward` to expose them.
Example:
// _variables.scss
$primary-color: #007bff;
// _index.scss
@forward 'variables';
// main.scss
@use './styles/index';
body {
background-color: index.$primary-color;
}
—
### β Optional: Use `sass-migrator` Tool
Sass offers an automatic tool to migrate everything:
npx sass-migrator division path/to/your/file.scss
npx sass-migrator module path/to/your/file.scss
More: [https://sass-lang.com/documentation/cli/migrator](https://sass-lang.com/documentation/cli/migrator)
—
### β Quick Fix (Suppress Warning for Now)
If you're not ready to migrate everything:
– You can temporarily **ignore the warning**
– But make sure to plan the migration before Sass 3.0.0 is released
—
Let me know if you want help converting your entire SCSS folder to the new module system β I can automate that for you.
68 form.value?.reset()
~~~~~
TS2339: Property 'reset' does not exist on type 'never'
—
### β Fix: Properly Type the Form Ref
Tell TypeScript that your `form` is a Vuetify form component. In Vuetify 3, you should type the form ref using `VForm`'s type helper from Vuetify:
—
### π§ Step-by-step fix:
#### 1. Import `VForm` type
import type { VForm } from 'vuetify/components'
#### 2. Type the `form` ref
const form = ref<VForm | null>(null)
#### 3. Call `.reset()` safely
form.value?.reset()
#### β Full example:
import { ref } from 'vue'
import type { VForm } from 'vuetify/components'
const form = ref<VForm | null>(null)
// Later...
form.value?.reset()
—
### π Bonus: For `.validate()` usage
Also make sure `.validate()` is properly typed:
const valid = await form.value?.validate()
Vuetify's validate returns `{ valid: boolean }` so to safely check:
const result = await form.value?.validate()
if (result?.valid) {
// continue
}
—
Let me know if you're using Volar or have strict type checking enabled β I can help you write proper types across the whole component.
Type '{ id: string; }[]' is not assignable to type 'PortfolioItem[]'.
Type '{ id: string; }' is missing the following properties from type 'PortfolioItem': title, description, url, image, and 2 more.
25 portfolioItems.value = await fetchPortfolioItems();
~~~~~~~~~~~~~~~~~~~~
{
id: string;
title: string;
description: string;
url: string;
image: string;
imagePath: string;
createdAt: any;
}
—
### β Solution
#### Option 1: Fix the return type in `fetchPortfolioItems()`
Make sure your `fetchPortfolioItems` function is returning all the required properties:
export const fetchPortfolioItems = async (): Promise<PortfolioItem[]> => {
const snapshot = await getDocs(portfolioCollection)
return snapshot.docs.map(doc => ({
id: doc.id,
...(doc.data() as Omit<PortfolioItem, 'id'>),
}))
}
> This tells TypeScript: "`doc.data()` has everything **except** the `id`, and weβre adding that manually."
—
#### Option 2: Type assertion in your component (less ideal, but quick fix)
If you're **sure** the data returned by `fetchPortfolioItems()` is correct, you can assert the type where you use it:
portfolioItems.value = await fetchPortfolioItems() as PortfolioItem[]
—
### β Make sure you have the `PortfolioItem` interface
If not already defined, create one like this:
export interface PortfolioItem {
id: string
title: string
description: string
url: string
image: string
imagePath: string
createdAt: any
}
> Place it in a shared file like `@/types/portfolio.ts` and import where needed.
—
Let me know if you'd like me to convert all your Firebase return types to strict interfaces with full typings β happy to help you clean that up for better DX.
return categories.value.find((cat: PortfolioCategory) => cat.id === id)?.name || 'Uncategorized'
}Property 'name' does not exist on type 'never'
Property 'name' does not exist on type 'never'
because TypeScript doesn't know the type of `categories.value`, so when you call `.find(…)`, it assumes you're looping over `never[]`.
—
### β Solution: Explicitly type `categories`
You need to define the structure of a `PortfolioCategory`, then tell TypeScript what kind of array `categories` is.
—
### π Step-by-step fix
#### 1. Create a type or interface
interface PortfolioCategory {
id: string
name: string
}
You can place this in a types file (`types/portfolio.ts`) if you plan to reuse it.
—
#### 2. Type the `categories` ref
const categories = ref<PortfolioCategory[]>([])
—
#### 3. Your function is now valid
const getCategoryName = (id: string) => {
return categories.value.find((cat: PortfolioCategory) => cat.id === id)?.name || 'Uncategorized'
}
Actually, since `categories` is already typed, you donβt even need to re-type `cat`:
const getCategoryName = (id: string) => {
return categories.value.find(cat => cat.id === id)?.name || 'Uncategorized'
}
—
### β Final Result (minimal example)
interface PortfolioCategory {
id: string
name: string
}
const categories = ref<PortfolioCategory[]>([])
const getCategoryName = (id: string) => {
return categories.value.find(cat => cat.id === id)?.name || 'Uncategorized'
}
Let me know if you want to support **multi-category** tagging or relationships too!
Youβll get:
1. β
`deletePortfolioItem(id, imagePath)`
2. β
`updatePortfolioItem(id, updatedData)`
3. π§± Usage in Vue component: delete button and edit logic
—
## β 1. Add to `usePortfolio.js`
import { doc, deleteDoc, updateDoc } from 'firebase/firestore'
import { ref as storageRef, deleteObject } from 'firebase/storage'
import { db, storage } from '@/firebase'
// Delete portfolio item from Firestore + image from Storage
export const deletePortfolioItem = async (id, imagePath) => {
try {
// Delete document
await deleteDoc(doc(db, 'portfolio', id))
// Delete image from storage if exists
if (imagePath) {
const imgRef = storageRef(storage, imagePath)
await deleteObject(imgRef)
}
console.log(`Deleted item ${id}`)
} catch (err) {
console.error('Error deleting portfolio item:', err)
throw err
}
}
// Update portfolio item (title, description, url, categoryId, etc.)
export const updatePortfolioItem = async (id, updatedData) => {
try {
const docRef = doc(db, 'portfolio', id)
await updateDoc(docRef, updatedData)
console.log(`Updated item ${id}`)
} catch (err) {
console.error('Error updating portfolio item:', err)
throw err
}
}
—
## β 2. Add Delete Button in Vue
In your `PortfolioList.vue` or wherever you're showing the list:
<v-btn icon color="red" @click="handleDelete(item.id, item.imagePath)">
<v-icon>mdi-delete</v-icon>
</v-btn>
Then in “:
import { deletePortfolioItem } from '@/composables/usePortfolio'
const handleDelete = async (id: string, imagePath: string) => {
if (confirm('Are you sure you want to delete this item?')) {
await deletePortfolioItem(id, imagePath)
await loadItems() // re-fetch items
}
}
—
## β 3. Update Operation in Vue
To update an item:
await updatePortfolioItem(item.id, {
title: 'Updated title',
description: 'Updated description',
url: 'https://updated-url.com',
categoryId: 'newCategoryId',
})
You can build an edit form using the same fields as `AddPortfolio.vue`, pre-filled with the current values, then call this update function on submit.
—
## π§ͺ Bonus: Confirm Deletion with Vuetify Dialog
If you'd like a more elegant UX than `confirm()`, I can help you use “ to show a proper modal.
—
Let me know if you want:
– Image replacement on update
– Separate EditPortfolio.vue page
– In-place editing with modals or drawers
Ready when you are!
—
## β
Features:
– Fetch and pre-fill portfolio data in the form
– Edit and update fields
– Optional: replace the image
– Submit to Firebase Firestore
—
## π§© Setup: `EditPortfolio.vue` (example component)
<template>
<v-container>
<v-card class="pa-6" elevation="2">
<v-card-title>Edit Portfolio Item</v-card-title>
<v-form ref="form" v-model="formValid" @submit.prevent="handleUpdate">
<v-text-field
v-model="title"
label="Title"
:rules="[v => !!v || 'Title is required']"
required
/>
<label class="text-subtitle-1 d-block mb-1">Description</label>
<QuillEditor
v-model:content="description"
content-type="html"
theme="snow"
style="min-height: 200px; background: white; border: 1px solid #ccc;"
/>
<v-text-field
v-model="url"
label="Project URL"
:rules="[v => !!v || 'URL is required']"
required
/>
<v-select
v-model="selectedCategory"
:items="categories"
item-title="name"
item-value="id"
label="Select Category"
:rules="[v => !!v || 'Category is required']"
required
/>
<v-file-input
v-model="newImageFile"
label="Replace Image (optional)"
accept="image/*"
/>
<v-btn type="submit" color="primary" :disabled="!formValid || loading" class="mt-4">
<v-progress-circular
v-if="loading"
indeterminate
size="20"
color="white"
class="mr-2"
/>
Update
</v-btn>
</v-form>
</v-card>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { QuillEditor } from 'vue3-quill'
import 'quill/dist/quill.snow.css'
import {
updatePortfolioItem,
fetchCategories,
fetchPortfolioItems,
deletePortfolioItem,
addPortfolioItem,
} from '@/composables/usePortfolio'
import { getDoc, doc } from 'firebase/firestore'
import { db, storage } from '@/firebase'
import { getDownloadURL, ref as storageRef, uploadBytes, deleteObject } from 'firebase/storage'
const route = useRoute()
const router = useRouter()
const id = route.params.id as string
const form = ref(null)
const formValid = ref(false)
const loading = ref(false)
const title = ref('')
const description = ref('')
const url = ref('')
const imagePath = ref('')
const selectedCategory = ref('')
const newImageFile = ref(null)
const categories = ref([])
// π§ Load existing portfolio data
const loadPortfolio = async () => {
const docRef = doc(db, 'portfolio', id)
const snapshot = await getDoc(docRef)
const data = snapshot.data()
if (data) {
title.value = data.title
description.value = data.description
url.value = data.url
imagePath.value = data.imagePath
selectedCategory.value = data.categoryId
}
}
// π Load categories
const loadCategories = async () => {
categories.value = await fetchCategories()
}
// β
Submit handler
const handleUpdate = async () => {
if (!form.value?.validate()) return
loading.value = true
try {
let updatedImageUrl = null
// πΌοΈ If replacing the image
if (newImageFile.value) {
// Delete old image
const oldRef = storageRef(storage, imagePath.value)
await deleteObject(oldRef)
// Upload new image
const newPath = `portfolio/${Date.now()}_${newImageFile.value.name}`
const newRef = storageRef(storage, newPath)
await uploadBytes(newRef, newImageFile.value)
updatedImageUrl = await getDownloadURL(newRef)
imagePath.value = newPath
}
// π Update document
await updatePortfolioItem(id, {
title: title.value,
description: description.value,
url: url.value,
categoryId: selectedCategory.value,
...(updatedImageUrl && { image: updatedImageUrl, imagePath: imagePath.value }),
})
console.log('Portfolio updated:', id)
router.push('/portfolio') // or wherever you want
} catch (err) {
console.error('Error updating portfolio:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
loadPortfolio()
loadCategories()
})
</script>
—
## β Route Example for Edit Page
If you're using Vue Router:
{
path: '/portfolio/edit/:id',
name: 'EditPortfolio',
component: () => import('@/views/yourpath/EditPortfolio.vue'),
}
—
## β Recap
You now have:
– Pre-filled editable form
– Optional image replacement
– Firestore + Storage update
– Smooth update + redirect
—
Let me know if you want:
– In-place modal editing
– Toast/snackbar messages
– Validation or form section toggle
Happy to build it with you!
1. β
Store `providerId` in each portfolio document
2. β
Query Firestore using a `where` condition
3. β
Update `fetchPortfolioItems(providerId)` function
—
## β Step 1: Update Portfolio Schema
Make sure when you add a portfolio item, you're saving the `providerId`:
await addDoc(portfolioCollection, {
title,
description,
url,
image: imageUrl,
imagePath: imgPath,
categoryId,
providerId, // β
Add this field
createdAt: new Date(),
})
> This could be the Firebase Auth user ID, or any user/provider ID you're managing in your app.
—
## β Step 2: Update `fetchPortfolioItems(providerId)` in `usePortfolio.js`
import { query, where, getDocs, collection } from 'firebase/firestore'
import { db } from '@/firebase'
const portfolioCollection = collection(db, 'portfolio')
// π Fetch portfolio items by providerId
export const fetchPortfolioByProvider = async (providerId) => {
const q = query(portfolioCollection, where('providerId', '==', providerId))
const snapshot = await getDocs(q)
return snapshot.docs.map(doc => ({
id: doc.id,
...(doc.data())
}))
}
—
## β Step 3: Use it in Vue
In your Vue component:
import { fetchPortfolioByProvider } from '@/composables/usePortfolio'
const portfolioItems = ref([])
onMounted(async () => {
const providerId = 'abc123' // Replace with actual logic (maybe from Auth)
portfolioItems.value = await fetchPortfolioByProvider(providerId)
})
—
## β Bonus: Dynamic from Firebase Auth
If you're using Firebase Authentication:
import { getAuth } from 'firebase/auth'
const auth = getAuth()
const providerId = auth.currentUser?.uid
Pass that into `fetchPortfolioByProvider()`.
—
Let me know if you want to:
– Show all providersβ portfolios grouped
– Filter by category and provider
– Make this reactive to logged-in user
I can hook that up for you too!
try {
const snapshot = await getDocs(portfolioCollection)
return snapshot.docs.map(doc => ({
id: doc.id,
…doc.data(),
}))
console.log('Fetched items:', snapshot);
} catch (error) {
console.error('Error fetching portfolio items:', error)
return []
}
}
but if provided null then get all portfolio items
– β
Return **all portfolio items** if `provider_id` is `null` or not provided
– β
Return **filtered items** if `provider_id` is passed
—
### β Updated `fetchPortfolioItems` Function
import { getDocs, query, where, collection } from 'firebase/firestore'
import { db } from '@/firebase'
const portfolioCollection = collection(db, 'portfolio')
export const fetchPortfolioItems = async (provider_id?: string) => {
try {
const q = provider_id
? query(portfolioCollection, where('providerId', '==', provider_id))
: portfolioCollection
const snapshot = await getDocs(q)
console.log('Fetched items:', snapshot.size)
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}))
} catch (error) {
console.error('Error fetching portfolio items:', error)
return []
}
}
—
### π§ͺ Usage
// Fetch all
await fetchPortfolioItems()
// Fetch only for a specific provider
await fetchPortfolioItems('provider_abc123')
—
Let me know if you want to:
– Add pagination
– Combine this with category filtering
– Automatically get `provider_id` from Firebase Auth
Happy to wire that in too.
const loadItems = async () => {
portfolioItems.value = await fetchPortfolioItems(authStore.user?.provider_id) as PortfolioItem[];
console.log(portfolioItems.value);
}
for particular user pass null to provider
– β
Pass `null` to `fetchPortfolioItems()` if the logged-in user is a **general user** (to get only their items).
– β
Or pass nothing (or `undefined`) to get **all items** (e.g. for admin or public view).
—
### β Solution Logic
Hereβs how to do it:
const portfolioItems = ref<PortfolioItem[]>([])
const loadItems = async () => {
const providerId = authStore.user?.role === 'provider'
? authStore.user?.provider_id
: null
portfolioItems.value = await fetchPortfolioItems(providerId) as PortfolioItem[]
console.log(portfolioItems.value)
}
> β
This ensures:
> – If the user is a **provider**, fetch only their portfolio
> – If not (admin/user), fetch all
—
### π§ Alternative: Use `undefined` to fetch all
If your `fetchPortfolioItems()` is written like this:
const q = provider_id
? query(portfolioCollection, where('providerId', '==', provider_id))
: portfolioCollection
Then you can also do:
const providerId = authStore.user?.role === 'provider'
? authStore.user?.provider_id
: undefined
Either way is fine β just stay consistent with how you handle `null` vs `undefined` in your logic.
—
Let me know if you want to:
– Add a toggle (show all / my items)
– Use Vueβs computed for dynamic filtering
– Extend this with category filters or pagination
Happy to help!
https://lh3.googleusercontent.com/a/ACg8ocJJWjqtnKWGzoGpP4CnX82HWkLCEHmnralDAYc8fQkschPO_dvsjg=s96-c
firebase url for image return image not 429 error
—
## β 1. Programmatic Check in JavaScript
You can check using `fetch()` or by creating a new `Image()` object.
### π Option A: Using `fetch()`
const isImageValid = async (url: string): Promise<boolean> => {
try {
const response = await fetch(url, { method: 'HEAD' })
return response.ok // returns false for 429, 404, etc.
} catch (err) {
console.error('Fetch error:', err)
return false
}
}
### π Option B: Using “ tag (image load check)
const checkImageUrl = (url: string): Promise<boolean> => {
return new Promise(resolve => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = url
})
}
—
## β 2. Use It in a Vue Component
const isImageLoaded = ref(false)
onMounted(async () => {
const url = 'https://lh3.googleusercontent.com/...'
isImageLoaded.value = await checkImageUrl(url)
})
Then in template:
<img v-if="isImageLoaded" :src="url" alt="Image" />
<span v-else>Image not available</span>
—
## β οΈ About 429 Errors
– **Googleusercontent URLs** are usually profile pictures, and they can hit rate limits (429) if requested too many times without proper caching.
– Firebase-hosted URLs from `firebasestorage.googleapis.com` are different and usually not subject to strict 429s unless abused.
—
## β Bonus: Cache the result
Use a `Map` or similar in your app to avoid checking the same image URL again and again if you're listing multiple users.
—
Let me know if you'd like to auto-fallback to a default avatar image if invalid β I can help with that too.
—
## β 1. **Server-side** (Admin SDK β secure & full access)
If you're on a **Node.js backend (like Cloud Functions, Express, etc.)**, use the **Firebase Admin SDK**:
### π§ Setup (Node.js backend):
npm install firebase-admin
### β¨ Code to Get User by UID:
import admin from 'firebase-admin'
// Initialize once (if not already)
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.applicationDefault(), // or use cert
})
}
const getUserByUid = async (uid: string) => {
try {
const userRecord = await admin.auth().getUser(uid)
console.log('User:', userRecord.toJSON())
return userRecord
} catch (error) {
console.error('Error fetching user:', error)
throw error
}
}
This returns full info:
{
uid: 'abc123',
email: 'user@example.com',
displayName: 'User Name',
photoURL: '...',
emailVerified: true,
disabled: false,
metadata: { ... },
customClaims: { ... },
}
—
## β 2. **Client-side (Web App)** β Not Allowed
> β οΈ You **cannot** get another user's details (e.g., email, name, etc.) by UID from the **client-side SDK** for privacy reasons.
Only the currently authenticated user can be accessed via:
import { getAuth } from 'firebase/auth'
const auth = getAuth()
const currentUser = auth.currentUser
console.log(currentUser?.uid, currentUser?.email)
If you need to fetch user profiles **by UID** in your frontend, store extra user data in **Firestore** under a `users` collection.
—
## β 3. Firestore-based "User Profiles" (Client-friendly)
### π Example Firestore structure:
users/
uid12345/
displayName: 'Jane Doe'
photoURL: '...'
role: 'provider'
### Fetch from client:
import { doc, getDoc } from 'firebase/firestore'
import { db } from '@/firebase'
const getUserProfile = async (uid: string) => {
const docRef = doc(db, 'users', uid)
const snapshot = await getDoc(docRef)
if (snapshot.exists()) {
return { id: snapshot.id, ...snapshot.data() }
} else {
return null
}
}
—
Let me know if you're using Firebase Auth + Firestore for custom claims (roles), and I can show how to set it all up securely.
—
## β Step-by-Step: Cache Image Check Results in Vue
### 1. Create a reusable cache
// outside of component scope (global)
const imageCheckCache = new Map<string, boolean>()
—
### 2. Create a reusable function to check image & cache result
const checkImageWithCache = async (url: string): Promise<boolean> => {
if (imageCheckCache.has(url)) {
return imageCheckCache.get(url)!
}
const result = await new Promise<boolean>((resolve) => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = url
})
imageCheckCache.set(url, result)
return result
}
—
### 3. Use it in your Vue component
const profileImageUrl = ref('https://lh3.googleusercontent.com/a/...')
const isImageAvailable = ref(false)
onMounted(async () => {
isImageAvailable.value = await checkImageWithCache(profileImageUrl.value)
})
—
### 4. Display conditionally in template
<template>
<div>
<img
v-if="isImageAvailable"
:src="profileImageUrl"
alt="Profile"
class="rounded-circle"
width="48"
/>
<img
v-else
src="/default-avatar.png"
alt="Fallback"
class="rounded-circle"
width="48"
/>
</div>
</template>
—
## β Result
– Image is checked only once
– Results are cached using a `Map`
– Repeated users with the same URL don't trigger more network requests
—
Let me know if youβd prefer to use a **Pinia store for shared caching**, or want to persist the cache across sessions with `localStorage` too.