initial app
This commit is contained in:
		
						commit
						27c78e2ac4
					
				
							
								
								
									
										35
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||||
|  | 
 | ||||||
|  | # dependencies | ||||||
|  | /node_modules | ||||||
|  | /.pnp | ||||||
|  | .pnp.js | ||||||
|  | .yarn/install-state.gz | ||||||
|  | 
 | ||||||
|  | # testing | ||||||
|  | /coverage | ||||||
|  | 
 | ||||||
|  | # next.js | ||||||
|  | /.next/ | ||||||
|  | /out/ | ||||||
|  | 
 | ||||||
|  | # production | ||||||
|  | /build | ||||||
|  | 
 | ||||||
|  | # misc | ||||||
|  | .DS_Store | ||||||
|  | *.pem | ||||||
|  | 
 | ||||||
|  | # debug | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | 
 | ||||||
|  | # local env files | ||||||
|  | .env*.local | ||||||
|  | 
 | ||||||
|  | # Vercel (removed - using VPS deployment) | ||||||
|  | 
 | ||||||
|  | # typescript | ||||||
|  | *.tsbuildinfo | ||||||
|  | next-env.d.ts | ||||||
							
								
								
									
										168
									
								
								DEPLOYMENT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								DEPLOYMENT.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,168 @@ | |||||||
|  | # Deployment Guide | ||||||
|  | 
 | ||||||
|  | ## 🚀 Deploy to a VPS with Nginx (Reverse Proxy) | ||||||
|  | 
 | ||||||
|  | This project exports fully static assets in `out/`. You can serve them directly with Nginx. | ||||||
|  | 
 | ||||||
