Initial commit: LemonLink landing page
This commit is contained in:
commit
6683b5963e
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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! 🚀
|
||||||
|
|
@ -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;"]
|
||||||
|
|
@ -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! 🚀
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## ✨ 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 ☕
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>© 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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue