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