|  | ### 1) Build static files | ||||||
|  | ```bash | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | By default, the app is built for the root path `/`. If you want to serve it under a subpath (e.g. `/namecard`), build with: | ||||||
|  | ```bash | ||||||
|  | BASE_PATH=/namecard NEXT_PUBLIC_BASE_URL=https://example.com/namecard npm run build | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 2) Upload to server | ||||||
|  | ```bash | ||||||
|  | scp -r out/ user@server:/var/www/namecard | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 3) Nginx config (root path) | ||||||
|  | ```nginx | ||||||
|  | server { | ||||||
|  |     listen 80; | ||||||
|  |     server_name example.com; # your domain | ||||||
|  | 
 | ||||||
|  |     root /var/www/namecard; | ||||||
|  |     index index.html; | ||||||
|  | 
 | ||||||
|  |     location / { | ||||||
|  |         try_files $uri $uri/ /index.html; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 4) Nginx config (subpath, e.g. /namecard) | ||||||
|  | Build with `BASE_PATH=/namecard` and then: | ||||||
|  | ```nginx | ||||||
|  | server { | ||||||
|  |     listen 80; | ||||||
|  |     server_name example.com; | ||||||
|  | 
 | ||||||
|  |     location /namecard/ { | ||||||
|  |         alias /var/www/namecard/; | ||||||
|  |         try_files $uri $uri/ /namecard/index.html; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 5) Reload Nginx | ||||||
|  | ```bash | ||||||
|  | sudo nginx -t && sudo systemctl reload nginx | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 🌐 Custom Domain Setup | ||||||
|  | 
 | ||||||
|  | 1. **Configure DNS:** | ||||||
|  |    - Add A record pointing to your VPS IP address | ||||||
|  |    - Or CNAME if using a subdomain (e.g., `card.rooftop.my`) | ||||||
|  | 
 | ||||||
|  | 2. **SSL Certificate:** | ||||||
|  |    - Use Let's Encrypt: `sudo certbot --nginx` | ||||||
|  |    - Auto-renewal for free SSL certificates | ||||||
|  | 
 | ||||||
|  | ## 📱 PWA Configuration (Optional) | ||||||
|  | 
 | ||||||
|  | To enable "Add to Home Screen" functionality: | ||||||
|  | 
 | ||||||
|  | 1. **Create manifest.json:** | ||||||
|  |    ```json | ||||||
|  |    { | ||||||
|  |      "name": "Rooftop Energy Namecard", | ||||||
|  |      "short_name": "Rooftop", | ||||||
|  |      "description": "Digital namecard for Rooftop Energy staff", | ||||||
|  |      "start_url": "/", | ||||||
|  |      "display": "standalone", | ||||||
|  |      "background_color": "#0a0a0a", | ||||||
|  |      "theme_color": "#fcd913", | ||||||
|  |      "icons": [ | ||||||
|  |        { | ||||||
|  |          "src": "/logo.png", | ||||||
|  |          "sizes": "192x192", | ||||||
|  |          "type": "image/png" | ||||||
|  |        } | ||||||
|  |      ] | ||||||
|  |    } | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 2. **Add to layout.tsx:** | ||||||
|  |    ```tsx | ||||||
|  |    <head> | ||||||
|  |      <link rel="manifest" href="/manifest.json" /> | ||||||
|  |    </head> | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | ## 🔧 Environment Variables | ||||||
|  | 
 | ||||||
|  | Set at build time (locally or in CI): | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | # Base URL used for Open Graph/Twitter image URLs and absolute links | ||||||
|  | NEXT_PUBLIC_BASE_URL=https://example.com | ||||||
|  | 
 | ||||||
|  | # Optional: if serving under a subpath, set both | ||||||
|  | BASE_PATH=/namecard | ||||||
|  | NEXT_PUBLIC_BASE_URL=https://example.com/namecard | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 📊 Performance Monitoring | ||||||
|  | 
 | ||||||
|  | 1. **Web Analytics:** | ||||||
|  |    - Use Google Analytics or similar service | ||||||
|  |    - Monitor Core Web Vitals with PageSpeed Insights | ||||||
|  | 
 | ||||||
|  | 2. **Server Monitoring:** | ||||||
|  |    - Monitor server resources (CPU, memory, disk) | ||||||
|  |    - Set up alerts for high resource usage | ||||||
|  | 
 | ||||||
|  | ## 🚨 Troubleshooting | ||||||
|  | 
 | ||||||
|  | ### Build Errors | ||||||
|  | 
 | ||||||
|  | - **Node version:** Ensure Node.js 18+ is used | ||||||
|  | - **Dependencies:** Run `npm install` locally first | ||||||
|  | - **TypeScript errors:** Run `npm run type-check` locally | ||||||
|  | 
 | ||||||
|  | ### Runtime Issues | ||||||
|  | 
 | ||||||
|  | - **Logo not loading:** Check `public/logo.png` exists | ||||||
|  | - **Styling issues:** Verify Tailwind CSS is building correctly | ||||||
|  | - **Query params not working:** Check URL encoding | ||||||
|  | 
 | ||||||
|  | ### Performance Issues | ||||||
|  | 
 | ||||||
|  | - **Large bundle:** Check for unused dependencies | ||||||
|  | - **Slow loading:** Optimize images and enable compression | ||||||
|  | - **SEO issues:** Verify meta tags and Open Graph | ||||||
|  | 
 | ||||||
|  | ## 📈 Post-Deployment Checklist | ||||||
|  | 
 | ||||||
|  | - [ ] Logo displays correctly | ||||||
|  | - [ ] All CTAs work (Call, Email, WhatsApp, LinkedIn) | ||||||
|  | - [ ] vCard download functions | ||||||
|  | - [ ] Copy details works | ||||||
|  | - [ ] Mobile responsive design | ||||||
|  | - [ ] URL query overrides work | ||||||
|  | - [ ] Performance scores meet targets | ||||||
|  | - [ ] Analytics tracking (if enabled) | ||||||
|  | - [ ] Custom domain configured (if applicable) | ||||||
|  | 
 | ||||||
|  | ## 🔄 Updates & Maintenance | ||||||
|  | 
 | ||||||
|  | ### Regular Updates | ||||||
|  | 
 | ||||||
|  | 1. **Dependencies:** Run `npm audit` and update packages | ||||||
|  | 2. **Content:** Edit `src/config/staff.ts` for staff changes | ||||||
|  | 3. **Logo:** Replace `public/logo.png` when needed | ||||||
|  | 
 | ||||||
|  | ### Deployment Updates | ||||||
|  | 
 | ||||||
|  | 1. Build locally: `npm run build` | ||||||
|  | 2. Sync `out/` to your server: `rsync -az out/ user@server:/var/www/namecard` | ||||||
|  | 3. Reload Nginx: `sudo systemctl reload nginx` | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | **Need help?** Check the main README.md or contact the development team. | ||||||
							
								
								
									
										252
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,252 @@ | |||||||
|  | # Rooftop Energy Digital Namecard | ||||||
|  | 
 | ||||||
|  | A mobile-first, dark-mode single-page application that presents Rooftop Energy staff namecards with clear CTAs, vCard download, and contact details copying functionality. | ||||||
|  | 
 | ||||||
|  | ## 🚀 Features | ||||||
|  | 
 | ||||||
|  | - **Contact CTAs**: Call, Email, WhatsApp, and LinkedIn buttons | ||||||
|  | - **vCard Download**: Generate and download contact information as .vcf files | ||||||
|  | - **Copy Details**: Copy all contact information to clipboard | ||||||
|  | - **URL Overrides**: Customize details via query parameters for quick sharing | ||||||
|  | - **Responsive Design**: Optimized for mobile with graceful desktop scaling | ||||||
|  | - **Dark Mode**: Professional dark theme with Rooftop Energy branding | ||||||
|  | 
 | ||||||
|  | ## 🛠️ Tech Stack | ||||||
|  | 
 | ||||||
|  | - **Framework**: Next.js 14 (App Router) | ||||||
|  | - **Styling**: Tailwind CSS | ||||||
|  | - **Icons**: Lucide React | ||||||
|  | - **Fonts**: Exo 2 (Google Fonts) | ||||||
|  | - **Deployment**: VPS-ready with static export and reverse proxy support | ||||||
|  | 
 | ||||||
|  | ## 📁 Project Structure | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | src/ | ||||||
|  | ├── app/ | ||||||
|  | │   ├── globals.css          # Global styles and Tailwind imports | ||||||
|  | │   ├── layout.tsx           # Root layout with font loading | ||||||
|  | │   ├── page.tsx             # Main page with employee directory | ||||||
|  | │   ├── [employeeId]/        # Dynamic routes for individual employees | ||||||
|  | │   │   └── page.tsx         # Individual employee namecard page | ||||||
|  | │   └── not-found.tsx        # Custom 404 page | ||||||
|  | ├── components/ | ||||||
|  | │   └── Namecard.tsx         # Main namecard component | ||||||
|  | └── config/ | ||||||
|  |     ├── staff.ts              # Base staff profile interface | ||||||
|  |     └── employees.ts          # All employee data and utilities | ||||||
|  | public/ | ||||||
|  | ├── logo.png                 # Company logo (replace with actual file) | ||||||
|  | ├── profilepic.png           # Default profile picture | ||||||
|  | ├── whatsapp.png            # WhatsApp icon | ||||||
|  | └── linkedin.png            # LinkedIn icon | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## ⚙️ Configuration | ||||||
|  | 
 | ||||||
|  | ### Managing Employee Data | ||||||
|  | 
 | ||||||
|  | The application now supports multiple employees with individual routing. All employee data is stored in `src/config/employees.ts`. | ||||||
|  | 
 | ||||||
|  | #### Adding New Employees | ||||||
|  | 
 | ||||||
|  | To add a new employee, add an entry to the `employees` array: | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | { | ||||||
|  |   id: "unique-employee-id",           // URL-friendly identifier (e.g., "john-doe") | ||||||
|  |   name: "John Doe",                   // Staff member's name | ||||||
|  |   title: "Senior Energy Consultant",  // Job title | ||||||
|  |   phone: "+60 12-345 6789",          // Formatted phone number | ||||||
|  |   whatsapp: "60123456789",           // WhatsApp number (digits only) | ||||||
|  |   email: "john.doe@rooftop.my",      // Email address | ||||||
|  |   linkedin: "https://linkedin.com/in/johndoe", // LinkedIn profile URL | ||||||
|  |   address: "Level 15, Menara 1 Sentral\nKuala Lumpur Sentral\n50470 Kuala Lumpur\nMalaysia", // Address with \n for line breaks | ||||||
|  |   website: "https://rooftop.my",     // Company website | ||||||
|  |   logoPath: "/logo.png",             // Path to company logo | ||||||
|  |   profilePic: "/profilepic.png"      // Path to profile picture | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### URL Structure | ||||||
|  | 
 | ||||||
|  | Each employee gets their own URL: | ||||||
|  | - **Homepage**: `card.rooftop.my/` - Shows all employees | ||||||
|  | - **Individual**: `card.rooftop.my/john-doe` - Shows John Doe's namecard | ||||||
|  | - **Direct sharing**: Share individual employee URLs for targeted contact | ||||||
|  | 
 | ||||||
|  | ### URL Query Parameter Overrides | ||||||
|  | 
 | ||||||
|  | You can temporarily override any field via URL parameters: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | https://yoursite.com/?name=Jane%20Smith&title=Energy%20Analyst&phone=%2B60123456789 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Available parameters: | ||||||
|  | - `name` - Staff name | ||||||
|  | - `title` - Job title | ||||||
|  | - `phone` - Phone number | ||||||
|  | - `wa` - WhatsApp number | ||||||
|  | - `email` - Email address | ||||||
|  | - `linkedin` - LinkedIn URL | ||||||
|  | - `address` - Address (use %0A for line breaks) | ||||||
|  | - `site` - Website URL | ||||||
|  | - `logo` - Logo path | ||||||
|  | 
 | ||||||
|  | ## 🎨 Branding | ||||||
|  | 
 | ||||||
|  | The application uses the Rooftop Energy brand colors: | ||||||
|  | - **Primary Accent**: `#fcd913` (Rooftop yellow) | ||||||
|  | - **Background**: Deep charcoal (`#0a0a0a`) | ||||||
|  | - **Card Surface**: Near-black (`#1a1a1a`) | ||||||
|  | - **Borders**: Muted indigo (`#2a2a3a`) | ||||||
|  | - **Text**: White and muted blue-gray (`#8b9bb4`) | ||||||
|  | 
 | ||||||
|  | ## 🚀 Getting Started | ||||||
|  | 
 | ||||||
|  | ### Prerequisites | ||||||
|  | 
 | ||||||
|  | - Node.js 18+  | ||||||
|  | - npm or yarn | ||||||
|  | 
 | ||||||
|  | ### Installation | ||||||
|  | 
 | ||||||
|  | 1. Clone the repository: | ||||||
|  | ```bash | ||||||
|  | git clone <your-repo-url> | ||||||
|  | cd rooftop-energy-namecard | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 2. Install dependencies: | ||||||
|  | ```bash | ||||||
|  | npm install | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 3. Replace the logo: | ||||||
|  |    - Place your Rooftop Energy logo in `public/logo.png` | ||||||
|  |    - Recommended: 120x120px transparent PNG or SVG | ||||||
|  | 
 | ||||||
|  | 4. Update staff details in `src/config/staff.ts` | ||||||
|  | 
 | ||||||
|  | ### Development | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | npm run dev | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Open [http://localhost:3000](http://localhost:3000) to view the application. | ||||||
|  | 
 | ||||||
|  | ### Building | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The built application will be in the `out/` directory. | ||||||
|  | 
 | ||||||
|  | ## 🌐 Deployment | ||||||
|  | 
 | ||||||
|  | ### VPS + Reverse Proxy (Nginx example) | ||||||
|  | 
 | ||||||
|  | 1. Build static files: | ||||||
|  | ```bash | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 2. Copy the `out/` directory to your server, e.g. `/var/www/namecard`: | ||||||
|  | ```bash | ||||||
|  | scp -r out/ user@server:/var/www/namecard | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 3. Configure Nginx: | ||||||
|  | ```nginx | ||||||
|  | server { | ||||||
|  |     listen 80; | ||||||
|  |     server_name example.com; # change to your domain | ||||||
|  | 
 | ||||||
|  |     # Optional subpath base (set BASE_PATH to match, e.g. "/namecard") | ||||||
|  |     # location /namecard/ { | ||||||
|  |     #   alias /var/www/namecard/; | ||||||
|  |     #   try_files $uri $uri/ /index.html; | ||||||
|  |     # } | ||||||
|  | 
 | ||||||
|  |     root /var/www/namecard; | ||||||
|  |     index index.html; | ||||||
|  | 
 | ||||||
|  |     location / { | ||||||
|  |         try_files $uri $uri/ /index.html; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 4. If you want to serve from a subpath (e.g. `/namecard`), set at build time: | ||||||
|  | ```bash | ||||||
|  | BASE_PATH=/namecard NEXT_PUBLIC_BASE_URL=https://example.com/namecard npm run build | ||||||
|  | ``` | ||||||
|  | Then deploy the `out/` directory to the path configured by Nginx. | ||||||
|  | 
 | ||||||
|  | 5. Reload Nginx: | ||||||
|  | ```bash | ||||||
|  | sudo nginx -t && sudo systemctl reload nginx | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 📱 Mobile Optimization | ||||||
|  | 
 | ||||||
|  | - Touch targets are ≥44px height | ||||||
|  | - Optimized for 360-414px viewport widths | ||||||
|  | - Responsive grid layouts (2×2 on mobile, 1×4 on desktop) | ||||||
|  | - Mobile-first design approach | ||||||
|  | 
 | ||||||
|  | ## ♿ Accessibility | ||||||
|  | 
 | ||||||
|  | - WCAG AA color contrast compliance | ||||||
|  | - Visible focus states | ||||||
|  | - Proper ARIA labels | ||||||
|  | - Semantic HTML structure | ||||||
|  | - Keyboard navigation support | ||||||
|  | 
 | ||||||
|  | ## 🔧 Customization | ||||||
|  | 
 | ||||||
|  | ### Adding Multiple Profiles | ||||||
|  | 
 | ||||||
|  | To support multiple staff members, you can: | ||||||
|  | 
 | ||||||
|  | 1. Create a profiles directory: `src/config/profiles/` | ||||||
|  | 2. Add individual profile files | ||||||
|  | 3. Modify the routing to support `/[username]` paths | ||||||
|  | 4. Update the main page to load profiles dynamically | ||||||
|  | 
 | ||||||
|  | ### Styling Changes | ||||||
|  | 
 | ||||||
|  | - Colors: Update `tailwind.config.js` | ||||||
|  | - Typography: Modify `src/app/globals.css` | ||||||
|  | - Layout: Edit `src/components/Namecard.tsx` | ||||||
|  | 
 | ||||||
|  | ## 📊 Performance | ||||||
|  | 
 | ||||||
|  | The application targets: | ||||||
|  | - **Lighthouse Performance**: ≥90 | ||||||
|  | - **Accessibility**: ≥95 | ||||||
|  | - **Best Practices**: ≥95 | ||||||
|  | - **SEO**: ≥90 | ||||||
|  | 
 | ||||||
|  | ## 🤝 Contributing | ||||||
|  | 
 | ||||||
|  | 1. Fork the repository | ||||||
|  | 2. Create a feature branch | ||||||
|  | 3. Make your changes | ||||||
|  | 4. Test thoroughly | ||||||
|  | 5. Submit a pull request | ||||||
|  | 
 | ||||||
|  | ## 📄 License | ||||||
|  | 
 | ||||||
|  | This project is proprietary to Rooftop Energy. | ||||||
|  | 
 | ||||||
|  | ## 🆘 Support | ||||||
|  | 
 | ||||||
|  | For technical support or questions about the application, please contact the development team. | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | **Note**: Remember to replace `public/logo.png` with the actual Rooftop Energy logo before deploying! | ||||||
							
								
								
									
										11
									
								
								env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								env.example
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | # Example environment variables for local development | ||||||
|  | # Copy this file to .env.local and update values as needed | ||||||
|  | 
 | ||||||
|  | # Base URL for the application (used for QR codes and social sharing) | ||||||
|  | NEXT_PUBLIC_BASE_URL=http://localhost:3000 | ||||||
|  | 
 | ||||||
|  | # Analytics (optional) | ||||||
|  | NEXT_PUBLIC_GA_ID=your-google-analytics-id | ||||||
|  | 
 | ||||||
|  | # Contact form (if adding contact forms later) | ||||||
|  | CONTACT_EMAIL=contact@rooftop.my | ||||||
							
								
								
									
										13
									
								
								next.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								next.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | /** @type {import('next').NextConfig} */ | ||||||
|  | const basePath = process.env.BASE_PATH || '' | ||||||
|  | 
 | ||||||
|  | const nextConfig = { | ||||||
|  |   output: 'export', | ||||||
|  |   trailingSlash: true, | ||||||
|  |   images: { | ||||||
|  |     unoptimized: true | ||||||
|  |   }, | ||||||
|  |   basePath | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = nextConfig | ||||||
							
								
								
									
										6048
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6048
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										40
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | { | ||||||
|  |   "name": "rooftop-energy-namecard", | ||||||
|  |   "version": "0.1.0", | ||||||
|  |   "private": true, | ||||||
|  |   "description": "Digital namecard application for Rooftop Energy staff", | ||||||
|  |   "keywords": ["namecard", "digital", "contact", "rooftop", "energy"], | ||||||
|  |   "author": "Rooftop Energy", | ||||||
|  |   "license": "UNLICENSED", | ||||||
|  |   "scripts": { | ||||||
|  |     "dev": "next dev", | ||||||
|  |     "build": "next build", | ||||||
|  |     "start": "next start", | ||||||
|  |     "lint": "next lint", | ||||||
|  |     "export": "next build", | ||||||
|  |     "type-check": "tsc --noEmit", | ||||||
|  |     "clean": "rm -rf .next out", | ||||||
|  |     "setup": "npm install && npm run build" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "next": "14.0.4", | ||||||
|  |     "react": "^18", | ||||||
|  |     "react-dom": "^18", | ||||||
|  |     "lucide-react": "^0.294.0" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@types/node": "^20", | ||||||
|  |     "@types/react": "^18", | ||||||
|  |     "@types/react-dom": "^18", | ||||||
|  |     "autoprefixer": "^10.0.1", | ||||||
|  |     "eslint": "^8", | ||||||
|  |     "eslint-config-next": "14.0.4", | ||||||
|  |     "postcss": "^8", | ||||||
|  |     "tailwindcss": "^3.3.0", | ||||||
|  |     "typescript": "^5" | ||||||
|  |   }, | ||||||
|  |   "engines": { | ||||||
|  |     "node": ">=18.0.0", | ||||||
|  |     "npm": ">=8.0.0" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | module.exports = { | ||||||
|  |   plugins: { | ||||||
|  |     tailwindcss: {}, | ||||||
|  |     autoprefixer: {}, | ||||||
|  |   }, | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								public/linkedin.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/linkedin.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 54 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/profilepic.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/profilepic.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/whatsapp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/whatsapp.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										38
									
								
								src/app/[employeeId]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/app/[employeeId]/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | import { Suspense } from 'react'; | ||||||
|  | import { notFound } from 'next/navigation'; | ||||||
|  | import Namecard from '@/components/Namecard'; | ||||||
|  | import { getEmployeeById } from '@/config/employees'; | ||||||
|  | 
 | ||||||
|  | interface PageProps { | ||||||
|  |   params: { | ||||||
|  |     employeeId: string; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function EmployeePage({ params }: PageProps) { | ||||||
|  |   const employee = getEmployeeById(params.employeeId); | ||||||
|  |    | ||||||
|  |   if (!employee) { | ||||||
|  |     notFound(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Suspense fallback={ | ||||||
|  |       <div className="min-h-screen bg-deep-charcoal flex items-center justify-center"> | ||||||
|  |         <div className="text-white text-lg">Loading...</div> | ||||||
|  |       </div> | ||||||
|  |     }> | ||||||
|  |       <Namecard profile={employee} /> | ||||||
|  |     </Suspense> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Generate static params for all employees
 | ||||||
|  | export async function generateStaticParams() { | ||||||
|  |   const { getAllEmployees } = await import('@/config/employees'); | ||||||
|  |   const employees = getAllEmployees(); | ||||||
|  |    | ||||||
|  |   return employees.map((employee) => ({ | ||||||
|  |     employeeId: employee.id, | ||||||
|  |   })); | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								src/app/globals.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/app/globals.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | @tailwind base; | ||||||
|  | @tailwind components; | ||||||
|  | @tailwind utilities; | ||||||
|  | 
 | ||||||
|  | @layer base { | ||||||
|  |   html { | ||||||
|  |     font-family: var(--font-exo-2), system-ui, sans-serif; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   body { | ||||||
|  |     @apply bg-deep-charcoal text-white; | ||||||
|  |     font-feature-settings: "kern" 1; | ||||||
|  |     text-rendering: optimizeLegibility; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @layer components { | ||||||
|  |   .btn-primary { | ||||||
|  |     @apply bg-rooftop-yellow text-deep-charcoal font-semibold px-6 py-3 rounded-xl  | ||||||
|  |            hover:scale-105 active:scale-95 transition-all duration-200 ease-out | ||||||
|  |            focus:outline-none focus:ring-2 focus:ring-rooftop-yellow focus:ring-offset-2 focus:ring-offset-deep-charcoal; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .btn-secondary { | ||||||
|  |     @apply bg-card-surface text-white border border-muted-border font-medium px-6 py-3 rounded-xl | ||||||
|  |            hover:bg-muted-border hover:scale-105 active:scale-95 transition-all duration-200 ease-out | ||||||
|  |            focus:outline-none focus:ring-2 focus:ring-muted-border focus:ring-offset-2 focus:ring-offset-deep-charcoal; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .card { | ||||||
|  |     @apply bg-card-surface border border-muted-border rounded-2xl shadow-xl; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								src/app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | import type { Metadata } from 'next' | ||||||
|  | import { Exo_2 } from 'next/font/google' | ||||||
|  | import './globals.css' | ||||||
|  | 
 | ||||||
|  | const exo2 = Exo_2({ | ||||||
|  |   subsets: ['latin'], | ||||||
|  |   weight: ['300', '400', '600', '700'], | ||||||
|  |   variable: '--font-exo-2', | ||||||
|  |   display: 'swap', | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export const metadata: Metadata = { | ||||||
|  |   title: 'Rooftop Energy - Digital Namecard', | ||||||
|  |   description: 'Connect with our energy consultants. Call, email, WhatsApp, or download contact details.', | ||||||
|  |   keywords: 'Rooftop Energy, energy consultant, Malaysia, renewable energy, solar', | ||||||
|  |   authors: [{ name: 'Rooftop Energy' }], | ||||||
|  |   metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'), | ||||||
|  |   openGraph: { | ||||||
|  |     title: 'Rooftop Energy - Digital Namecard', | ||||||
|  |     description: 'Connect with our energy consultants. Call, email, WhatsApp, or download contact details.', | ||||||
|  |     url: 'https://rooftop.my', | ||||||
|  |     siteName: 'Rooftop Energy', | ||||||
|  |     images: [ | ||||||
|  |       { | ||||||
|  |         url: '/logo.png', | ||||||
|  |         width: 1200, | ||||||
|  |         height: 630, | ||||||
|  |         alt: 'Rooftop Energy Logo', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     locale: 'en_MY', | ||||||
|  |     type: 'website', | ||||||
|  |   }, | ||||||
|  |   twitter: { | ||||||
|  |     card: 'summary_large_image', | ||||||
|  |     title: 'Rooftop Energy - Digital Namecard', | ||||||
|  |     description: 'Connect with our energy consultants. Call, email, WhatsApp, or download contact details.', | ||||||
|  |     images: ['/logo.png'], | ||||||
|  |   }, | ||||||
|  |   robots: { | ||||||
|  |     index: true, | ||||||
|  |     follow: true, | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function RootLayout({ | ||||||
|  |   children, | ||||||
|  | }: { | ||||||
|  |   children: React.ReactNode | ||||||
|  | }) { | ||||||
|  |   return ( | ||||||
|  |     <html lang="en" className={exo2.variable}> | ||||||
|  |       <head> | ||||||
|  |         {/* Favicon and touch icons can be added here when available */} | ||||||
|  |       </head> | ||||||
|  |       <body className={`${exo2.variable} font-exo antialiased`}> | ||||||
|  |         {children} | ||||||
|  |       </body> | ||||||
|  |     </html> | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								src/app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | import Link from 'next/link'; | ||||||
|  | 
 | ||||||
|  | export default function NotFound() { | ||||||
|  |   return ( | ||||||
|  |     <div className="min-h-screen bg-deep-charcoal flex items-center justify-center p-4"> | ||||||
|  |       <div className="text-center max-w-md"> | ||||||
|  |         <div className="mb-8"> | ||||||
|  |           <h1 className="text-6xl font-bold text-rooftop-yellow mb-4">404</h1> | ||||||
|  |           <h2 className="text-2xl font-semibold text-white mb-4"> | ||||||
|  |             Employee Not Found | ||||||
|  |           </h2> | ||||||
|  |           <p className="text-muted-text mb-8"> | ||||||
|  |             The employee you're looking for doesn't exist or has been moved. | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div className="space-y-4"> | ||||||
|  |           <Link | ||||||
|  |             href="/" | ||||||
|  |             className="inline-block w-full px-6 py-3 bg-rooftop-yellow text-deep-charcoal font-semibold rounded-xl hover:bg-yellow-400 transition-colors" | ||||||
|  |           > | ||||||
|  |             View All Employees | ||||||
|  |           </Link> | ||||||
|  |            | ||||||
|  |           <p className="text-sm text-muted-text"> | ||||||
|  |             Or check the URL and try again. | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								src/app/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/app/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | import Link from 'next/link'; | ||||||
|  | import { getAllEmployees } from '@/config/employees'; | ||||||
|  | 
 | ||||||
|  | export default function HomePage() { | ||||||
|  |   const employees = getAllEmployees(); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="min-h-screen bg-deep-charcoal p-4 sm:p-6 lg:p-8"> | ||||||
|  |       <div className="max-w-4xl mx-auto"> | ||||||
|  |         {/* Header */} | ||||||
|  |         <div className="text-center mb-12"> | ||||||
|  |           <h1 className="text-4xl font-bold text-white mb-4"> | ||||||
|  |             Rooftop Energy Team | ||||||
|  |           </h1> | ||||||
|  |           <p className="text-xl text-muted-text"> | ||||||
|  |             Connect with our energy consultants and specialists | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {/* Employee Grid */} | ||||||
|  |         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | ||||||
|  |           {employees.map((employee) => ( | ||||||
|  |             <Link | ||||||
|  |               key={employee.id} | ||||||
|  |               href={`/${employee.id}`} | ||||||
|  |               className="card p-6 hover:scale-105 transition-transform duration-200 cursor-pointer group" | ||||||
|  |             > | ||||||
|  |               <div className="text-center"> | ||||||
|  |                 {/* Profile Picture */} | ||||||
|  |                 <img  | ||||||
|  |                   src={employee.profilePic}  | ||||||
|  |                   alt={employee.name}  | ||||||
|  |                   className="w-20 h-20 mx-auto mb-4 rounded-full object-cover border-2 border-muted-text group-hover:border-rooftop-yellow transition-colors" | ||||||
|  |                 /> | ||||||
|  |                  | ||||||
|  |                 {/* Name and Title */} | ||||||
|  |                 <h2 className="text-xl font-semibold text-white mb-2 group-hover:text-rooftop-yellow transition-colors"> | ||||||
|  |                   {employee.name} | ||||||
|  |                 </h2> | ||||||
|  |                 <p className="text-muted-text mb-4"> | ||||||
|  |                   {employee.title} | ||||||
|  |                 </p> | ||||||
|  |                  | ||||||
|  |                 {/* Contact Info Preview */} | ||||||
|  |                 <div className="space-y-2 text-sm"> | ||||||
|  |                   <div className="flex items-center justify-center gap-2 text-muted-text"> | ||||||
|  |                     <span>📧</span> | ||||||
|  |                     <span className="truncate">{employee.email}</span> | ||||||
|  |                   </div> | ||||||
|  |                   <div className="flex items-center justify-center gap-2 text-muted-text"> | ||||||
|  |                     <span>📱</span> | ||||||
|  |                     <span>{employee.phone}</span> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |                  | ||||||
|  |                 {/* View Card Button */} | ||||||
|  |                 <div className="mt-4"> | ||||||
|  |                   <span className="inline-block px-4 py-2 bg-rooftop-yellow text-deep-charcoal font-medium rounded-lg text-sm group-hover:bg-yellow-400 transition-colors"> | ||||||
|  |                     View Namecard | ||||||
|  |                   </span> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </Link> | ||||||
|  |           ))} | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {/* Footer */} | ||||||
|  |         <div className="text-center mt-12 text-muted-text text-sm"> | ||||||
|  |           <p>© 2024 Rooftop Energy. All rights reserved.</p> | ||||||
|  |           <p className="mt-1">Powered by sustainable technology</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										240
									
								
								src/components/Namecard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/components/Namecard.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,240 @@ | |||||||
|  | 'use client'; | ||||||
|  | 
 | ||||||
|  | import { useState, useEffect } from 'react'; | ||||||
|  | import { Phone, Mail, Globe, Copy, Download, Check } from 'lucide-react'; | ||||||
|  | import { StaffProfile } from '@/config/staff'; | ||||||
|  | 
 | ||||||
|  | interface NamecardProps { | ||||||
|  |   profile: StaffProfile; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function Namecard({ profile }: NamecardProps) { | ||||||
|  |   const [copySuccess, setCopySuccess] = useState(false); | ||||||
|  |   const [showFullAddress, setShowFullAddress] = useState(false); | ||||||
|  |   const [currentProfile, setCurrentProfile] = useState<StaffProfile>(profile); | ||||||
|  | 
 | ||||||
|  |   // Handle URL query parameters on client side
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (typeof window !== 'undefined') { | ||||||
|  |       const urlParams = new URLSearchParams(window.location.search); | ||||||
|  |       const overrides: Partial<StaffProfile> = {}; | ||||||
|  |        | ||||||
|  |       if (urlParams.get('name')) overrides.name = urlParams.get('name')!; | ||||||
|  |       if (urlParams.get('title')) overrides.title = urlParams.get('title')!; | ||||||
|  |       if (urlParams.get('phone')) overrides.phone = urlParams.get('phone')!; | ||||||
|  |       if (urlParams.get('wa')) overrides.whatsapp = urlParams.get('wa')!; | ||||||
|  |       if (urlParams.get('email')) overrides.email = urlParams.get('email')!; | ||||||
|  |       if (urlParams.get('linkedin')) overrides.linkedin = urlParams.get('linkedin')!; | ||||||
|  |       if (urlParams.get('address')) overrides.address = urlParams.get('address')!; | ||||||
|  |       if (urlParams.get('site')) overrides.website = urlParams.get('site')!; | ||||||
|  |       if (urlParams.get('logo')) overrides.logoPath = urlParams.get('logo')!; | ||||||
|  |        | ||||||
|  |       if (Object.keys(overrides).length > 0) { | ||||||
|  |         setCurrentProfile({ ...profile, ...overrides }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, [profile]); | ||||||
|  | 
 | ||||||
|  |   const handleCopyDetails = async () => { | ||||||
|  |     const details = `${currentProfile.name} | ||||||
|  | ${currentProfile.title} | ||||||
|  | Rooftop Energy | ||||||
|  | ${currentProfile.phone} | ||||||
|  | ${currentProfile.email} | ||||||
|  | ${currentProfile.website} | ||||||
|  | ${currentProfile.address}`;
 | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       await navigator.clipboard.writeText(details); | ||||||
|  |       setCopySuccess(true); | ||||||
|  |       setTimeout(() => setCopySuccess(false), 2000); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error('Failed to copy: ', err); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const generateVCard = () => { | ||||||
|  |     const vcard = `BEGIN:VCARD
 | ||||||
|  | VERSION:3.0 | ||||||
|  | FN:${currentProfile.name} | ||||||
|  | N:${currentProfile.name.split(' ').reverse().join(';')};;; | ||||||
|  | ORG:Rooftop Energy | ||||||
|  | TITLE:${currentProfile.title} | ||||||
|  | TEL;TYPE=CELL:${currentProfile.phone.replace(/\s+/g, '')} | ||||||
|  | EMAIL;TYPE=INTERNET:${currentProfile.email} | ||||||
|  | URL:${currentProfile.website} | ||||||
|  | ADR;TYPE=WORK:${currentProfile.address.replace(/\n/g, ';')} | ||||||
|  | END:VCARD`;
 | ||||||
|  | 
 | ||||||
|  |     const blob = new Blob([vcard], { type: 'text/vcard' }); | ||||||
|  |     const url = URL.createObjectURL(blob); | ||||||
|  |     const link = document.createElement('a'); | ||||||
|  |     link.href = url; | ||||||
|  |     link.download = `${currentProfile.name.replace(/\s+/g, '_')}_RooftopEnergy.vcf`; | ||||||
|  |     document.body.appendChild(link); | ||||||
|  |     link.click(); | ||||||
|  |     document.body.removeChild(link); | ||||||
|  |     URL.revokeObjectURL(url); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const addressLines = currentProfile.address.split('\n'); | ||||||
|  |   const displayAddress = showFullAddress ? addressLines : addressLines.slice(0, 2); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="min-h-screen bg-deep-charcoal p-4 sm:p-6 lg:p-8"> | ||||||
|  |       <div className="max-w-md mx-auto"> | ||||||
|  |                  {/* Header */} | ||||||
|  |          <div className="card p-6 mb-6 text-center"> | ||||||
|  |            <div className="mb-6"> | ||||||
|  |              <img  | ||||||
|  |                src={currentProfile.logoPath}  | ||||||
|  |                alt="Rooftop Energy"  | ||||||
|  |                className="w-24 h-auto mx-auto mb-4" | ||||||
|  |              /> | ||||||
|  |               | ||||||
|  |                            {/* Profile Picture */} | ||||||
|  |               <img  | ||||||
|  |                 src={currentProfile.profilePic || "/profilepic.png"}  | ||||||
|  |                 alt="Profile Picture"  | ||||||
|  |                 className="w-24 h-24 mx-auto mb-4 rounded-full object-cover border-2 border-muted-text" | ||||||
|  |               /> | ||||||
|  |               | ||||||
|  |              <h1 className="text-2xl font-bold tracking-tight text-white mb-2"> | ||||||
|  |                {currentProfile.name} | ||||||
|  |              </h1> | ||||||
|  |              <p className="text-lg text-muted-text font-light"> | ||||||
|  |                {currentProfile.title} | ||||||
|  |              </p> | ||||||
|  |            </div> | ||||||
|  |            | ||||||
|  |           {/* Website button - only show on larger screens */} | ||||||
|  |           <div className="hidden sm:block mb-6"> | ||||||
|  |             <a  | ||||||
|  |               href={currentProfile.website} | ||||||
|  |               target="_blank" | ||||||
|  |               rel="noopener noreferrer" | ||||||
|  |               className="inline-flex items-center gap-2 text-rooftop-yellow hover:text-yellow-400 transition-colors" | ||||||
|  |             > | ||||||
|  |               <Globe size={16} /> | ||||||
|  |               <span className="text-sm font-medium">Visit Website</span> | ||||||
|  |             </a> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {/* CTA Grid */} | ||||||
|  |         <div className="card p-6 mb-6"> | ||||||
|  |           <div className="grid grid-cols-2 gap-3"> | ||||||
|  |             <a  | ||||||
|  |               href={`tel:${currentProfile.phone}`} | ||||||
|  |               className="btn-primary flex flex-col items-center gap-2 py-4" | ||||||
|  |             > | ||||||
|  |               <Phone size={20} /> | ||||||
|  |               <span className="text-sm font-semibold">Call</span> | ||||||
|  |             </a> | ||||||
|  |              | ||||||
|  |             <a  | ||||||
|  |               href={`mailto:${currentProfile.email}?subject=Hello ${currentProfile.name.split(' ')[0]} — via Rooftop Energy`} | ||||||
|  |               className="btn-primary flex flex-col items-center gap-2 py-4" | ||||||
|  |             > | ||||||
|  |               <Mail size={20} /> | ||||||
|  |               <span className="text-sm font-semibold">Email</span> | ||||||
|  |             </a> | ||||||
|  |              | ||||||
|  |             <a  | ||||||
|  |               href={`https://wa.me/${currentProfile.whatsapp}?text=Hello ${currentProfile.name.split(' ')[0]}, I'd like to discuss energy solutions with Rooftop Energy.`} | ||||||
|  |               target="_blank" | ||||||
|  |               rel="noopener noreferrer" | ||||||
|  |               className="btn-primary flex flex-col items-center gap-2 py-4" | ||||||
|  |             > | ||||||
|  |               <img src="/whatsapp.png" alt="WhatsApp" className="w-5 h-5" /> | ||||||
|  |               <span className="text-sm font-semibold">WhatsApp</span> | ||||||
|  |             </a> | ||||||
|  |              | ||||||
|  |             {currentProfile.linkedin && ( | ||||||
|  |               <a  | ||||||
|  |                 href={currentProfile.linkedin} | ||||||
|  |                 target="_blank" | ||||||
|  |                 rel="noopener noreferrer" | ||||||
|  |                 className="btn-primary flex flex-col items-center gap-2 py-4" | ||||||
|  |               > | ||||||
|  |                 <img src="/linkedin.png" alt="LinkedIn" className="w-5 h-auto" /> | ||||||
|  |                 <span className="text-sm font-semibold">LinkedIn</span> | ||||||
|  |               </a> | ||||||
|  |             )} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {/* Details */} | ||||||
|  |         <div className="card p-6 mb-6"> | ||||||
|  |           <h2 className="text-lg font-semibold text-white mb-4">Contact Details</h2> | ||||||
|  |           <div className="space-y-3 text-sm"> | ||||||
|  |             <div> | ||||||
|  |               <span className="text-muted-text">Phone:</span> | ||||||
|  |               <span className="text-white ml-2">{currentProfile.phone}</span> | ||||||
|  |             </div> | ||||||
|  |             <div> | ||||||
|  |               <span className="text-muted-text">Email:</span> | ||||||
|  |               <span className="text-white ml-2">{currentProfile.email}</span> | ||||||
|  |             </div> | ||||||
|  |             <div> | ||||||
|  |               <span className="text-muted-text">Website:</span> | ||||||
|  |               <span className="text-white ml-2">{currentProfile.website}</span> | ||||||
|  |             </div> | ||||||
|  |             <div> | ||||||
|  |               <span className="text-muted-text">Address:</span> | ||||||
|  |               <div className="text-white ml-2 mt-1"> | ||||||
|  |                 {displayAddress.map((line, index) => ( | ||||||
|  |                   <div key={index}>{line}</div> | ||||||
|  |                 ))} | ||||||
|  |                 {addressLines.length > 2 && ( | ||||||
|  |                   <button | ||||||
|  |                     onClick={() => setShowFullAddress(!showFullAddress)} | ||||||
|  |                     className="text-rooftop-yellow hover:text-yellow-400 text-xs mt-1 transition-colors" | ||||||
|  |                   > | ||||||
|  |                     {showFullAddress ? 'Show less' : 'Show more'} | ||||||
|  |                   </button> | ||||||
|  |                 )} | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {/* Action Row */} | ||||||
|  |         <div className="card p-6 mb-6"> | ||||||
|  |           <div className="grid grid-cols-2 gap-3"> | ||||||
|  |             <button | ||||||
|  |               onClick={handleCopyDetails} | ||||||
|  |               className="btn-secondary flex items-center justify-center gap-2 py-4" | ||||||
|  |             > | ||||||
|  |               {copySuccess ? ( | ||||||
|  |                 <> | ||||||
|  |                   <Check size={18} className="text-green-400" /> | ||||||
|  |                   <span className="text-sm">Copied!</span> | ||||||
|  |                 </> | ||||||
|  |               ) : ( | ||||||
|  |                 <> | ||||||
|  |                   <Copy size={18} /> | ||||||
|  |                   <span className="text-sm">Copy Details</span> | ||||||
|  |                 </> | ||||||
|  |               )} | ||||||
|  |             </button> | ||||||
|  |              | ||||||
|  |             <button | ||||||
|  |               onClick={generateVCard} | ||||||
|  |               className="btn-secondary flex items-center justify-center gap-2 py-4" | ||||||
|  |             > | ||||||
|  |               <Download size={18} /> | ||||||
|  |               <span className="text-sm">Save vCard</span> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {/* Footer */} | ||||||
|  |         <div className="text-center text-muted-text text-xs"> | ||||||
|  |           <p>© 2024 Rooftop Energy. All rights reserved.</p> | ||||||
|  |           <p className="mt-1">Powered by sustainable technology</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								src/config/employees.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/config/employees.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | import { StaffProfile } from './staff'; | ||||||
|  | 
 | ||||||
|  | export interface Employee extends StaffProfile { | ||||||
|  |   id: string; | ||||||
|  |   profilePic: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Import the YAML data (you'll need to install js-yaml if you want to parse YAML files)
 | ||||||
|  | // For now, we'll define the data directly in TypeScript
 | ||||||
|  | export const employees: Employee[] = [ | ||||||
|  |   { | ||||||
|  |     id: "john-doe", | ||||||
|  |     name: "John Doe", | ||||||
|  |     title: "Senior Energy Consultant", | ||||||
|  |     phone: "+60 12-345 6789", | ||||||
|  |     whatsapp: "60123456789", | ||||||
|  |     email: "john.doe@rooftop.my", | ||||||
|  |     linkedin: "https://linkedin.com/in/johndoe", | ||||||
|  |     address: "Level 15, Menara 1 Sentral\nKuala Lumpur Sentral\n50470 Kuala Lumpur\nMalaysia", | ||||||
|  |     website: "https://rooftop.my", | ||||||
|  |     logoPath: "/logo.png", | ||||||
|  |     profilePic: "/profilepic.png" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: "jane-smith", | ||||||
|  |     name: "Jane Smith", | ||||||
|  |     title: "Energy Analyst", | ||||||
|  |     phone: "+60 12-345 6790", | ||||||
|  |     whatsapp: "60123456790", | ||||||
|  |     email: "jane.smith@rooftop.my", | ||||||
|  |     linkedin: "https://linkedin.com/in/janesmith", | ||||||
|  |     address: "Level 15, Menara 1 Sentral\nKuala Lumpur Sentral\n50470 Kuala Lumpur\nMalaysia", | ||||||
|  |     website: "https://rooftop.my", | ||||||
|  |     logoPath: "/logo.png", | ||||||
|  |     profilePic: "/profilepic.png" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: "ahmad-ali", | ||||||
|  |     name: "Ahmad Ali", | ||||||
|  |     title: "Renewable Energy Specialist", | ||||||
|  |     phone: "+60 12-345 6791", | ||||||
|  |     whatsapp: "60123456791", | ||||||
|  |     email: "ahmad.ali@rooftop.my", | ||||||
|  |     linkedin: "https://linkedin.com/in/ahmadali", | ||||||
|  |     address: "Level 15, Menara 1 Sentral\nKuala Lumpur Sentral\n50470 Kuala Lumpur\nMalaysia", | ||||||
|  |     website: "https://rooftop.my", | ||||||
|  |     logoPath: "/logo.png", | ||||||
|  |     profilePic: "/profilepic.png" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: "sarah-wong", | ||||||
|  |     name: "Sarah Wong", | ||||||
|  |     title: "Solar Energy Engineer", | ||||||
|  |     phone: "+60 12-345 6792", | ||||||
|  |     whatsapp: "60123456792", | ||||||
|  |     email: "sarah.wong@rooftop.my", | ||||||
|  |     linkedin: "https://linkedin.com/in/sarahwong", | ||||||
|  |     address: "Level 15, Menara 1 Sentral\nKuala Lumpur Sentral\n50470 Kuala Lumpur\nMalaysia", | ||||||
|  |     website: "https://rooftop.my", | ||||||
|  |     logoPath: "/logo.png", | ||||||
|  |     profilePic: "/profilepic.png" | ||||||
|  |   } | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | export function getEmployeeById(id: string): Employee | undefined { | ||||||
|  |   return employees.find(emp => emp.id === id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getAllEmployees(): Employee[] { | ||||||
|  |   return employees; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default employees; | ||||||
							
								
								
									
										26
									
								
								src/config/staff.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/config/staff.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | export interface StaffProfile { | ||||||
|  |   name: string; | ||||||
|  |   title: string; | ||||||
|  |   phone: string; | ||||||
|  |   whatsapp: string; // digits only
 | ||||||
|  |   email: string; | ||||||
|  |   linkedin: string; // full URL
 | ||||||
|  |   address: string; // supports \n for line breaks
 | ||||||
|  |   website: string; | ||||||
|  |   logoPath: string; | ||||||
|  |   profilePic?: string; // optional profile picture path
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const staffProfile: StaffProfile = { | ||||||
|  |   name: "John Doe", | ||||||
|  |   title: "Senior Energy Consultant", | ||||||
|  |   phone: "+60 12-345 6789", | ||||||
|  |   whatsapp: "60123456789", | ||||||
|  |   email: "john.doe@rooftop.my", | ||||||
|  |   linkedin: "https://linkedin.com/in/johndoe", | ||||||
|  |   address: "Level 15, Menara 1 Sentral\nKuala Lumpur Sentral\n50470 Kuala Lumpur\nMalaysia", | ||||||
|  |   website: "https://rooftop.my", | ||||||
|  |   logoPath: "/logo.png" | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default staffProfile; | ||||||
							
								
								
									
										38
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | /** @type {import('tailwindcss').Config} */ | ||||||
|  | module.exports = { | ||||||
|  |   content: [ | ||||||
|  |     './src/pages/**/*.{js,ts,jsx,tsx,mdx}', | ||||||
|  |     './src/components/**/*.{js,ts,jsx,tsx,mdx}', | ||||||
|  |     './src/app/**/*.{js,ts,jsx,tsx,mdx}', | ||||||
|  |   ], | ||||||
|  |   theme: { | ||||||
|  |     extend: { | ||||||
|  |       colors: { | ||||||
|  |         'rooftop-yellow': '#fcd913', | ||||||
|  |         'deep-charcoal': '#0a0a0a', | ||||||
|  |         'near-black': '#111111', | ||||||
|  |         'card-surface': '#1a1a1a', | ||||||
|  |         'muted-border': '#2a2a3a', | ||||||
|  |         'muted-text': '#8b9bb4', | ||||||
|  |       }, | ||||||
|  |       fontFamily: { | ||||||
|  |         'exo': ['var(--font-exo-2)', 'sans-serif'], | ||||||
|  |       }, | ||||||
|  |       animation: { | ||||||
|  |         'fade-in': 'fadeIn 0.2s ease-out', | ||||||
|  |         'scale-in': 'scaleIn 0.15s ease-out', | ||||||
|  |       }, | ||||||
|  |       keyframes: { | ||||||
|  |         fadeIn: { | ||||||
|  |           '0%': { opacity: '0' }, | ||||||
|  |           '100%': { opacity: '1' }, | ||||||
|  |         }, | ||||||
|  |         scaleIn: { | ||||||
|  |           '0%': { transform: 'scale(0.95)' }, | ||||||
|  |           '100%': { transform: 'scale(1)' }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   plugins: [], | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "target": "es5", | ||||||
|  |     "lib": ["dom", "dom.iterable", "es6"], | ||||||
|  |     "allowJs": true, | ||||||
|  |     "skipLibCheck": true, | ||||||
|  |     "strict": true, | ||||||
|  |     "noEmit": true, | ||||||
|  |     "esModuleInterop": true, | ||||||
|  |     "module": "esnext", | ||||||
|  |     "moduleResolution": "bundler", | ||||||
|  |     "resolveJsonModule": true, | ||||||
|  |     "isolatedModules": true, | ||||||
|  |     "jsx": "preserve", | ||||||
|  |     "incremental": true, | ||||||
|  |     "plugins": [ | ||||||
|  |       { | ||||||
|  |         "name": "next" | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": ["./src/*"] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | ||||||
|  |   "exclude": ["node_modules"] | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user