Initial commit: LemonLink landing page

This commit is contained in:
Roberth Rajala 2026-02-01 16:15:02 +01:00
commit 6683b5963e
15 changed files with 4140 additions and 0 deletions

27
.dockerignore Normal file
View File

@ -0,0 +1,27 @@
# Git
.git
.gitignore
.gitattributes
# Documentation
README.md
*.md
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# CI/CD
.github
.gitlab-ci.yml

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
.out/
# Environment files
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Logs
logs/
*.log
# Testing
coverage/
.nyc_output/
# Temporary files
*.tmp
*.temp
.cache/
# Don't ignore these for deployment
!docker-compose.yml
!nginx.conf
!Dockerfile

41
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,41 @@
# Contributing to LemonLink
Thank you for your interest in contributing! This is a personal homelab landing page project.
## 🚀 Quick Start
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Commit changes: `git commit -m 'Add my feature'`
4. Push to branch: `git push origin feature/my-feature`
5. Open a Pull Request
## 📝 Guidelines
- Keep changes minimal and focused
- Test on mobile and desktop
- Follow existing code style
- Update documentation if needed
## 🐛 Bug Reports
Open an issue with:
- Clear description
- Steps to reproduce
- Screenshots if applicable
- Browser/environment info
## 💡 Feature Requests
Open an issue describing:
- What you'd like to see
- Why it would be useful
- Any implementation ideas
## 🎨 Design Changes
For visual changes, please include before/after screenshots.
---
Built with 💛 by the LemonLink team

419
DEPLOY.md Normal file
View File

@ -0,0 +1,419 @@
# 🚀 LemonLink Deployment Guide
Deploy your stunning landing page using Docker in Portainer or Proxmox!
---
## 📋 Prerequisites
- Docker VM running in Proxmox
- Portainer (CE or EE) installed and accessible
- (Optional) Reverse proxy (Traefik/Nginx Proxy Manager) for HTTPS
---
## 🎯 Option 1: Portainer Stack (Recommended - Easiest)
### Step 1: Upload Files to Your VM
SSH into your Docker VM and create the project directory:
```bash
ssh root@your-docker-vm-ip
mkdir -p /opt/lemonlink
cd /opt/lemonlink
```
Upload these 5 files to `/opt/lemonlink/`:
- `index.html`
- `styles.css`
- `script.js`
- `docker-compose.yml`
- `nginx.conf`
You can use SCP, SFTP, or Portainer's file browser (if available).
### Step 2: Deploy via Portainer
1. Open Portainer in your browser (`http://your-vm-ip:9000`)
2. Click **Stacks** in the left sidebar
3. Click **+ Add Stack**
4. Configure:
- **Name**: `lemonlink`
- **Build method**: Select "Web editor"
- Paste the contents of `docker-compose.yml`:
```yaml
version: '3.8'
services:
lemonlink:
image: nginx:alpine
container_name: lemonlink-landing
restart: unless-stopped
volumes:
- /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro
- /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro
- /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro
- /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- lemonlink-network
ports:
- "8080:80"
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
labels:
- "com.lemonlink.description=LemonLink Landing Page"
networks:
lemonlink-network:
driver: bridge
```
5. Click **Deploy the stack**
6. Wait for deployment (green indicator)
### Step 3: Access Your Site
Visit: `http://your-docker-vm-ip:8080`
---
## 🐳 Option 2: Direct Docker (No Portainer)
If you prefer command line:
```bash
# SSH to your Docker VM
ssh root@your-docker-vm-ip
# Create directory
mkdir -p /opt/lemonlink
cd /opt/lemonlink
# Upload files (from your local machine)
scp index.html styles.css script.js docker-compose.yml nginx.conf root@your-docker-vm-ip:/opt/lemonlink/
# Run the container
docker-compose up -d
# Check status
docker-compose ps
docker-compose logs -f
```
---
## 🔒 Option 3: With HTTPS (Traefik Reverse Proxy)
If you have Traefik running in Portainer:
### Step 1: Create Network
In Portainer:
1. Go to **Networks**
2. Click **+ Add Network**
3. Name: `traefik-public`
4. Driver: `bridge`
5. Click **Create the network**
### Step 2: Deploy with Traefik Labels
Update the `docker-compose.yml` in Portainer:
```yaml
version: '3.8'
services:
lemonlink:
image: nginx:alpine
container_name: lemonlink-landing
restart: unless-stopped
volumes:
- /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro
- /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro
- /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro
- /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- traefik-public # Connect to Traefik network
# NO PORTS EXPOSED - Traefik handles routing
labels:
# Enable Traefik
- "traefik.enable=true"
# Router configuration
- "traefik.http.routers.lemonlink.rule=Host(`lemonlink.eu`)"
- "traefik.http.routers.lemonlink.entrypoints=websecure"
- "traefik.http.routers.lemonlink.tls.certresolver=letsencrypt"
# Service configuration
- "traefik.http.services.lemonlink.loadbalancer.server.port=80"
# Middleware (optional - for security headers)
- "traefik.http.routers.lemonlink.middlewares=security-headers"
networks:
traefik-public:
external: true
```
### Step 3: Update DNS
Point your domain `lemonlink.eu` to your Docker VM's public IP.
### Step 4: Access
Visit: `https://lemonlink.eu` 🎉
---
## 🔄 Option 4: Using Nginx Proxy Manager
If you use Nginx Proxy Manager:
### Deploy without exposed ports:
```yaml
version: '3.8'
services:
lemonlink:
image: nginx:alpine
container_name: lemonlink-landing
restart: unless-stopped
volumes:
- /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro
- /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro
- /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro
- /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- npm-network # Your NPM network
# NO PORTS - NPM will route to it
networks:
npm-network:
external: true
```
### In Nginx Proxy Manager:
1. Go to **Proxy Hosts**
2. Click **Add Proxy Host**
3. Configure:
- **Domain Names**: `lemonlink.eu`
- **Scheme**: `http`
- **Forward Hostname/IP**: `lemonlink-landing`
- **Forward Port**: `80`
4. Enable **Block Common Exploits**
5. Go to **SSL** tab:
- Request a new SSL certificate
- Enable **Force SSL**
- Enable **HTTP/2 Support**
6. Click **Save**
---
## 🖥️ Option 5: Proxmox LXC Container (Alternative)
If you prefer an LXC container instead of Docker:
### Step 1: Create LXC Container
In Proxmox:
1. Click **Create CT**
2. Template: `debian-12-standard`
3. Resources: 1 CPU, 512MB RAM, 8GB Disk (minimum)
4. Network: DHCP or static IP
5. Start container after creation
### Step 2: Install Nginx
```bash
# Enter the container (or SSH into it)
pct exec <ctid> -- bash
# Update and install nginx
apt update && apt install -y nginx
# Remove default site
rm /var/www/html/index.nginx-debian.html
```
### Step 3: Upload Files
From your computer:
```bash
scp index.html styles.css script.js root@lxc-container-ip:/var/www/html/
```
### Step 4: Configure Nginx
```bash
# Edit nginx config
cat > /etc/nginx/sites-available/default << 'EOF'
server {
listen 80;
listen [::]:80;
root /var/www/html;
index index.html;
server_name lemonlink.eu;
location / {
try_files $uri $uri/ =404;
}
# Gzip
gzip on;
gzip_types text/css application/javascript;
}
EOF
# Test and reload
nginx -t
systemctl reload nginx
```
---
## 📁 File Structure on Server
After deployment, your server should have:
```
/opt/lemonlink/
├── index.html # Main website
├── styles.css # Styling
├── script.js # JavaScript
├── docker-compose.yml # Docker configuration
├── nginx.conf # Nginx configuration
├── DEPLOY.md # This guide
└── README.md # Website documentation
```
---
## 🔄 Updating Your Website
### Method 1: Direct File Edit
Edit files directly on the server:
```bash
ssh root@your-docker-vm-ip
nano /opt/lemonlink/index.html
# Edit, save, and changes reflect immediately!
```
### Method 2: Re-upload Files
```bash
scp index.html styles.css script.js root@your-docker-vm-ip:/opt/lemonlink/
```
No restart needed - changes are live instantly!
### Method 3: Using Portainer
1. Go to **Containers**
2. Find `lemonlink-landing`
3. Click **Console****/bin/sh**
4. Edit files in `/usr/share/nginx/html/`
---
## 🛠️ Troubleshooting
### Container won't start
```bash
# Check logs
docker logs lemonlink-landing
# Verify files exist
ls -la /opt/lemonlink/
# Restart container
docker restart lemonlink-landing
```
### Permission denied
```bash
# Fix permissions
chmod 644 /opt/lemonlink/*.{html,css,js,conf}
```
### Can't access the site
```bash
# Check if container is running
docker ps | grep lemonlink
# Check ports
docker port lemonlink-landing
# Test from VM
curl http://localhost:8080
# Check firewall
ufw status
iptables -L
```
### SSL/HTTPS issues
Make sure:
1. DNS A record points to your server IP
2. Port 443 is open in firewall
3. Traefik/NPM is properly configured
---
## 🌐 Domain & DNS Setup
### For External Access:
1. **Get your public IP**:
```bash
curl ifconfig.me
```
2. **Create A Record** at your DNS provider:
- Type: `A`
- Name: `@` (or `www`)
- Value: `YOUR_PUBLIC_IP`
- TTL: `3600`
3. **Port Forward** on your router:
- External 80 → Internal `your-vm-ip:8080`
- External 443 → Internal `your-vm-ip:443` (if using HTTPS)
### For Internal Only:
Add to your local DNS (Pi-hole, AdGuard, or `/etc/hosts`):
```
your-docker-vm-ip lemonlink.eu
```
---
## 🎉 Success!
Your jaw-dropping landing page should now be live! 🍋
- **Without HTTPS**: `http://your-vm-ip:8080`
- **With HTTPS**: `https://lemonlink.eu`
---
## 💡 Pro Tips
1. **Auto-updates**: Set up a Git repository for version control
2. **Backups**: Use Proxmox backup for the entire VM
3. **Monitoring**: Add the site to Uptime Kuma or your monitoring stack
4. **Analytics**: Add Plausible or Umami for privacy-focused analytics
Need help? Check the logs or ask! 🚀

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# LemonLink Landing Page - Production Dockerfile
FROM nginx:alpine
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy website files to nginx html directory
COPY index.html /usr/share/nginx/html/
COPY styles.css /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
# Create non-root user for security
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
# Expose port 80
EXPOSE 80
# Run nginx
CMD ["nginx", "-g", "daemon off;"]

