A fully responsive personal portfolio website with an integrated Firebase-powered blog admin panel. Features a modern admin dashboard for content management, Firebase Storage media uploads, scheduled post publishing, and a public blog with modal post views.
- HTML5 + CSS3 β Semantic markup with modern responsive design
- Vanilla JavaScript β Zero framework dependencies for maximum performance
- Firebase SDK β Authentication, Realtime Database, Storage integration
- Node.js + Express β Optional upload proxy server (for Firebase Storage)
- Firebase Admin SDK β Server-side Firebase operations
- Multer β File upload handling
- Firebase Authentication β Email/password & GitHub OAuth
- Firebase Realtime Database β Blog post storage and retrieval
- Cloudinary β Media file hosting for images and videos
dee/
βββ index.html # Main portfolio page
βββ blog.html # Public blog with post cards and modal viewer
βββ admin.html # Admin dashboard (requires auth)
βββ styles.css # Global styles (1200+ lines)
βββ script.js # Portfolio animations and interactions
βββ blogscript.js # Blog data loading from Firebase
βββ adminscript.js # Admin panel logic (auth, CRUD, uploads)
βββ server.js # Optional Node.js upload proxy
βββ package.json # Node dependencies for upload server
βββ package-lock.json # Locked dependency versions
βββ .env.example # Environment variable template
βββ serviceAccountKey.json # Firebase Admin credentials (gitignored)
βββ cors.json # Firebase Storage CORS configuration
βββ media/ # Images, videos, icons, favicon
βββ docs/ # Project documentation
βββ README_ADMIN.md
βββ ADMIN_IMPROVEMENTS.md
βββ ADMIN_QUICK_START.md
βββ FIREBASE_SETUP.md
βββ FIREBASE_CONFIG_LOCATION.md
βββ SYSTEM_CSS_DESIGN.md
- Hero Section β Animated background video with profile overlay
- About Section β Bio, stats, and animated code editor
- Projects Carousel β Horizontal slider with media support (images/videos)
- Skills Grid β Progress bars and technology badges
- Contact Section β Direct links to email, GitHub, LinkedIn
- Responsive Design β Mobile-first approach with hamburger navigation
- Post Cards β Grid layout with featured images/videos
- Category Filtering β Filter posts by category or view all
- Tag Cloud β Visual tag navigation
- Post Modal β Full-screen overlay for reading complete posts
- Inline Code Markup β Render
`code`in titles and excerpts - Media Support β Display images or videos (15-30s clips)
- Firebase Integration β Real-time post loading from database
- Secure Authentication β Email/password and GitHub OAuth with Firebase Auth
- Post Composer β Rich form with live preview
- Media Upload β Cloudinary integration for images/videos
- Scheduled Publishing β Set future publish dates
- Post Management β Edit, delete, search, and filter posts
- Status Tracking β Published, scheduled, or draft states
- Category & Tags β Multiple tag support with visual chips
- Responsive Layout β Works on desktop and mobile
- Real-time Preview β See how posts will appear on blog
git clone <repository-url>
cd dee
npm install # Only needed if using upload server- Go to Firebase Console
- Create a new project:
dee-s-site(or your preferred name) - Enable Authentication β Email/Password & GitHub OAuth providers
- Enable Realtime Database β Start in test mode
- Go to Project Settings β General
- Scroll to Your apps β Click web icon (</>) to add a web app
- Copy the
firebaseConfigvalues - Create
.envfrom.env.exampleand fill the matchingFIREBASE_*variables - Start the Node server (
npm start) so it serves/firebase-config.jsto the frontend
- Go to Realtime Database β Rules
- Replace with:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"blogPosts": {
".read": true,
".write": "auth != null"
}
}
}- Click Publish
- Sign up at Cloudinary (free tier)
- Go to your Dashboard and copy your Cloud Name
- Go to Settings β Upload
- Create an Upload Preset:
- Name:
blog_uploads - Signing Mode: Unsigned
- Click Save
- Name:
- Update
adminscript.jsline 233:Replaceconst cloudinaryUrl = 'https://api.cloudinary.com/v1_1/YOUR_CLOUD_NAME/upload';
YOUR_CLOUD_NAMEwith your actual Cloudinary cloud name
To enable GitHub sign-in for the admin panel:
- Save as
serviceAccountKey.jsonin project root
-
Set CORS rules (requires
gsutil):gsutil cors set cors.json gs://your-bucket-name.appspot.com -
Update
adminscript.jsto use the upload server (seeserver.js)
# Start app server (serves pages, upload API, and /firebase-config.js)
npm start- Open http://localhost:4001/admin.html
- Create an account with email/password
- Start creating blog posts!
- Navigate to
admin.htmland sign in - Fill out the form:
- Title (supports
`inline code`) - Category (tutorials/projects/tips)
- Date (defaults to today)
- Excerpt (short description, 150 chars recommended)
- Content (full post body)
- Featured Media:
- Option A: Paste media URL
- Option B: Upload image/video (auto-uploads to Cloudinary)
- Tags (press Enter after each tag)
- Publish Time (optional: schedule for future)
- Title (supports
- Preview updates live as you type
- Click "Create Post" to publish immediately or schedule
- View on blog at
blog.html
- Edit: Click "Edit" on any post in the Manage Posts section
- Delete: Click "Delete" and confirm in the modal
- Search: Type in the search box to filter by title/tags
- Filter: Use dropdowns to filter by category or status
- Open
blog.html - Browse post cards with featured media
- Click "Read More β" to open full post modal
- Close modal by:
- Clicking the Γ button
- Pressing Escape
- Clicking outside the modal
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"blogPosts": {
".read": true,
".write": "auth != null"
}
}
}Explanation:
users/{uid}β Only authenticated users can read/write their own datablogPostsβ Anyone can read (public blog), only authenticated users can write
Cloudinary handles media uploads securely with:
- Upload Preset:
blog_uploadsset to Unsigned mode - CDN Delivery: Automatic CORS and optimization
- Free Tier: Up to 25GB storage, 25GB bandwidth per month
- Update name in
index.htmlline 16:<div class="nav-brand">Dee</div> - Update footer in all pages: Line ~130+ in each HTML file
- Replace logo in
media/favicon.svg
Edit CSS variables in styles.css (lines 9-25):
:root {
--primary-color: #1e3a8a;
--secondary-color: #1e40af;
--accent-color: #2563eb;
--text-primary: #1f2937;
--bg-primary: #ffffff;
/* ... */
}Edit the array in blog.html (lines 150-228) to customize placeholder posts
{
id: "generated-firebase-key",
title: "Post Title",
slug: "post-title",
excerpt: "Brief summary...",
content: "Full post content...",
category: "tutorials",
tags: ["JavaScript", "React"],
author: "Dee",
date: "2026-02-15",
image: "https://...", // Deprecated, use mediaUrl
mediaUrl: "https://...", // Cloudinary secure URL
mediaType: "image", // "image" or "video"
status: "published", // "published", "scheduled", "draft"
publishAt: "", // ISO date for scheduled posts
createdAt: "2026-02-15T10:30:00Z",
updatedAt: "2026-02-15T10:30:00Z",
publishedAt: "2026-02-15T10:30:00Z"
}- Check Firebase Database rules allow public read on
blogPosts - Verify post
statusis"published"(not"scheduled"or"draft") - Check browser console (F12) for permission errors
- Ensure Firebase config is correct in
blogscript.js
- Cloudinary errors: Verify Cloud Name and upload preset name
- "Upload preset not found": Make sure preset is named
blog_uploadsand is Unsigned - CORS errors: Cloudinary handles CORS automatically
- Network errors: Check internet connection and Cloudinary config
- Update Firebase Database rules (see Security Configuration above)
- Sign out and sign back in to refresh auth token
- Check Firebase Auth is enabled in console
- Check browser console for JavaScript errors
- Verify
postsCollectionarray is populated - Hard refresh (Ctrl+Shift+R) to clear cache
- Format: JPG, PNG, WebP, or AVIF
- Size: 1200Γ800px recommended for featured images
- File size: <500KB (use compression tools)
- Ratio: 3:2 aspect ratio looks best in cards
- Format: MP4, WebM, MOV
- Duration: 15-30 seconds for featured clips
- Resolution: 1280Γ720 or 1920Γ1080
- File size: <10MB recommended
- Codec: H.264 for broad compatibility
- Format: SVG preferred (or PNG with transparency)
- Size: 64Γ64 to 512Γ512 pixels
- Colors: Match brand palette in
styles.css
- Push code to GitHub
- Connect repository to hosting platform
- Configure build settings:
- Build command: (none, static site)
- Publish directory:
/(root)
- Add environment variables (if any)
- Deploy!
# Install Firebase CLI
npm install -g firebase-tools
# Login to Firebase
firebase login
# Initialize hosting
firebase init hosting
# Deploy
firebase deploy --only hosting| File | Purpose |
|---|---|
| docs/README_ADMIN.md | Admin panel architecture and workflows |
| docs/ADMIN_IMPROVEMENTS.md | Admin enhancement notes and updates |
| docs/ADMIN_QUICK_START.md | 5-minute setup checklist |
| docs/FIREBASE_SETUP.md | Detailed Firebase configuration |
| docs/FIREBASE_CONFIG_LOCATION.md | Where to place Firebase credentials and config |
| docs/SYSTEM_CSS_DESIGN.md | UI components and visual guidelines |
This is a personal portfolio project, but suggestions and improvements are welcome:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See LICENSE. Free to use and adapt for personal projects. Attribution appreciated but not required.
- Developer: Dee (Agoma Divine E.)
- Framework: None (Vanilla JS for maximum performance)
- Icons: Hand-crafted SVGs
- Fonts: Inter (Google Fonts)
Built with β€οΈ using vanilla HTML, CSS, and JavaScript