Implement Firebase configuration with environment variables and update related components
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal 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
1
.gitignore
vendored
@@ -29,4 +29,3 @@ pnpm-workspace.yaml
|
|||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
.env*
|
|
||||||
15
Dockerfile
15
Dockerfile
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
23
firebase.ts
23
firebase.ts
@@ -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 };
|
|
||||||
116
src/App.tsx
116
src/App.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
58
src/firebase.ts
Normal 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;
|
||||||
@@ -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
14
src/vite-env.d.ts
vendored
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user