Implement Firebase configuration with environment variables and update related components

This commit is contained in:
2026-01-03 12:48:36 -06:00
parent 7fc2aeebc8
commit 611d9424e1
11 changed files with 285 additions and 204 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Vite only exposes env vars prefixed with VITE_
# Firebase Web config (these values are not private keys, but still treat your .env as local-only)
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APP_ID=
VITE_FIREBASE_MEASUREMENT_ID=

1
.gitignore vendored
View File

@@ -29,4 +29,3 @@ pnpm-workspace.yaml
# Environment variables # Environment variables
.env .env
.env*

View File

@@ -2,6 +2,14 @@ FROM node:22 AS builder
WORKDIR /app WORKDIR /app
ARG VITE_FIREBASE_API_KEY
ARG VITE_FIREBASE_AUTH_DOMAIN
ARG VITE_FIREBASE_PROJECT_ID
ARG VITE_FIREBASE_STORAGE_BUCKET
ARG VITE_FIREBASE_MESSAGING_SENDER_ID
ARG VITE_FIREBASE_APP_ID
ARG VITE_FIREBASE_MEASUREMENT_ID
# Copy package*.json # Copy package*.json
COPY package*.json . COPY package*.json .
@@ -12,6 +20,13 @@ RUN npm install
COPY . . COPY . .
# Build the application # Build the application
ENV VITE_FIREBASE_API_KEY=$VITE_FIREBASE_API_KEY \
VITE_FIREBASE_AUTH_DOMAIN=$VITE_FIREBASE_AUTH_DOMAIN \
VITE_FIREBASE_PROJECT_ID=$VITE_FIREBASE_PROJECT_ID \
VITE_FIREBASE_STORAGE_BUCKET=$VITE_FIREBASE_STORAGE_BUCKET \
VITE_FIREBASE_MESSAGING_SENDER_ID=$VITE_FIREBASE_MESSAGING_SENDER_ID \
VITE_FIREBASE_APP_ID=$VITE_FIREBASE_APP_ID \
VITE_FIREBASE_MEASUREMENT_ID=$VITE_FIREBASE_MEASUREMENT_ID
RUN npm run build RUN npm run build
FROM nginx:alpine FROM nginx:alpine

View File

@@ -1,18 +1,19 @@
services: services:
app: app:
build: . build:
environment: context: .
apiKey: ${apiKey} args:
authDomain: ${authDomain} VITE_FIREBASE_API_KEY: ${VITE_FIREBASE_API_KEY}
projectId: ${projectId} VITE_FIREBASE_AUTH_DOMAIN: ${VITE_FIREBASE_AUTH_DOMAIN}
storageBucket: ${storageBucket} VITE_FIREBASE_PROJECT_ID: ${VITE_FIREBASE_PROJECT_ID}
messagingSenderId: ${messagingSenderId} VITE_FIREBASE_STORAGE_BUCKET: ${VITE_FIREBASE_STORAGE_BUCKET}
appId: ${appId} VITE_FIREBASE_MESSAGING_SENDER_ID: ${VITE_FIREBASE_MESSAGING_SENDER_ID}
measurementId: ${measurementId} VITE_FIREBASE_APP_ID: ${VITE_FIREBASE_APP_ID}
VITE_FIREBASE_MEASUREMENT_ID: ${VITE_FIREBASE_MEASUREMENT_ID}
ports: ports:
- "80" - "80"
deploy: deploy:
replicas: 1 replicas: 3
restart_policy: restart_policy:
condition: on-failure condition: on-failure
rollback_config: rollback_config:

View File

@@ -1,22 +1 @@
// Import the functions you need from the SDKs you need export { app, db } from "./src/firebase.ts";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: process.env.apiKey,
authDomain: process.env.authDomain,
projectId: process.env.projectId,
storageBucket: process.env.storageBucket,
messagingSenderId: process.env.messagingSenderId,
appId: process.env.appId,
measurementId: process.env.measurementId
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
export { app, db };

View File

