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