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=

3
.gitignore vendored
View File

@@ -28,5 +28,4 @@ pnpm-lock.yaml
pnpm-workspace.yaml 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,29 +88,26 @@ 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" }`}
}`}
/> />
)); ));
}; };
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"
}`} }`}
> >
{/* 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"
}`} }`}
aria-label="Toggle theme" aria-label="Toggle theme"
> >
{theme === "light" ? ( {theme === "light" ? (
@@ -128,26 +153,23 @@ 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"
}`} }`}
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
@@ -155,9 +177,8 @@ 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
</label> </label>
@@ -167,19 +188,17 @@ 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"
}`} }`}
placeholder="Your full name" placeholder="Your full name"
/> />
</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
</label> </label>
@@ -189,19 +208,17 @@ 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"
}`} }`}
placeholder="Your job title and company" placeholder="Your job title and company"
/> />
</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
</label> </label>
@@ -215,13 +232,12 @@ 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"
: "text-gray-300" : "text-gray-300"
}`} }`}
/> />
</button> </button>
))} ))}
@@ -229,9 +245,8 @@ 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
</label> </label>
@@ -240,11 +255,10 @@ 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"
}`} }`}
rows={4} rows={4}
placeholder="Share your experience working with Maaz..." placeholder="Share your experience working with Maaz..."
/> />
@@ -253,17 +267,17 @@ 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"
}`} }`}
> >
Cancel Cancel
</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

@@ -2,52 +2,51 @@ import { Mail, Linkedin, Github } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
interface Props { interface Props {
isOutOfCollege?: boolean; isOutOfCollege?: boolean;
} }
const Footer: React.FC<Props> = ({ isOutOfCollege }) => { const Footer: React.FC<Props> = ({ isOutOfCollege }) => {
const { theme } = useTheme(); const { theme } = useTheme();
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"> Ready to collaborate on something amazing? I'm always excited to
Ready to collaborate on something amazing? I'm always excited to discuss new opportunities and innovative projects.
discuss new opportunities and innovative projects. </p>
</p> <div className="flex flex-wrap justify-center gap-6">
<div className="flex flex-wrap justify-center gap-6"> <a
<a href="mailto:khokharmaaz@gmail.com"
href="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" /> Email Me
Email Me </a>
</a> {isOutOfCollege && (
{isOutOfCollege && ( <a
<a href="https://linkedin.com/MyLinkedinProfile"
href="https://linkedin.com/MyLinkedinProfile" 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" >
> <Linkedin className="w-5 h-5" />
<Linkedin className="w-5 h-5" /> LinkedIn
LinkedIn </a>
</a> )}
)} <a
<a href="https://github.com/coolestcoder655"
href="https://github.com/coolestcoder655" 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" >
> <Github className="w-5 h-5" />
<Github className="w-5 h-5" /> GitHub
GitHub </a>
</a> </div>
</div> </div>
</div> </section>
</section> );
);
}; };
export default Footer; export default Footer;

View File

@@ -1,88 +1,79 @@
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';
interface Props { interface Props {
setShowModal: (show: boolean) => void; setShowModal: (show: boolean) => void;
reviews: Review[]; reviews: Review[];
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 ${theme === 'dark' ? 'text-white' : 'text-slate-800'
<h2 className={`text-3xl font-bold transition-colors duration-300 ${ }`}>
theme === 'dark' ? 'text-white' : 'text-slate-800' Professional Reviews
}`}> </h2>
Professional Reviews </div>
</h2> <button
</div> onClick={() => setShowModal(true)}
<button className="flex items-center gap-2 px-6 py-3 font-medium text-white transition-colors bg-indigo-600 rounded-lg hover:bg-indigo-700"
onClick={() => setShowModal(true)} >
className="flex items-center gap-2 px-6 py-3 font-medium text-white transition-colors bg-indigo-600 rounded-lg hover:bg-indigo-700" <Plus className="w-5 h-5" />
Add Review
</button>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{reviews.length > 0 ? (
reviews.map((review, index) => (
<div
key={index}
className={`rounded-2xl shadow-lg p-6 hover:shadow-xl transition-all duration-300 ${theme === 'dark' ? 'bg-slate-800' : 'bg-white'
}`}
> >
<Plus className="w-5 h-5" /> <div className="flex items-center gap-3 mb-4">
Add Review <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'
</button> }`}>
</div> <Star className={`w-8 h-8 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> }`} />
{reviews.length > 1 ? ( </div>
reviews.map((review, index) => ( <div>
<div <h3 className={`text-lg font-semibold transition-colors duration-300 ${theme === 'dark' ? 'text-white' : 'text-slate-800'
key={index} }`}>
className={`rounded-2xl shadow-lg p-6 hover:shadow-xl transition-all duration-300 ${ {review.name}
theme === 'dark' ? 'bg-slate-800' : 'bg-white' </h3>
}`} <p className={`text-sm transition-colors duration-300 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-600'
> }`}>
<div className="flex items-center gap-3 mb-4"> {review.position}
<div className={`w-12 h-12 rounded-full flex items-center justify-center transition-colors duration-300 ${ </p>
theme === 'dark' ? 'bg-blue-900/30' : 'bg-blue-100'
}`}>
<Star className={`w-8 h-8 ${
theme === 'dark' ? 'text-blue-400' : 'text-blue-600'
}`} />
</div>
<div>
<h3 className={`text-lg font-semibold transition-colors duration-300 ${
theme === 'dark' ? 'text-white' : 'text-slate-800'
}`}>
{review.name}
</h3>
<p className={`text-sm transition-colors duration-300 ${
theme === 'dark' ? 'text-slate-400' : 'text-slate-600'
}`}>
{review.position}
</p>
</div>
</div>
<div className="flex items-center mb-4">
{renderStars(review.rating)}
</div>
<p className={`transition-colors duration-300 ${
theme === 'dark' ? 'text-slate-300' : 'text-slate-700'
}`}>{review.text}</p>
</div> </div>
))
) : (
<div className={`col-span-full text-center rounded-2xl p-6 transition-colors duration-300 ${
theme === 'dark'
? 'text-slate-400 bg-slate-700'
: 'text-slate-500 bg-slate-100'
}`}>
No reviews yet. Be the first to add one!
</div> </div>
)} <div className="flex items-center mb-4">
{renderStars(review.rating)}
</div>
<p className={`transition-colors duration-300 ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'
}`}>{review.text}</p>
</div>
))
) : (
<div className={`col-span-full text-center rounded-2xl p-6 transition-colors duration-300 ${theme === 'dark'
? 'text-slate-400 bg-slate-700'
: 'text-slate-500 bg-slate-100'
}`}>
No reviews yet. Be the first to add one!
</div> </div>
</section> )}
); </div>
</section>
);
}; };
export default ReviewSection; export default ReviewSection;

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;
}