@@ -11,6 +11,7 @@ import AthleticSection from "./components/AthleticsSection";
import EntrepreuneurshipSection from "./components/EntrepreneurshipSection"; import EntrepreuneurshipSection from "./components/EntrepreneurshipSection";
import ReviewSection from "./components/ReviewSection"; import ReviewSection from "./components/ReviewSection";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
import { isFirebaseConfigured } from "./firebase";
const App = () => { const App = () => {
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
@@ -23,11 +24,34 @@ const App = () => {
text: "", text: "",
}); });
const formatFirebaseError = (error: unknown): string => {
if (error && typeof error === "object") {
const maybeCode = (error as { code?: unknown }).code;
const maybeMessage = (error as { message?: unknown }).message;
const code = typeof maybeCode === "string" ? maybeCode : undefined;
const message = typeof maybeMessage === "string" ? maybeMessage : undefined;
if (code && message) return `${code}: ${message}`;
if (message) return message;
}
return String(error);
};
useEffect(() => { useEffect(() => {
fetchReviews().then(setReviews); fetchReviews()
.then(setReviews)
.catch((error) => {
console.error("Error fetching reviews:", error);
});
}, []); }, []);
const handleAddReview = () => { const handleAddReview = () => {
if (!isFirebaseConfigured) {
alert(
"Reviews are not configured yet. Add VITE_FIREBASE_* values to a .env/.env.local file and restart `pnpm dev`."
);
return;
}
if ( if (
!newReview.name || !newReview.name ||
newReview.rating < 1 || newReview.rating < 1 ||
@@ -42,14 +66,18 @@ const App = () => {
// Add the new review to the state // Add the new review to the state
addReview(newReview) addReview(newReview)
.then(() => { .then(() => {
fetchReviews().then(setReviews); fetchReviews()
.then(setReviews)
.catch((error) => {
console.error("Error fetching reviews after add:", error);
});
alert("Review added successfully!"); alert("Review added successfully!");
setNewReview({ name: "", rating: -1, position: "", text: "" }); setNewReview({ name: "", rating: -1, position: "", text: "" });
setShowModal(false); setShowModal(false);
}) })
.catch((error) => { .catch((error) => {
console.error("Error adding review:", error); console.error("Error adding review:", error);
alert("Failed to add review. Please try again later."); alert(`Failed to add review: ${formatFirebaseError(error)}`);
setShowModal(false); setShowModal(false);
}); });
}; };
@@ -60,8 +88,7 @@ const App = () => {
.map((_, i) => ( .map((_, i) => (
<Star <Star
key={i} key={i}
className={`w-4 h-4 ${ className={`w-4 h-4 ${i < rating ? "fill-yellow-400 text-yellow-400" : "text-gray-300"
i < rating ? "fill-yellow-400 text-yellow-400" : "text-gray-300"
}`} }`}
/> />
)); ));
@@ -69,8 +96,7 @@ const App = () => {
return ( return (
<div <div
className={`min-h-screen transition-all duration-300 ${ className={`min-h-screen transition-all duration-300 ${theme === "dark"
theme === "dark"
? "bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900" ? "bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"
: "bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100" : "bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100"
}`} }`}
@@ -78,8 +104,7 @@ const App = () => {
{/* Theme Toggle Button */} {/* Theme Toggle Button */}
<button <button
onClick={toggleTheme} onClick={toggleTheme}
className={`fixed top-4 right-4 z-50 p-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 border ${ className={`fixed top-4 right-4 z-50 p-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 border ${theme === "dark"
theme === "dark"
? "bg-slate-800 text-white border-slate-700 hover:bg-slate-700" ? "bg-slate-800 text-white border-slate-700 hover:bg-slate-700"
: "bg-white text-slate-800 border-slate-200 hover:bg-slate-50" : "bg-white text-slate-800 border-slate-200 hover:bg-slate-50"
}`} }`}
@@ -128,23 +153,20 @@ const App = () => {
{showModal && ( {showModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div <div
className={`rounded-2xl shadow-2xl w-full max-w-md ${ className={`rounded-2xl shadow-2xl w-full max-w-md ${theme === "dark" ? "bg-slate-800" : "bg-white"
theme === "dark" ? "bg-slate-800" : "bg-white"
}`} }`}
> >
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h3 <h3
className={`text-xl font-semibold ${ className={`text-xl font-semibold ${theme === "dark" ? "text-white" : "text-slate-800"
theme === "dark" ? "text-white" : "text-slate-800"
}`} }`}
> >
Add a Review Add a Review
</h3> </h3>
<button <button
onClick={() => setShowModal(false)} onClick={() => setShowModal(false)}
className={`transition-colors ${ className={`transition-colors ${theme === "dark"
theme === "dark"
? "text-slate-500 hover:text-slate-300" ? "text-slate-500 hover:text-slate-300"
: "text-slate-400 hover:text-slate-600" : "text-slate-400 hover:text-slate-600"
}`} }`}
@@ -155,8 +177,7 @@ const App = () => {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label <label
className={`block text-sm font-medium mb-1 ${ className={`block text-sm font-medium mb-1 ${theme === "dark" ? "text-slate-300" : "text-slate-700"
theme === "dark" ? "text-slate-300" : "text-slate-700"
}`} }`}
> >
Name Name
@@ -167,8 +188,7 @@ const App = () => {
onChange={(e) => onChange={(e) =>
setNewReview({ ...newReview, name: e.target.value }) setNewReview({ ...newReview, name: e.target.value })
} }
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${ className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${theme === "dark"
theme === "dark"
? "border-slate-600 bg-slate-700 text-white" ? "border-slate-600 bg-slate-700 text-white"
: "border-slate-300 bg-white text-slate-900" : "border-slate-300 bg-white text-slate-900"
}`} }`}
@@ -177,8 +197,7 @@ const App = () => {
</div> </div>
<div> <div>
<label <label
className={`block text-sm font-medium mb-1 ${ className={`block text-sm font-medium mb-1 ${theme === "dark" ? "text-slate-300" : "text-slate-700"
theme === "dark" ? "text-slate-300" : "text-slate-700"
}`} }`}
> >
Position Position
@@ -189,8 +208,7 @@ const App = () => {
onChange={(e) => onChange={(e) =>
setNewReview({ ...newReview, position: e.target.value }) setNewReview({ ...newReview, position: e.target.value })
} }
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${ className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${theme === "dark"
theme === "dark"
? "border-slate-600 bg-slate-700 text-white" ? "border-slate-600 bg-slate-700 text-white"
: "border-slate-300 bg-white text-slate-900" : "border-slate-300 bg-white text-slate-900"
}`} }`}
@@ -199,8 +217,7 @@ const App = () => {
</div> </div>
<div> <div>
<label <label
className={`block text-sm font-medium mb-1 ${ className={`block text-sm font-medium mb-1 ${theme === "dark" ? "text-slate-300" : "text-slate-700"
theme === "dark" ? "text-slate-300" : "text-slate-700"
}`} }`}
> >
Rating Rating
@@ -215,8 +232,7 @@ const App = () => {
className="text-2xl transition-transform hover:scale-110" className="text-2xl transition-transform hover:scale-110"
> >
<Star <Star
className={`w-6 h-6 ${ className={`w-6 h-6 ${star <= newReview.rating
star <= newReview.rating
? "fill-yellow-400 text-yellow-400" ? "fill-yellow-400 text-yellow-400"
: theme === "dark" : theme === "dark"
? "text-gray-600" ? "text-gray-600"
@@ -229,8 +245,7 @@ const App = () => {
</div> </div>
<div> <div>
<label <label
className={`block text-sm font-medium mb-1 ${ className={`block text-sm font-medium mb-1 ${theme === "dark" ? "text-slate-300" : "text-slate-700"
theme === "dark" ? "text-slate-300" : "text-slate-700"
}`} }`}
> >
Review Review
@@ -240,8 +255,7 @@ const App = () => {
onChange={(e) => onChange={(e) =>
setNewReview({ ...newReview, text: e.target.value }) setNewReview({ ...newReview, text: e.target.value })
} }
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${ className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${theme === "dark"
theme === "dark"
? "border-slate-600 bg-slate-700 text-white" ? "border-slate-600 bg-slate-700 text-white"
: "border-slate-300 bg-white text-slate-900" : "border-slate-300 bg-white text-slate-900"
}`} }`}
@@ -253,8 +267,7 @@ const App = () => {
<div className="flex gap-3 mt-6"> <div className="flex gap-3 mt-6">
<button <button
onClick={() => setShowModal(false)} onClick={() => setShowModal(false)}
className={`flex-1 px-4 py-2 border rounded-lg transition-colors ${ className={`flex-1 px-4 py-2 border rounded-lg transition-colors ${theme === "dark"
theme === "dark"
? "border-slate-600 text-slate-300 bg-slate-700 hover:bg-slate-600" ? "border-slate-600 text-slate-300 bg-slate-700 hover:bg-slate-600"
: "border-slate-300 text-slate-700 bg-white hover:bg-slate-50" : "border-slate-300 text-slate-700 bg-white hover:bg-slate-50"
}`} }`}
@@ -263,7 +276,8 @@ const App = () => {
</button> </button>
<button <button
onClick={handleAddReview} onClick={handleAddReview}
className="flex-1 px-4 py-2 text-white transition-colors bg-indigo-600 rounded-lg hover:bg-indigo-700" disabled={!isFirebaseConfigured}
className="flex-1 px-4 py-2 text-white transition-colors bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-indigo-600"
> >
Add Review Add Review
</button> </button>

View File

@@ -10,10 +10,9 @@ const Footer: React.FC<Props> = ({ isOutOfCollege }) => {
return ( return (
<section className="mb-16"> <section className="mb-16">
<div className={`rounded-2xl shadow-xl text-white p-12 text-center transition-all duration-300 ${ <div className={`rounded-2xl shadow-xl text-white p-12 text-center transition-all duration-300 ${theme === 'dark'
theme === 'dark' ? 'bg-linear-to-r from-indigo-700 to-purple-700'
? 'bg-gradient-to-r from-indigo-700 to-purple-700' : 'bg-linear-to-r from-indigo-600 to-purple-600'
: 'bg-gradient-to-r from-indigo-600 to-purple-600'
}`}> }`}>
<h2 className="mb-6 text-4xl font-bold">Let's Connect</h2> <h2 className="mb-6 text-4xl font-bold">Let's Connect</h2>
<p className="mb-8 text-xl opacity-90"> <p className="mb-8 text-xl opacity-90">
@@ -22,7 +21,7 @@ const Footer: React.FC<Props> = ({ isOutOfCollege }) => {
</p> </p>
<div className="flex flex-wrap justify-center gap-6"> <div className="flex flex-wrap justify-center gap-6">
<a <a
href="khokharmaaz@gmail.com" href="mailto:khokharmaaz@gmail.com"
className="flex items-center gap-2 px-6 py-3 transition-colors rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30" className="flex items-center gap-2 px-6 py-3 transition-colors rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30"
> >
<Mail className="w-5 h-5" /> <Mail className="w-5 h-5" />

View File

@@ -1,5 +1,5 @@
import { Star, Plus } from 'lucide-react'; import { Star, Plus } from 'lucide-react';
import { type JSX } from 'react'; import type { FC, JSX } from 'react';
import { type Review } from '../reviewsApi'; import { type Review } from '../reviewsApi';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
@@ -9,18 +9,16 @@ interface Props {
renderStars: (rating: number) => JSX.Element[]; renderStars: (rating: number) => JSX.Element[];
} }
const ReviewSection: React.FC<Props> = ({ setShowModal, renderStars, reviews }) => { const ReviewSection: FC<Props> = ({ setShowModal, renderStars, reviews }) => {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<section className="mb-16"> <section className="mb-16">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Star className={`w-8 h-8 transition-colors duration-300 ${ <Star className={`w-8 h-8 transition-colors duration-300 ${theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'
theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'
}`} /> }`} />
<h2 className={`text-3xl font-bold transition-colors duration-300 ${ <h2 className={`text-3xl font-bold transition-colors duration-300 ${theme === 'dark' ? 'text-white' : 'text-slate-800'
theme === 'dark' ? 'text-white' : 'text-slate-800'
}`}> }`}>
Professional Reviews Professional Reviews
</h2> </h2>
@@ -34,30 +32,25 @@ const ReviewSection: React.FC<Props> = ({ setShowModal, renderStars, reviews })
</button> </button>
</div> </div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{reviews.length > 1 ? ( {reviews.length > 0 ? (
reviews.map((review, index) => ( reviews.map((review, index) => (
<div <div
key={index} key={index}
className={`rounded-2xl shadow-lg p-6 hover:shadow-xl transition-all duration-300 ${ className={`rounded-2xl shadow-lg p-6 hover:shadow-xl transition-all duration-300 ${theme === 'dark' ? 'bg-slate-800' : 'bg-white'
theme === 'dark' ? 'bg-slate-800' : 'bg-white'
}`} }`}
> >
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className={`w-12 h-12 rounded-full flex items-center justify-center transition-colors duration-300 ${ <div className={`w-12 h-12 rounded-full flex items-center justify-center transition-colors duration-300 ${theme === 'dark' ? 'bg-blue-900/30' : 'bg-blue-100'
theme === 'dark' ? 'bg-blue-900/30' : 'bg-blue-100'
}`}> }`}>
<Star className={`w-8 h-8 ${ <Star className={`w-8 h-8 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'
theme === 'dark' ? 'text-blue-400' : 'text-blue-600'
}`} /> }`} />
</div> </div>
<div> <div>
<h3 className={`text-lg font-semibold transition-colors duration-300 ${ <h3 className={`text-lg font-semibold transition-colors duration-300 ${theme === 'dark' ? 'text-white' : 'text-slate-800'
theme === 'dark' ? 'text-white' : 'text-slate-800'
}`}> }`}>
{review.name} {review.name}
</h3> </h3>
<p className={`text-sm transition-colors duration-300 ${ <p className={`text-sm transition-colors duration-300 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-600'
theme === 'dark' ? 'text-slate-400' : 'text-slate-600'
}`}> }`}>
{review.position} {review.position}
</p> </p>
@@ -66,14 +59,12 @@ const ReviewSection: React.FC<Props> = ({ setShowModal, renderStars, reviews })
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
{renderStars(review.rating)} {renderStars(review.rating)}
</div> </div>
<p className={`transition-colors duration-300 ${ <p className={`transition-colors duration-300 ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'
theme === 'dark' ? 'text-slate-300' : 'text-slate-700'
}`}>{review.text}</p> }`}>{review.text}</p>
</div> </div>
)) ))
) : ( ) : (
<div className={`col-span-full text-center rounded-2xl p-6 transition-colors duration-300 ${ <div className={`col-span-full text-center rounded-2xl p-6 transition-colors duration-300 ${theme === 'dark'
theme === 'dark'
? 'text-slate-400 bg-slate-700' ? 'text-slate-400 bg-slate-700'
: 'text-slate-500 bg-slate-100' : 'text-slate-500 bg-slate-100'
}`}> }`}>

58
src/firebase.ts Normal file
View File

@@ -0,0 +1,58 @@
import { initializeApp, type FirebaseApp } from "firebase/app";
import { getFirestore, type Firestore } from "firebase/firestore";
const requiredEnvKeys = [
"VITE_FIREBASE_API_KEY",
"VITE_FIREBASE_AUTH_DOMAIN",
"VITE_FIREBASE_PROJECT_ID",
"VITE_FIREBASE_STORAGE_BUCKET",
"VITE_FIREBASE_MESSAGING_SENDER_ID",
"VITE_FIREBASE_APP_ID",
] as const;
type RequiredEnvKey = (typeof requiredEnvKeys)[number];
type FirebaseConfig = {
apiKey: string;
authDomain: string;
projectId: string;
storageBucket: string;
messagingSenderId: string;
appId: string;
measurementId?: string;
};
function buildFirebaseConfig(): FirebaseConfig | null {
const missing = requiredEnvKeys.filter(
(key) => !import.meta.env[key as RequiredEnvKey]
);
if (missing.length > 0) {
console.error(
`[firebase] Missing Vite env vars: ${missing.join(
", "
)}. Add them to .env (dev) or as build args (docker build).`
);
return null;
}
return {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY!,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN!,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID!,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID!,
appId: import.meta.env.VITE_FIREBASE_APP_ID!,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};
}
const firebaseConfig = buildFirebaseConfig();
export const isFirebaseConfigured = Boolean(firebaseConfig);
export const app: FirebaseApp | null = firebaseConfig
? initializeApp(firebaseConfig)
: null;
export const db: Firestore | null = app ? getFirestore(app) : null;

View File

@@ -1,4 +1,4 @@
import { db } from '../firebase'; import { db } from './firebase.ts';
import { collection, getDocs, addDoc } from 'firebase/firestore'; import { collection, getDocs, addDoc } from 'firebase/firestore';
export type Review = { export type Review = {
@@ -8,13 +8,15 @@ export type Review = {
text: string; text: string;
}; };
const reviewsCollection = collection(db, 'reviews'); const reviewsCollection = db ? collection(db, 'reviews') : null;
export async function fetchReviews(): Promise<Review[]> { export async function fetchReviews(): Promise<Review[]> {
if (!reviewsCollection) return [];
const snapshot = await getDocs(reviewsCollection); const snapshot = await getDocs(reviewsCollection);
return snapshot.docs.map(doc => doc.data() as Review); return snapshot.docs.map(doc => doc.data() as Review);
} }
export async function addReview(review: Review): Promise<void> { export async function addReview(review: Review): Promise<void> {
if (!reviewsCollection) throw new Error('Firestore is not configured (missing Vite Firebase env vars).');
await addDoc(reviewsCollection, review); await addDoc(reviewsCollection, review);
} }

14
src/vite-env.d.ts vendored
View File

@@ -1 +1,15 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_FIREBASE_API_KEY?: string;
readonly VITE_FIREBASE_AUTH_DOMAIN?: string;
readonly VITE_FIREBASE_PROJECT_ID?: string;
readonly VITE_FIREBASE_STORAGE_BUCKET?: string;
readonly VITE_FIREBASE_MESSAGING_SENDER_ID?: string;
readonly VITE_FIREBASE_APP_ID?: string;
readonly VITE_FIREBASE_MEASUREMENT_ID?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}