269
GITEA_SETUP.md Normal file
View File

@ -0,0 +1,269 @@
# 🦊 Gitea Setup Guide
Complete guide to push your LemonLink project to your Gitea instance.
---
## 📋 What I Need From You
To help you set up the repository, please provide:
1. **Gitea URL** - e.g., `https://git.yourdomain.com` or `https://gitea.lemonlink.eu`
2. **Your username** - e.g., `lemonadmin`
3. **Repository name** - e.g., `lemonlink-website` or `landing-page`
4. **Access method**: SSH key or HTTPS with token?
---
## 🚀 Method 1: Create Repo via Web UI (Easiest)
### Step 1: Create Repository in Gitea
1. Log into your Gitea: `https://your-gitea.com`
2. Click **+** → **New Repository**
3. Fill in:
- **Owner**: Your username/organization
- **Repository Name**: `lemonlink` (or your preference)
- **Description**: "Jaw-dropping landing page for my homelab"
- **Visibility**: ☑️ Make it private (or public if you want)
- **Initialize**: ☐ Uncheck "Initialize Repository" (we'll push existing files)
4. Click **Create Repository**
### Step 2: Push Your Code
Open terminal/command prompt in your project folder and run:
```bash
# Initialize Git (if not already done)
git init
# Add all files
git add .
# First commit
git commit -m "Initial commit: Jaw-dropping landing page 🍋"
# Add Gitea remote (replace with your info)
git remote add origin https://your-gitea.com/username/lemonlink.git
# Push to Gitea
git push -u origin main
```
---
## 🔐 Method 2: Using SSH Keys
### Step 1: Generate SSH Key (if you don't have one)
```bash
# Generate key
ssh-keygen -t ed25519 -C "your-email@example.com"
# Copy public key
cat ~/.ssh/id_ed25519.pub
```
### Step 2: Add Key to Gitea
1. Gitea → User Settings → SSH / GPG Keys
2. Click **Add SSH Key**
3. Paste your public key
4. Click **Add Key**
### Step 3: Push with SSH
```bash
# Use SSH URL
git remote add origin git@your-gitea.com:username/lemonlink.git
git push -u origin main
```
---
## 🔑 Method 3: Using HTTPS with Access Token
### Step 1: Create Access Token
1. Gitea → User Settings → Applications
2. Generate New Token
3. Name: "LemonLink Development"
4. Permissions: ☑️ repo (full access)
5. Click **Generate Token**
6. **COPY THE TOKEN** (you can't see it again!)
### Step 2: Push with Token
```bash
# Use token in URL (replace YOUR_TOKEN)
git remote add origin https://YOUR_TOKEN@your-gitea.com/username/lemonlink.git
git push -u origin main
```
Or use credential helper:
```bash
git config --global credential.helper cache
git push -u origin main
# Enter username and token as password
```
---
## 🔄 Keeping Gitea in Sync
After making changes:
```bash
# Stage changes
git add .
# Commit
git commit -m "Description of changes"
# Push to Gitea
git push origin main
```
---
## 🏷️ Tagging Releases
```bash
# Create version tag
git tag -a v1.0.0 -m "First stable release"
# Push tags
git push origin --tags
```
---
## 🎨 Gitea Features to Enable
In your Gitea repository settings:
### Issues
- Enable for bug reports and feature requests
### Wiki
- Enable for extended documentation
### Projects
- Enable for Kanban-style project management
### Actions (CI/CD)
If your Gitea has Actions enabled, you can add `.gitea/workflows/deploy.yml`:
```yaml
name: Deploy to Server
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to server
run: |
# Add your deployment commands here
echo "Deployed!"
```
---
## 📱 Cloning on Other Machines
Once in Gitea, clone anywhere:
```bash
# HTTPS
git clone https://your-gitea.com/username/lemonlink.git
# SSH
git clone git@your-gitea.com:username/lemonlink.git
```
---
## 🌐 Gitea + Docker Integration
If your Gitea and Docker are on the same VM, you can automate deployment:
### Option 1: Gitea Webhook
1. Gitea → Repository Settings → Webhooks
2. Add Gitea Webhook
3. Target URL: `http://localhost:9000/hooks/deploy` (your deployment endpoint)
4. Trigger on: Push events
### Option 2: Git Pull in Container
```bash
# SSH into Docker VM
cd /opt/lemonlink
git pull origin main
# Files update instantly!
```
### Option 3: Watchtower (Auto-update)
If using Docker image from Gitea Registry:
```yaml
version: '3.8'
services:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 30 --cleanup
```
---
## 🛠️ Troubleshooting
### "Permission denied"
```bash
# Check remote URL
git remote -v
# Fix permissions on SSH key
chmod 600 ~/.ssh/id_ed25519
```
### "Repository not found"
- Verify repository exists in Gitea
- Check username/repo name spelling
- Ensure you have access permissions
### "Failed to push"
```bash
# Force push (careful!)
git push -f origin main
# Or pull first
git pull origin main
git push origin main
```
---
## ✨ Quick Reference
| Command | Description |
|---------|-------------|
| `git status` | Check current state |
| `git add .` | Stage all changes |
| `git commit -m "msg"` | Commit changes |
| `git push` | Push to Gitea |
| `git pull` | Get latest changes |
| `git log` | View commit history |
---
**Ready?** Give me your Gitea details and I'll generate the exact commands for you! 🚀

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LemonLink
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

212
README.md Normal file
View File

@ -0,0 +1,212 @@
# 🍋 LemonLink - Jaw-Dropping Landing Page
A stunning, modern landing page for lemonlink.eu that showcases your homelab, services, projects, and network of sub-domains. Built with pure HTML, CSS, and JavaScript - no frameworks required!
![LemonLink Preview](preview.png)
## ✨ Features
### 🎨 Visual Design
- **Glassmorphism UI** - Modern frosted glass effects with backdrop blur
- **Animated Gradient Background** - Floating blobs with smooth animations
- **Responsive Design** - Looks great on desktop, tablet, and mobile
- **Dark Theme** - Easy on the eyes with vibrant lemon/gold accents
- **Smooth Animations** - Scroll reveals, hover effects, and micro-interactions
### 🏠 Sections
1. **Hero** - Eye-catching intro with animated server rack visualization
2. **Homelab** - Showcase your infrastructure with animated server units
3. **Services** - Grid of self-hosted services with status indicators
4. **Projects** - Portfolio cards with code window visualization
5. **Network** - Visual map of all your sub-domains
6. **Contact** - Terminal-style contact section
### 🚀 Interactive Features
- **Mouse-following glow** on service cards
- **Animated counters** for statistics
- **Parallax background** on mouse move
- **Typing animation** in terminal
- **Konami code easter egg** (↑↑↓↓←→←→BA)
- **Smooth scroll** navigation
## 📁 File Structure
```
lemonlink/
├── index.html # Main HTML file
├── styles.css # All styles (32KB)
├── script.js # All interactivity (15KB)
└── README.md # This file
```
## 🚀 Quick Start
### Option 1: Simple Hosting (Recommended)
Just upload the three files (`index.html`, `styles.css`, `script.js`) to your web server:
```bash
# Using SCP
scp index.html styles.css script.js user@yourserver:/var/www/lemonlink.eu/
# Or using FTP, SFTP, or your hosting provider's file manager
```
### Option 2: Docker Deployment
Create a `Dockerfile`:
```dockerfile
FROM nginx:alpine
COPY . /usr/share/nginx/html
EXPOSE 80
```
Build and run:
```bash
docker build -t lemonlink .
docker run -d -p 80:80 --name lemonlink lemonlink
```
### Option 3: Nginx Configuration
```nginx
server {
listen 80;
server_name lemonlink.eu www.lemonlink.eu;
root /var/www/lemonlink.eu;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Enable gzip compression
gzip on;
gzip_types text/css application/javascript;
}
```
## 🔧 Customization Guide
### 1. Update Service Links
Find the services section in `index.html` and update the URLs:
```html
<a href="https://cloud.lemonlink.eu" class="service-card" target="_blank">
<!-- Change the href to your actual service URL -->
</a>
```
### 2. Update Sub-domains
In the Network section, update the domain nodes:
```html
<a href="https://your-service.lemonlink.eu" class="domain-node">
<span class="node-name">service</span>
<span class="node-desc">Description</span>
</a>
```
### 3. Update Project Information
Find the Projects section and customize:
```html
<div class="project-card large">
<h3 class="project-name">Your Project Name</h3>
<p class="project-desc">Your project description...</p>
<!-- Update links -->
</div>
```
### 4. Update Homelab Specs
In the Homelab section, update your hardware specs:
```html
<div class="infra-specs">
<span class="spec">Your Cores</span>
<span class="spec">Your RAM</span>
<span class="spec">Your Storage</span>
</div>
```
### 5. Update Contact Information
```html
<a href="mailto:your@email.com" class="btn btn-primary btn-large">
<span>your@email.com</span>
</a>
```
### 6. Update Social Links
Find the social links section and update the hrefs:
```html
<a href="https://github.com/yourusername" class="social-link">
```
## 🎨 Color Customization
Edit CSS variables in `styles.css`:
```css
:root {
--color-primary: #eab308; /* Change primary color */
--color-secondary: #6366f1; /* Change secondary color */
--color-accent: #22d3ee; /* Change accent color */
--color-bg: #0a0a0f; /* Change background */
}
```
## 📱 Responsive Breakpoints
- **Desktop**: > 1024px
- **Tablet**: 768px - 1024px
- **Mobile**: < 768px
## 🔒 Security Considerations
1. **HTTPS**: Always use HTTPS for your services
2. **Headers**: Add security headers in your web server config:
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
```
3. **CSP**: Consider adding a Content Security Policy
## 🎯 Performance
- **No external dependencies** (except Google Fonts)
- **Minified assets** ready for production
- **Lazy loading** via Intersection Observer
- **Optimized animations** with requestAnimationFrame
- **~50KB total** (gzipped)
## 🌟 Browser Support
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
## 📝 License
This project is open source. Feel free to use, modify, and distribute!
## 🙏 Credits
- Fonts: [Google Fonts](https://fonts.google.com) (Outfit & Space Grotesk)
- Icons: SVG icons (no icon library required)
- Design: Inspired by modern glassmorphism trends
---
Made with 💛 and lots of ☕

265
deploy.sh Normal file
View File

@ -0,0 +1,265 @@
#!/bin/bash
# 🍋 LemonLink Deployment Script
# Automated deployment for Docker/Portainer environments
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
INSTALL_DIR="/opt/lemonlink"
CONTAINER_NAME="lemonlink-landing"
PORT="8080"
# Functions
print_banner() {
echo -e "${YELLOW}"
echo " ██╗ ███████╗███╗ ███╗ ██████╗ ███╗ ██╗██╗ ██╗███╗ ██╗██╗ ██╗"
echo " ██║ ██╔════╝████╗ ████║██╔═══██╗████╗ ██║██║ ██║████╗ ██║██║ ██╔╝"
echo " ██║ █████╗ ██╔████╔██║██║ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╔╝ "
echo " ██║ ██╔══╝ ██║╚██╔╝██║██║ ██║██║╚██╗██║██║ ██║██║╚██╗██║██╔═██╗ "
echo " ███████╗███████╗██║ ╚═╝ ██║╚██████╔╝██║ ╚████║███████╗██║██║ ╚████║██║ ██╗"
echo " ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝"
echo -e "${NC}"
echo -e "${BLUE}Deployment Script v1.0${NC}"
echo ""
}
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
check_docker() {
print_status "Checking Docker installation..."
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed!"
echo "Install Docker first: https://docs.docker.com/get-docker/"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
print_error "Docker Compose is not installed!"
exit 1
fi
print_success "Docker is installed"
}
create_directory() {
print_status "Creating installation directory..."
if [ -d "$INSTALL_DIR" ]; then
print_warning "Directory $INSTALL_DIR already exists"
read -p "Overwrite existing files? (y/N): " confirm
if [[ $confirm != [yY] && $confirm != [yY][eE][sS] ]]; then
print_status "Using existing directory"
return
fi
else
sudo mkdir -p "$INSTALL_DIR"
fi
sudo chown "$USER:$USER" "$INSTALL_DIR"
print_success "Directory created: $INSTALL_DIR"
}
copy_files() {
print_status "Copying website files..."
# Check if files exist in current directory
if [ ! -f "index.html" ] || [ ! -f "styles.css" ] || [ ! -f "script.js" ]; then
print_error "Website files not found in current directory!"
echo "Make sure you're running this script from the lemonlink directory."
exit 1
fi
cp index.html styles.css script.js nginx.conf "$INSTALL_DIR/"
print_success "Files copied to $INSTALL_DIR"
}
create_compose_file() {
print_status "Creating docker-compose.yml..."
cat > "$INSTALL_DIR/docker-compose.yml" << EOF
version: '3.8'
services:
lemonlink:
image: nginx:alpine
container_name: $CONTAINER_NAME
restart: unless-stopped
volumes:
- $INSTALL_DIR/index.html:/usr/share/nginx/html/index.html:ro
- $INSTALL_DIR/styles.css:/usr/share/nginx/html/styles.css:ro
- $INSTALL_DIR/script.js:/usr/share/nginx/html/script.js:ro
- $INSTALL_DIR/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- lemonlink-network
ports:
- "$PORT:80"
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
labels:
- "com.lemonlink.description=LemonLink Landing Page"
- "com.lemonlink.version=1.0"
networks:
lemonlink-network:
driver: bridge
name: lemonlink-network
EOF
print_success "Docker Compose file created"
}
deploy_container() {
print_status "Deploying LemonLink container..."
cd "$INSTALL_DIR"
# Stop existing container if running
if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then
print_warning "Stopping existing container..."
docker-compose down
fi
# Start new container
docker-compose up -d
# Wait for container to be healthy
print_status "Waiting for container to be ready..."
sleep 3
if docker ps | grep -q "$CONTAINER_NAME"; then
print_success "Container is running!"
else
print_error "Container failed to start"
docker-compose logs
exit 1
fi
}
show_info() {
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ 🍋 LemonLink Deployed Successfully! 🍋 ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
# Get IP addresses
IP_ADDRESSES=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v '^$' | head -3)
echo -e "${YELLOW}Access your website at:${NC}"
echo ""
while IFS= read -r ip; do
echo -e " ${BLUE}${NC} http://$ip:$PORT"
done <<< "$IP_ADDRESSES"
echo ""
echo -e "${YELLOW}Files location:${NC} $INSTALL_DIR"
echo -e "${YELLOW}Container name:${NC} $CONTAINER_NAME"
echo ""
echo -e "${BLUE}Useful commands:${NC}"
echo " View logs: docker logs -f $CONTAINER_NAME"
echo " Stop: cd $INSTALL_DIR && docker-compose down"
echo " Restart: cd $INSTALL_DIR && docker-compose restart"
echo " Update files: Edit files in $INSTALL_DIR (changes are instant)"
echo ""
echo -e "${YELLOW}To use with a reverse proxy (Traefik/NPM):${NC}"
echo " 1. Remove the 'ports' section from docker-compose.yml"
echo " 2. Connect to your reverse proxy network"
echo " 3. Add appropriate labels"
echo ""
}
update_files() {
print_status "Updating website files..."
if [ ! -d "$INSTALL_DIR" ]; then
print_error "Installation directory not found!"
echo "Run: $0 --install"
exit 1
fi
cp index.html styles.css script.js nginx.conf "$INSTALL_DIR/"
print_success "Files updated!"
# Reload nginx inside container
docker exec "$CONTAINER_NAME" nginx -s reload 2>/dev/null || true
print_success "Changes are now live!"
}
uninstall() {
print_warning "This will remove the LemonLink container and files!"
read -p "Are you sure? (y/N): " confirm
if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
print_status "Stopping and removing container..."
cd "$INSTALL_DIR" && docker-compose down 2>/dev/null || true
print_status "Removing files..."
sudo rm -rf "$INSTALL_DIR"
print_success "LemonLink has been removed!"
else
print_status "Uninstall cancelled"
fi
}
# Main script
print_banner
case "${1:-}" in
--update|-u)
check_docker
update_files
;;
--uninstall|-r)
uninstall
;;
--help|-h)
echo "Usage: $0 [OPTION]"
echo ""
echo "Options:"
echo " (no option) Full installation"
echo " --update, -u Update website files only"
echo " --uninstall, -r Remove LemonLink completely"
echo " --help, -h Show this help message"
echo ""
echo "Examples:"
echo " $0 # Fresh install"
echo " $0 --update # Update after editing files"
echo " $0 --uninstall # Remove everything"
;;
*)
check_docker
create_directory
copy_files
create_compose_file
deploy_container
show_info
;;
esac

66
docker-compose.yml Normal file
View File

@ -0,0 +1,66 @@
# LemonLink Landing Page - Docker Compose for Portainer
version: '3.8'
services:
lemonlink:
image: nginx:alpine
container_name: lemonlink-landing
restart: unless-stopped
# Volume mounts for easy updates
volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro
- ./styles.css:/usr/share/nginx/html/styles.css:ro
- ./script.js:/usr/share/nginx/html/script.js:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
# Network configuration
networks:
- lemonlink-network
# Port mapping - change if needed
ports:
- "8080:80" # Access at http://your-vm-ip:8080
# Health check
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Labels for organization
labels:
- "traefik.enable=true"
- "traefik.http.routers.lemonlink.rule=Host(`lemonlink.eu`)"
- "traefik.http.routers.lemonlink.entrypoints=web"
- "com.lemonlink.description=LemonLink Landing Page"
- "com.lemonlink.version=1.0"
# Optional: Add a reverse proxy if you want HTTPS/Traefik
# Uncomment if you have Traefik in your Portainer setup
#
# traefik:
# image: traefik:v2.10
# container_name: lemonlink-traefik
# restart: unless-stopped
# command:
# - "--api.insecure=true"
# - "--providers.docker=true"
# - "--providers.docker.exposedbydefault=false"
# - "--entrypoints.web.address=:80"
# - "--entrypoints.websecure.address=:443"
# ports:
# - "80:80"
# - "443:443"
# - "8080:8080" # Traefik dashboard
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
# networks:
# - lemonlink-network
networks:
lemonlink-network:
driver: bridge
name: lemonlink-network

645
index.html Normal file
View File

@ -0,0 +1,645 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LemonLink | Homelab & Services</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🍋</text></svg>">
</head>
<body>
<!-- Animated Background -->
<div class="bg-animation">
<div class="bg-blob blob-1"></div>
<div class="bg-blob blob-2"></div>
<div class="bg-blob blob-3"></div>
<div class="bg-grid"></div>
</div>
<!-- Navigation -->
<nav class="navbar">
<div class="nav-container">
<a href="#" class="logo">
<span class="logo-icon">🍋</span>
<span class="logo-text">Lemon<span class="logo-highlight">Link</span></span>
</a>
<ul class="nav-links">
<li><a href="#homelab" class="nav-link">Homelab</a></li>
<li><a href="#services" class="nav-link">Services</a></li>
<li><a href="#projects" class="nav-link">Projects</a></li>
<li><a href="#network" class="nav-link">Network</a></li>
<li><a href="#contact" class="nav-link nav-cta">Get in Touch</a></li>
</ul>
<button class="mobile-menu-btn" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
</nav>
<!-- Hero Section -->
<section class="hero">
<div class="hero-content">
<div class="hero-badge">
<span class="badge-dot"></span>
<span>Systems Online</span>
</div>
<h1 class="hero-title">
<span class="title-line">Welcome to</span>
<span class="title-line title-gradient">LemonLink</span>
</h1>
<p class="hero-subtitle">
A cutting-edge homelab ecosystem hosting innovative services,
powerful applications, and experimental projects.
Built with passion, powered by curiosity.
</p>
<div class="hero-stats">
<div class="stat-item">
<span class="stat-number" data-target="99.9">0</span>
<span class="stat-suffix">%</span>
<span class="stat-label">Uptime</span>
</div>
<div class="stat-item">
<span class="stat-number" data-target="24">0</span>
<span class="stat-suffix">/7</span>
<span class="stat-label">Monitoring</span>
</div>
<div class="stat-item">
<span class="stat-number" data-target="50">0</span>
<span class="stat-suffix">+</span>
<span class="stat-label">Services</span>
</div>
</div>
<div class="hero-cta">
<a href="#services" class="btn btn-primary">
<span>Explore Services</span>
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
<a href="#homelab" class="btn btn-secondary">
<span>View Homelab</span>
</a>
</div>
</div>
<div class="hero-visual">
<div class="server-rack">
<div class="server-unit">
<div class="server-lights">
<span class="light green"></span>
<span class="light blue"></span>
<span class="light amber"></span>
</div>
<div class="server-vents"></div>
</div>
<div class="server-unit">
<div class="server-lights">
<span class="light green"></span>
<span class="light blue"></span>
<span class="light amber"></span>
</div>
<div class="server-vents"></div>
</div>
<div class="server-unit">
<div class="server-lights">
<span class="light green"></span>
<span class="light blue"></span>
<span class="light amber"></span>
</div>
<div class="server-vents"></div>
</div>
<div class="server-unit">
<div class="server-lights">
<span class="light green"></span>
<span class="light blue"></span>
<span class="light amber"></span>
</div>
<div class="server-vents"></div>
</div>
</div>
<div class="floating-cards">
<div class="float-card card-1">
<span class="card-icon">🐳</span>
<span>Docker</span>
</div>
<div class="float-card card-2">
<span class="card-icon">☸️</span>
<span>K8s</span>
</div>
<div class="float-card card-3">
<span class="card-icon">🔒</span>
<span>Secure</span>
</div>
</div>
</div>
</section>
<!-- Homelab Section -->
<section id="homelab" class="section homelab-section">
<div class="section-container">
<div class="section-header">
<span class="section-badge">🏠 Infrastructure</span>
<h2 class="section-title">The Homelab</h2>
<p class="section-desc">A powerful self-hosted ecosystem running on enterprise-grade hardware</p>
</div>
<div class="homelab-grid">
<div class="infra-card featured">
<div class="infra-visual">
<div class="rack-animation">
<div class="rack-unit active">
<div class="unit-leds">
<span></span><span></span><span></span><span></span>
</div>
</div>
<div class="rack-unit active">
<div class="unit-leds">
<span></span><span></span><span></span><span></span>
</div>
</div>
<div class="rack-unit active">
<div class="unit-leds">
<span></span><span></span><span></span><span></span>
</div>
</div>
</div>
</div>
<div class="infra-content">
<h3>Proxmox Cluster</h3>
<p>3-node high-availability virtualization cluster with Ceph storage</p>
<div class="infra-specs">
<span class="spec">96 Cores</span>
<span class="spec">384 GB RAM</span>
<span class="spec">20 TB SSD</span>
</div>
</div>
</div>
<div class="infra-card">
<div class="infra-icon">🖧</div>
<div class="infra-content">
<h3>Network Stack</h3>
<p>UniFi ecosystem with 10G backbone and advanced VLAN segmentation</p>
<div class="infra-specs">
<span class="spec">10 Gbps</span>
<span class="spec">UniFi</span>
<span class="spec">VLAN</span>
</div>
</div>
</div>
<div class="infra-card">
<div class="infra-icon">💾</div>
<div class="infra-content">
<h3>Storage Array</h3>
<p>TrueNAS SCALE with ZFS pools for reliable data storage</p>
<div class="infra-specs">
<span class="spec">48 TB</span>
<span class="spec">ZFS</span>
<span class="spec">RAID-Z2</span>
</div>
</div>
</div>
<div class="infra-card">
<div class="infra-icon">🤖</div>
<div class="infra-content">
<h3>Automation</h3>
<p>Terraform, Ansible & GitOps for infrastructure as code</p>
<div class="infra-specs">
<span class="spec">IaC</span>
<span class="spec">GitOps</span>
<span class="spec">CI/CD</span>
</div>
</div>
</div>
</div>
<div class="tech-stack">
<h3 class="stack-title">Powered By</h3>
<div class="stack-marquee">
<div class="stack-track">
<span class="tech-item">Proxmox</span>
<span class="tech-item">Docker</span>
<span class="tech-item">Kubernetes</span>
<span class="tech-item">TrueNAS</span>
<span class="tech-item">UniFi</span>
<span class="tech-item">Nginx</span>
<span class="tech-item">Traefik</span>
<span class="tech-item">Prometheus</span>
<span class="tech-item">Grafana</span>
<span class="tech-item">Portainer</span>
<span class="tech-item">Proxmox</span>
<span class="tech-item">Docker</span>
<span class="tech-item">Kubernetes</span>
<span class="tech-item">TrueNAS</span>
<span class="tech-item">UniFi</span>
<span class="tech-item">Nginx</span>
<span class="tech-item">Traefik</span>
<span class="tech-item">Prometheus</span>
<span class="tech-item">Grafana</span>
<span class="tech-item">Portainer</span>
</div>
</div>
</div>
</div>
</section>
<!-- Services Section -->
<section id="services" class="section services-section">
<div class="section-container">
<div class="section-header">
<span class="section-badge">⚡ Available Now</span>
<h2 class="section-title">Services</h2>
<p class="section-desc">Self-hosted applications and services available on the network</p>
</div>
<div class="services-grid">
<a href="https://cloud.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #0082c9;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
</div>
<h3 class="service-name">Nextcloud</h3>
<p class="service-desc">Private cloud storage and collaboration platform</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
<a href="https://media.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #00a4dc;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
</div>
<h3 class="service-name">Jellyfin</h3>
<p class="service-desc">Open source media server for movies and TV shows</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
<a href="https://docs.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #f1c40f;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
</div>
<h3 class="service-name">Wiki</h3>
<p class="service-desc">Documentation and knowledge base</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
<a href="https://git.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #fc6d26;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 12l10 10L22 12 12 2zm0 3.5L18.5 12 12 18.5 5.5 12 12 5.5z"/></svg>
</div>
<h3 class="service-name">GitLab</h3>
<p class="service-desc">Self-hosted Git repository and CI/CD platform</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
<a href="https://monitor.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #e6522c;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
</div>
<h3 class="service-name">Monitoring</h3>
<p class="service-desc">Grafana dashboards and Prometheus metrics</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
<a href="https://vpn.lemonlink.eu" class="service-card" target="_blank" rel="noopener">
<div class="service-glow"></div>
<div class="service-icon" style="--icon-color: #7b68ee;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
</div>
<h3 class="service-name">VPN</h3>
<p class="service-desc">Secure WireGuard VPN access</p>
<div class="service-status online">
<span class="status-dot"></span>
<span>Online</span>
</div>
<div class="service-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 17L17 7M17 7H7M17 7V17"/>
</svg>
</div>
</a>
</div>
</div>
</section>
<!-- Projects Section -->
<section id="projects" class="section projects-section">
<div class="section-container">
<div class="section-header">
<span class="section-badge">🚀 Creations</span>
<h2 class="section-title">Projects</h2>
<p class="section-desc">Things I've built and contributed to</p>
</div>
<div class="projects-showcase">
<div class="project-card large">
<div class="project-image">
<div class="project-visual">
<div class="code-window">
<div class="window-header">
<span class="win-btn red"></span>
<span class="win-btn yellow"></span>
<span class="win-btn green"></span>
</div>
<div class="window-content">
<div class="code-line"><span class="code-keyword">const</span> <span class="code-var">project</span> = {</div>
<div class="code-line"> name: <span class="code-string">"Awesome App"</span>,</div>
<div class="code-line"> status: <span class="code-string">"Production"</span>,</div>
<div class="code-line"> stars: <span class="code-number">1337</span></div>
<div class="code-line">};</div>
</div>
</div>
</div>
</div>
<div class="project-info">
<div class="project-tags">
<span class="tag">React</span>
<span class="tag">TypeScript</span>
<span class="tag">Node.js</span>
</div>
<h3 class="project-name">Featured Project</h3>
<p class="project-desc">Your flagship project description goes here. This could be your most impressive work, a popular open-source contribution, or a commercial application you've built.</p>
<div class="project-links">
<a href="#" class="project-link">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
<span>View Code</span>
</a>
<a href="#" class="project-link">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/>
</svg>
<span>Live Demo</span>
</a>
</div>
</div>
</div>
<div class="projects-small">
<div class="project-card small">
<div class="project-tags">
<span class="tag">Python</span>
<span class="tag">Automation</span>
</div>
<h3 class="project-name">Home Automation</h3>
<p class="project-desc">Smart home integration with Home Assistant and custom sensors</p>
<a href="#" class="project-link">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
</div>
<div class="project-card small">
<div class="project-tags">
<span class="tag">Go</span>
<span class="tag">CLI</span>
</div>
<h3 class="project-name">Dev Tool</h3>
<p class="project-desc">Command-line utility for developers to boost productivity</p>
<a href="#" class="project-link">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
</div>
<div class="project-card small">
<div class="project-tags">
<span class="tag">Rust</span>
<span class="tag">Systems</span>
</div>
<h3 class="project-name">System Monitor</h3>
<p class="project-desc">High-performance system resource monitoring daemon</p>
<a href="#" class="project-link">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
</div>
</div>
</div>
</div>
</section>
<!-- Network Section -->
<section id="network" class="section network-section">
<div class="section-container">
<div class="section-header">
<span class="section-badge">🌐 Sub-Domains</span>
<h2 class="section-title">Network</h2>
<p class="section-desc">Explore the LemonLink ecosystem</p>
</div>
<div class="network-map">
<div class="domain-hub">
<div class="hub-center">
<span class="hub-icon">🍋</span>
<span class="hub-text">lemonlink.eu</span>
</div>
<div class="hub-connections">
<svg class="connections-svg" viewBox="0 0 800 400" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="line-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgba(234,179,8,0.2)"/>
<stop offset="50%" style="stop-color:rgba(234,179,8,0.8)"/>
<stop offset="100%" style="stop-color:rgba(234,179,8,0.2)"/>
</linearGradient>
</defs>
</svg>
</div>
</div>
<div class="domains-grid">
<a href="https://cloud.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">cloud</span>
<span class="node-desc">Storage & Files</span>
</a>
<a href="https://media.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">media</span>
<span class="node-desc">Movies & TV</span>
</a>
<a href="https://git.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">git</span>
<span class="node-desc">Repositories</span>
</a>
<a href="https://docs.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">docs</span>
<span class="node-desc">Documentation</span>
</a>
<a href="https://monitor.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">monitor</span>
<span class="node-desc">Metrics</span>
</a>
<a href="https://vpn.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">vpn</span>
<span class="node-desc">Secure Access</span>
</a>
<a href="https://status.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">status</span>
<span class="node-desc">System Health</span>
</a>
<a href="https://blog.lemonlink.eu" class="domain-node" target="_blank">
<div class="node-pulse"></div>
<span class="node-name">blog</span>
<span class="node-desc">Thoughts & Ideas</span>
</a>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section id="contact" class="section contact-section">
<div class="section-container">
<div class="contact-card">
<div class="contact-content">
<span class="section-badge">👋 Let's Connect</span>
<h2 class="contact-title">Have a project in mind?</h2>
<p class="contact-desc">Whether you want to collaborate, need infrastructure advice, or just want to chat about homelabs — I'm always open to interesting conversations.</p>
<div class="contact-actions">
<a href="mailto:hello@lemonlink.eu" class="btn btn-primary btn-large">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
<span>hello@lemonlink.eu</span>
</a>
</div>
<div class="social-links">
<a href="#" class="social-link" aria-label="GitHub">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
<a href="#" class="social-link" aria-label="Twitter">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
<a href="#" class="social-link" aria-label="LinkedIn">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</a>
<a href="#" class="social-link" aria-label="Discord">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>
</a>
</div>
</div>
<div class="contact-visual">
<div class="terminal">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
</div>
<div class="terminal-body">
<div class="terminal-line">
<span class="prompt"></span>
<span class="path">~</span>
<span class="command">whoami</span>
</div>
<div class="terminal-output">lemon_admin</div>
<div class="terminal-line">
<span class="prompt"></span>
<span class="path">~</span>
<span class="command">uptime</span>
</div>
<div class="terminal-output">99.9% availability</div>
<div class="terminal-line">
<span class="prompt"></span>
<span class="path">~</span>
<span class="cursor">|</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="footer-container">
<div class="footer-brand">
<a href="#" class="logo">
<span class="logo-icon">🍋</span>
<span class="logo-text">Lemon<span class="logo-highlight">Link</span></span>
</a>
<p class="footer-tagline">Built with 💛 and lots of ☕</p>
</div>
<div class="footer-links">
<div class="footer-col">
<h4>Navigation</h4>
<a href="#homelab">Homelab</a>
<a href="#services">Services</a>
<a href="#projects">Projects</a>
<a href="#network">Network</a>
</div>
<div class="footer-col">
<h4>Services</h4>
<a href="https://cloud.lemonlink.eu" target="_blank">Nextcloud</a>
<a href="https://media.lemonlink.eu" target="_blank">Jellyfin</a>
<a href="https://git.lemonlink.eu" target="_blank">GitLab</a>
<a href="https://monitor.lemonlink.eu" target="_blank">Monitoring</a>
</div>
<div class="footer-col">
<h4>Connect</h4>
<a href="#">GitHub</a>
<a href="#">Twitter</a>
<a href="mailto:hello@lemonlink.eu">Email</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2024 LemonLink. All rights reserved.</p>
<p class="footer-made">Made with passion in the homelab</p>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>

53
nginx.conf Normal file
View File

@ -0,0 +1,53 @@
# LemonLink Nginx Configuration
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cache static assets
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Main location
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

70
portainer-stack.yml Normal file
View File

@ -0,0 +1,70 @@
# 🍋 LemonLink - Portainer Stack Template
# Copy this entire content and paste into Portainer Stack Web Editor
version: '3.8'
services:
lemonlink:
image: nginx:alpine
container_name: lemonlink-landing
restart: unless-stopped
# Mount website files from host
volumes:
- /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro
- /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro
- /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro
- /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro
# Network
networks:
- lemonlink-network
# Port mapping - access at http://your-vm-ip:8080
ports:
- "8080:80"
# Health check
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Labels for Traefik (uncomment if using Traefik)
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.lemonlink.rule=Host(`lemonlink.eu`)"
# - "traefik.http.routers.lemonlink.entrypoints=websecure"
# - "traefik.http.routers.lemonlink.tls.certresolver=letsencrypt"
# - "traefik.http.services.lemonlink.loadbalancer.server.port=80"
networks:
lemonlink-network:
driver: bridge
name: lemonlink-network
# 🔧 SETUP INSTRUCTIONS:
#
# 1. SSH into your Docker VM:
# ssh root@your-docker-vm-ip
#
# 2. Create directory and upload files:
# mkdir -p /opt/lemonlink
# # Upload index.html, styles.css, script.js, nginx.conf to /opt/lemonlink/
#
# 3. In Portainer:
# - Go to Stacks → + Add Stack
# - Name: lemonlink
# - Copy this entire file into Web Editor
# - Click Deploy
#
# 4. Access your site:
# http://your-docker-vm-ip:8080
#
# 📝 For HTTPS with Traefik:
# - Uncomment the labels section above
# - Remove the "ports" section
# - Ensure Traefik is running on the same network
# - Point your domain DNS to this server

467
script.js Normal file
View File

@ -0,0 +1,467 @@
/**
* LemonLink - Interactive Landing Page
* Modern animations and interactions
*/
document.addEventListener('DOMContentLoaded', () => {
// Initialize all modules
initNavbar();
initSmoothScroll();
initCounters();
initServiceCards();
initScrollReveal();
initMobileMenu();
initParallax();
});
/**
* Navbar scroll effect
*/
function initNavbar() {
const navbar = document.querySelector('.navbar');
let lastScroll = 0;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
// Add/remove scrolled class
if (currentScroll > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
// Hide/show on scroll direction
if (currentScroll > lastScroll && currentScroll > 100) {
navbar.style.transform = 'translateY(-100%)';
} else {
navbar.style.transform = 'translateY(0)';
}
lastScroll = currentScroll;
});
// Smooth transition for navbar
navbar.style.transition = 'transform 0.3s ease, background 0.3s ease';
}
/**
* Smooth scroll for anchor links
*/
function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
}
/**
* Animated counters for stats
*/
function initCounters() {
const counters = document.querySelectorAll('.stat-number');
const animateCounter = (counter) => {
const target = parseFloat(counter.getAttribute('data-target'));
const duration = 2000;
const step = target / (duration / 16);
let current = 0;
const updateCounter = () => {
current += step;
if (current < target) {
counter.textContent = current.toFixed(1);
requestAnimationFrame(updateCounter);
} else {
counter.textContent = target;
}
};
updateCounter();
};
// Use Intersection Observer to trigger animation
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCounter(entry.target);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
counters.forEach(counter => observer.observe(counter));
}
/**
* Service cards mouse follow glow effect
*/
function initServiceCards() {
const cards = document.querySelectorAll('.service-card');
cards.forEach(card => {
const glow = card.querySelector('.service-glow');
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
card.style.setProperty('--mouse-x', `${x}%`);
card.style.setProperty('--mouse-y', `${y}%`);
});
});
}
/**
* Scroll reveal animations
*/
function initScrollReveal() {
const reveals = document.querySelectorAll('.section, .service-card, .infra-card, .project-card, .domain-node');
const revealOnScroll = (entries, observer) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
// Add stagger delay for cards
if (entry.target.classList.contains('service-card') ||
entry.target.classList.contains('domain-node')) {
entry.target.style.animationDelay = `${index * 0.1}s`;
}
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
observer.unobserve(entry.target);
}
});
};
const observer = new IntersectionObserver(revealOnScroll, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
reveals.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
}
/**
* Mobile menu toggle
*/
function initMobileMenu() {
const menuBtn = document.querySelector('.mobile-menu-btn');
const navLinks = document.querySelector('.nav-links');
if (!menuBtn) return;
menuBtn.addEventListener('click', () => {
menuBtn.classList.toggle('active');
navLinks.classList.toggle('active');
// Animate hamburger
const spans = menuBtn.querySelectorAll('span');
if (menuBtn.classList.contains('active')) {
spans[0].style.transform = 'rotate(45deg) translate(5px, 5px)';
spans[1].style.opacity = '0';
spans[2].style.transform = 'rotate(-45deg) translate(5px, -5px)';
// Show mobile menu
navLinks.style.display = 'flex';
navLinks.style.flexDirection = 'column';
navLinks.style.position = 'absolute';
navLinks.style.top = '100%';
navLinks.style.left = '0';
navLinks.style.right = '0';
navLinks.style.background = 'rgba(10, 10, 15, 0.98)';
navLinks.style.padding = '2rem';
navLinks.style.backdropFilter = 'blur(20px)';
navLinks.style.borderBottom = '1px solid rgba(255,255,255,0.1)';
} else {
spans[0].style.transform = 'none';
spans[1].style.opacity = '1';
spans[2].style.transform = 'none';
navLinks.style.display = '';
}
});
// Close menu on link click
navLinks.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
menuBtn.classList.remove('active');
navLinks.classList.remove('active');
const spans = menuBtn.querySelectorAll('span');
spans[0].style.transform = 'none';
spans[1].style.opacity = '1';
spans[2].style.transform = 'none';
navLinks.style.display = '';
});
});
}
/**
* Parallax effect for hero section
*/
function initParallax() {
const hero = document.querySelector('.hero');
const blobs = document.querySelectorAll('.bg-blob');
if (!hero) return;
// Check for touch device
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches;
if (isTouchDevice) return;
let ticking = false;
document.addEventListener('mousemove', (e) => {
if (!ticking) {
requestAnimationFrame(() => {
const x = (e.clientX / window.innerWidth - 0.5) * 2;
const y = (e.clientY / window.innerHeight - 0.5) * 2;
blobs.forEach((blob, index) => {
const speed = (index + 1) * 20;
blob.style.transform = `translate(${x * speed}px, ${y * speed}px)`;
});
ticking = false;
});
ticking = true;
}
});
}
/**
* Typing effect for terminal cursor
*/
function initTerminalTyping() {
const terminal = document.querySelector('.terminal-body');
if (!terminal) return;
const commands = [
{ text: 'docker ps', output: 'CONTAINER ID IMAGE STATUS\nabc123 nginx Up 3 days' },
{ text: 'kubectl get pods', output: 'NAME READY STATUS\nlemonlink-app-7d9f4 1/1 Running' },
{ text: 'systemctl status lemonlink', output: '● lemonlink.service - LemonLink Platform\n Active: active (running)' }
];
let currentCommand = 0;
let isTyping = false;
const typeCommand = () => {
if (isTyping) return;
isTyping = true;
const cmd = commands[currentCommand];
const lines = terminal.querySelectorAll('.terminal-line');
const lastLine = lines[lines.length - 1];
// Clear cursor from previous line
const cursor = terminal.querySelector('.cursor');
if (cursor) cursor.remove();
// Create new command line
const newLine = document.createElement('div');
newLine.className = 'terminal-line';
newLine.innerHTML = `
<span class="prompt"></span>
<span class="path">~</span>
<span class="command"></span>
<span class="cursor">|</span>
`;
terminal.appendChild(newLine);
const commandSpan = newLine.querySelector('.command');
let charIndex = 0;
const typeChar = () => {
if (charIndex < cmd.text.length) {
commandSpan.textContent += cmd.text[charIndex];
charIndex++;
setTimeout(typeChar, 50 + Math.random() * 50);
} else {
// Show output after typing
setTimeout(() => {
const outputDiv = document.createElement('div');
outputDiv.className = 'terminal-output';
outputDiv.innerHTML = cmd.output.replace(/\n/g, '<br>');
terminal.appendChild(outputDiv);
// Move cursor to new line
const nextLine = document.createElement('div');
nextLine.className = 'terminal-line';
nextLine.innerHTML = `
<span class="prompt"></span>
<span class="path">~</span>
<span class="cursor">|</span>
`;
terminal.appendChild(nextLine);
// Scroll to bottom
terminal.scrollTop = terminal.scrollHeight;
isTyping = false;
currentCommand = (currentCommand + 1) % commands.length;
}, 500);
}
};
typeChar();
};
// Start typing effect every 8 seconds
setInterval(typeCommand, 8000);
}
// Initialize terminal typing when contact section is visible
const terminalObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
initTerminalTyping();
terminalObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
const contactSection = document.querySelector('.contact-section');
if (contactSection) {
terminalObserver.observe(contactSection);
}
/**
* Network connection lines animation
*/
function drawConnectionLines() {
const hub = document.querySelector('.hub-center');
const nodes = document.querySelectorAll('.domain-node');
const svg = document.querySelector('.connections-svg');
if (!hub || !svg || nodes.length === 0) return;
const hubRect = hub.getBoundingClientRect();
const hubX = hubRect.left + hubRect.width / 2;
const hubY = hubRect.top + hubRect.height / 2;
// Clear existing lines
svg.innerHTML = svg.querySelector('defs').outerHTML;
nodes.forEach(node => {
const nodeRect = node.getBoundingClientRect();
const nodeX = nodeRect.left + nodeRect.width / 2;
const nodeY = nodeRect.top + nodeRect.height / 2;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', hubX);
line.setAttribute('y1', hubY);
line.setAttribute('x2', nodeX);
line.setAttribute('y2', nodeY);
line.setAttribute('stroke', 'url(#line-gradient)');
line.setAttribute('stroke-width', '1');
line.setAttribute('opacity', '0.3');
svg.appendChild(line);
});
}
// Draw lines on load and resize
window.addEventListener('load', drawConnectionLines);
window.addEventListener('resize', drawConnectionLines);
/**
* Easter egg: Konami code
*/
let konamiCode = [];
const konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
document.addEventListener('keydown', (e) => {
konamiCode.push(e.key);
konamiCode = konamiCode.slice(-10);
if (konamiCode.join(',') === konamiSequence.join(',')) {
// Activate lemon mode!
document.body.style.filter = 'hue-rotate(180deg)';
setTimeout(() => {
document.body.style.filter = '';
}, 3000);
// Create floating lemons
for (let i = 0; i < 20; i++) {
createFloatingLemon();
}
}
});
function createFloatingLemon() {
const lemon = document.createElement('div');
lemon.textContent = '🍋';
lemon.style.cssText = `
position: fixed;
font-size: 2rem;
pointer-events: none;
z-index: 9999;
left: ${Math.random() * 100}vw;
top: -50px;
animation: fall ${3 + Math.random() * 2}s linear forwards;
`;
document.body.appendChild(lemon);
setTimeout(() => lemon.remove(), 5000);
}
// Add falling animation
const style = document.createElement('style');
style.textContent = `
@keyframes fall {
to {
transform: translateY(110vh) rotate(${Math.random() * 360}deg);
}
}
`;
document.head.appendChild(style);
/**
* Service status check simulation
*/
function checkServiceStatus() {
const statusIndicators = document.querySelectorAll('.service-status');
statusIndicators.forEach(indicator => {
// Random status update for demo purposes
const isOnline = Math.random() > 0.1; // 90% online
const dot = indicator.querySelector('.status-dot');
if (isOnline) {
indicator.className = 'service-status online';
indicator.querySelector('span:last-child').textContent = 'Online';
dot.style.background = '#22c55e';
} else {
indicator.className = 'service-status maintenance';
indicator.querySelector('span:last-child').textContent = 'Maintenance';
dot.style.background = '#f59e0b';
}
});
}
// Simulate status check every 30 seconds
setInterval(checkServiceStatus, 30000);
/**
* Prefers reduced motion
*/
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// Disable animations
document.documentElement.style.setProperty('--transition-fast', '0s');
document.documentElement.style.setProperty('--transition-normal', '0s');
document.documentElement.style.setProperty('--transition-slow', '0s');
}

1514
styles.css Normal file

File diff suppressed because it is too large Load Diff