1183 lines
45 KiB
React
1183 lines
45 KiB
React
import React, { useState, useEffect, useRef, useContext } from 'react';
|
|
import {
|
|
Book, Code, Terminal, Server, Globe, Menu, X, Search,
|
|
MessageSquare, Headphones, Home, Gamepad2, ArrowLeft, Info, Database
|
|
} from "lucide-react";
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { AuthContext } from '../App';
|
|
import { colors } from '../theme/colors';
|
|
import branding from '../branding';
|
|
import {
|
|
ActionButton
|
|
} from '../components/DashboardComponents';
|
|
|
|
function DocumentationPage() {
|
|
const { isAuthenticated } = useContext(AuthContext);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [activeArticleId, setActiveArticleId] = useState('account-creation');
|
|
const navigate = useNavigate();
|
|
const searchInputRef = useRef(null);
|
|
const scrollRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === '/' &&
|
|
document.activeElement.tagName !== 'INPUT' &&
|
|
document.activeElement.tagName !== 'TEXTAREA') {
|
|
e.preventDefault();
|
|
searchInputRef.current?.focus();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, []);
|
|
|
|
const sections = [
|
|
{
|
|
id: 'getting-started',
|
|
title: 'Getting Started',
|
|
icon: Home,
|
|
color: colors.blue,
|
|
articles: [
|
|
{
|
|
id: 'account-creation',
|
|
title: 'Creating Your Account',
|
|
content: `# Creating Your Account
|
|
|
|
Creating an account on ${branding.name} gives you access to our Kubernetes-based cloud hosting platform.
|
|
|
|
## Step 1: Registration
|
|
Navigate to ${branding.name} and click the Register button. You can also sign up using Google or Apple for a faster experience.
|
|
|
|
## Step 2: Required Information
|
|
- **Username**: Must be unique. This determines your dedicated subdomain: \`u-{username}.${branding.domain}\`.
|
|
- **Email Address**: Used for verification and critical alerts.
|
|
- **Password**: Securely hashed using bcrypt.
|
|
|
|
## Step 3: Deployment
|
|
When you click "Create Account":
|
|
1. Your account is initialized in our **PostgreSQL** database.
|
|
2. A dedicated Kubernetes namespace is provisioned: \`user-{username}\`.
|
|
3. You receive **200 welcome credits** (2.00 EUR value) to test the platform.
|
|
4. A verification email is sent to finalize account activation.
|
|
|
|
> **Note**: Your username is permanent and directly affects your network identity. Choose carefully!`
|
|
},
|
|
{
|
|
id: 'logging-in',
|
|
title: 'Authentication and Security',
|
|
content: `# Authentication and Security
|
|
|
|
${branding.name} utilizes a hybrid approach of REST and WebSockets for secure, real-time management.
|
|
|
|
## Authentication Flow
|
|
1. Credentials are verified via secure hash comparison.
|
|
2. A **JWT (JSON Web Token)** is issued, valid for 24 hours.
|
|
3. This token is stored in your browser's \`localStorage\` as \`anchorhost_token\`.
|
|
4. The dashboard establishes a persistent **WSS (Secure WebSocket)** connection for live updates.
|
|
|
|
## Two-Factor Authentication (2FA)
|
|
We strongly recommend enabling 2FA in your **Account Settings**.
|
|
- Uses TOTP (Time-based One-Time Password) standards.
|
|
- Compatible with Google Authenticator, Authy, and Ente.
|
|
- Provides a critical layer of defense for your infrastructure.
|
|
|
|
## Real-time Connectivity
|
|
Look for the status indicator in the top bar:
|
|
- **Connected** (Green): Full sync with Kubernetes API.
|
|
- **Reconnecting** (Yellow): Attempting to restore session.
|
|
- **Disconnected** (Red): Network interruption detected.`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'how-to-connect',
|
|
title: 'How to Connect',
|
|
icon: Terminal,
|
|
color: colors.cyan,
|
|
articles: [
|
|
{
|
|
id: 'connection-basics',
|
|
title: 'The Golden Rule: IP and Port',
|
|
content: `# Understanding Connection Basics
|
|
|
|
If you are new to hosting, the most important thing to understand is how your computer "finds" your server.
|
|
|
|
## The Apartment Analogy
|
|
Think of our main server as a **Large Apartment Building**.
|
|
1. **The IP Address (85.237.179.121)**: This is the **Street Address** of the building. It gets you to the front door.
|
|
2. **The Port (e.g., 31452)**: This is your **Apartment Number**.
|
|
|
|
**Without the Apartment Number (Port), you are just standing at the front door of a building with 10,000 people. You will never find your room!**
|
|
|
|
## The Format
|
|
When connecting to anything on ${branding.name}, you will almost always use this format:
|
|
\`85.237.179.121:PORT\`
|
|
|
|
> **Idiot-Proof Tip**: Look at your Server Card in the Dashboard. The number after the colon (\`:\`) is your unique Port. You **MUST** include it.`
|
|
},
|
|
{
|
|
id: 'connect-website',
|
|
title: 'Connecting to your Website',
|
|
content: `# How to view your Website
|
|
|
|
Websites can be accessed via a custom subdomain or directly via IP.
|
|
|
|
## Method 1: Using a Subdomain (Recommended)
|
|
You can register a free subdomain (e.g., \`mysite.u-username.${branding.domain}\`) via the **Domains** tab.
|
|
1. Go to the **Domains** tab in your Dashboard.
|
|
2. Register your preferred subdomain.
|
|
3. Once registered, you can simply type it into your browser's address bar.
|
|
|
|
## Method 2: Using the IP
|
|
If you haven't set up a subdomain yet:
|
|
1. Find your **External Port** in the Ports tab (usually for web it maps to 80).
|
|
2. Type \`http://85.237.179.121:PORT\` into your browser.
|
|
|
|
> **Why is it not working?** Make sure your server is **Running** (Green status) and you have exposed Port 80 in the Ports tab!`
|
|
},
|
|
{
|
|
id: 'connect-game',
|
|
title: 'Connecting to a Game (Minecraft)',
|
|
content: `# How to join your Minecraft Server
|
|
|
|
Joining your server is simple once you have your Port.
|
|
|
|
## Step 1: Get your Address
|
|
In your Dashboard, look for your server's address. It will look like:
|
|
\`85.237.179.121:30542\`
|
|
|
|
## Step 2: In Minecraft
|
|
1. Open Minecraft and click **Multiplayer**.
|
|
2. Click **Add Server**.
|
|
3. In **Server Address**, paste the full address (including the numbers after the colon).
|
|
4. Click **Done** and Join!
|
|
|
|
> **Common Mistake**: Forgetting the port. If you just type \`85.237.179.121\`, you will see "Connection Refused" or join someone else's server!`
|
|
},
|
|
{
|
|
id: 'connection-software',
|
|
title: 'Software Recommendations',
|
|
content: `# Recommended Software for Every Platform
|
|
|
|
To connect to your servers, you need the right tools. Here is what we recommend for your device.
|
|
|
|
## SSH (Linux VPS & Game Console)
|
|
Used for typing commands and managing your server.
|
|
- **Windows**: **PowerShell** (Built-in), **PuTTY**, or **Termius**.
|
|
- **macOS / Linux**: **Terminal** (Built-in).
|
|
- **Android / iOS**: **Termius** or **JuiceSSH**.
|
|
|
|
## RDP (Windows VPS)
|
|
Used for seeing the Windows desktop.
|
|
- **Windows**: **Remote Desktop Connection** (Built-in).
|
|
- **macOS / iOS / Android**: **Microsoft Remote Desktop** app.
|
|
- **Linux**: **Remmina**.
|
|
|
|
## Databases (MySQL, Postgres, etc.)
|
|
Used for managing tables and data.
|
|
- **Windows / macOS / Linux**: **DBeaver** (Best all-in-one), **HeidiSQL** (Windows only, very light), **TablePlus**.
|
|
- **Mobile**: We do not recommend managing databases on mobile.
|
|
|
|
## File Transfer (SFTP)
|
|
Used for uploading and downloading files.
|
|
- **Windows / macOS / Linux**: **FileZilla** or **WinSCP** (Windows only).
|
|
- **Dashboard**: Use our built-in **File Manager** for quick edits!`
|
|
},
|
|
{
|
|
id: 'connect-vps-beginner',
|
|
title: 'Connecting to your VPS (Beginner)',
|
|
content: `# Your First Time Login to VPS
|
|
|
|
A VPS doesn't have a "game menu." You talk to it using a text window called **SSH**.
|
|
|
|
## Method A: The Dashboard Console (Easiest)
|
|
1. Click on your VPS in the Dashboard.
|
|
2. Click the **Console** tab.
|
|
3. You will see a black screen. Press **Enter**.
|
|
4. Type your username (e.g., \`ubuntu\`) and the default password.
|
|
5. **Note**: When you type passwords in Linux, you won't see any dots or stars. **It is still typing!** Just type it and press Enter.
|
|
|
|
## Method B: Using your PC (Recommended)
|
|
|
|
### Windows Users
|
|
1. Open **PowerShell** or **CMD**.
|
|
2. Type: \`ssh ubuntu@85.237.179.121 -p YOUR_PORT\`
|
|
3. (Optional) Download **PuTTY** if you prefer a graphical interface.
|
|
|
|
### macOS and Linux Users
|
|
1. Open **Terminal**.
|
|
2. Type: \`ssh ubuntu@85.237.179.121 -p YOUR_PORT\`
|
|
|
|
### Mobile Users
|
|
1. Download **Termius** from the App Store or Play Store.
|
|
2. Add a new "Host".
|
|
3. IP: \`85.237.179.121\`, Port: \`YOUR_PORT\`, User: \`ubuntu\`.`
|
|
},
|
|
{
|
|
id: 'connect-db-beginner',
|
|
title: 'Connecting to your Database',
|
|
content: `# How to see your Database Data
|
|
|
|
You don't "open" a database like a folder. You use a tool to look inside it.
|
|
|
|
## Recommended Tools
|
|
- **DBeaver (Universal)**: Works on Windows, Mac, and Linux. Supports everything.
|
|
- **HeidiSQL (Windows)**: Lightweight and fast for MySQL and Postgres.
|
|
|
|
## Step-by-Step Connection (Example using DBeaver)
|
|
1. Click **New Connection**.
|
|
2. Select your database type (MySQL, PostgreSQL, etc.).
|
|
3. **Host**: \`85.237.179.121\`
|
|
4. **Port**: Your **External Port** from the Dashboard.
|
|
5. **Username**: \`root\`
|
|
6. **Password**: \`anchorhost123\`
|
|
7. Click **Test Connection**.
|
|
|
|
> **Crucial Step**: You **MUST** go to the **Ports** tab in your Dashboard and add a port for your database (Internal 3306 for MySQL or 5432 for Postgres) or you won't be able to connect from your computer!`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'infrastructure',
|
|
title: 'Infrastructure',
|
|
icon: Server,
|
|
color: colors.green,
|
|
articles: [
|
|
{
|
|
id: 'what-is-vps',
|
|
title: 'What is a VPS?',
|
|
content: `# What is a VPS?
|
|
|
|
A VPS (Virtual Private Server) is a private, isolated portion of a powerful physical server.
|
|
|
|
## Simple Explanation
|
|
Imagine a large apartment building. The building itself is the physical server. Each apartment is a **VPS**.
|
|
- You have your own front door (isolated environment).
|
|
- You have your own kitchen and bathroom (dedicated resources like CPU and RAM).
|
|
- What you do in your apartment doesn't affect your neighbors, and vice-versa.
|
|
|
|
On ${branding.name}, our VPS instances are powered by **KubeVirt**, giving you a real virtual machine with its own kernel, allowing you to install any OS or software you need with full root access.`
|
|
},
|
|
{
|
|
id: 'server-types',
|
|
title: 'Server Archetypes',
|
|
content: `# Server Archetypes
|
|
|
|
We offer specialized hosting environments optimized for different workloads.
|
|
|
|
## VPS (Virtual Private Server)
|
|
Powered by **KubeVirt**, these are true Virtual Machines with dedicated kernels.
|
|
- **Root Access**: Complete control over the OS.
|
|
- **Persistent Storage**: Backed by **Longhorn** block storage.
|
|
- **SSH/RDP**: Native access via dedicated NodePorts.
|
|
|
|
## Game Servers
|
|
Containerized environments pre-configured for low-latency gaming.
|
|
- **Minecraft**: Support for Paper, Spigot, and Vanilla with integrated Plugin Store.
|
|
- **Other Games**: FiveM, CS2, Rust, ARK, and Valheim.
|
|
- **Live Console**: Direct access to the game process input/output.
|
|
|
|
## Web and Databases
|
|
Managed containers for application stacks.
|
|
- **Web**: Nginx, WordPress, and Node.js with automatic SSL via Traefik Ingress.
|
|
- **DB**: Managed MySQL, PostgreSQL, MongoDB, and Redis.
|
|
- **Defaults**: Managed databases use user \`root\` and password \`anchorhost123\` unless customized.`
|
|
},
|
|
{
|
|
id: 'how-billing-works',
|
|
title: 'How Billing Works',
|
|
content: `# How Billing Works
|
|
|
|
${branding.name} operates on a transparent **pre-paid credit system**. There are no hidden fees or surprise invoices.
|
|
|
|
## Credits and Value
|
|
- **100 credits = 1.00 EUR**.
|
|
- You purchase credits via **Stripe**.
|
|
- Credits are stored in your account balance.
|
|
|
|
## Billing Cycles
|
|
1. **Creation Fee**: A one-time small fee is deducted when you create a new server.
|
|
2. **Monthly Rent**: Every 30 days, the platform calculates the cost of your active servers based on their resource allocation (CPU, RAM, Storage) and service multiplier.
|
|
3. **Automatic Deduction**: This total is automatically deducted from your credit balance.
|
|
|
|
## Low Balance Protection
|
|
If your balance is insufficient to cover the monthly rent:
|
|
- You will receive an email warning.
|
|
- If credits run out, servers are automatically **Suspended (Stopped)** to prevent debt.
|
|
- Your data remains safe! Simply add credits and click **Start** to resume service.`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'minecraft-guides',
|
|
title: 'Minecraft Hosting',
|
|
icon: Gamepad2,
|
|
color: colors.red,
|
|
articles: [
|
|
{
|
|
id: 'java-vs-bedrock',
|
|
title: 'Java vs Bedrock Edition',
|
|
content: `# Java vs Bedrock Edition
|
|
|
|
It is important to understand which version of Minecraft you intend to host.
|
|
|
|
## Game Servers (Java Edition)
|
|
Currently, our **Game Server** archetype only supports **Minecraft Java Edition**. This is the standard version for PC players and supports the most extensive range of plugins and mods.
|
|
|
|
## Bedrock Edition
|
|
If you wish to host **Minecraft Bedrock** (for Console, Mobile, or Windows 10 Edition players):
|
|
- You must use a **VPS** archetype.
|
|
- You will need to manually install the Bedrock dedicated server software.
|
|
- This requires more technical knowledge but offers total control.`
|
|
},
|
|
{
|
|
id: 'minecraft-install',
|
|
title: 'Installing and Managing Minecraft',
|
|
content: `# Minecraft Server Management
|
|
|
|
${branding.name} provides a specialized environment for Minecraft servers using high-performance containers.
|
|
|
|
## Installation
|
|
1. Go to **Create Server**.
|
|
2. Select **Game Server**.
|
|
3. Choose **Minecraft** from the list.
|
|
4. Select your preferred **Version** (e.g., 1.21.1) and **Server Type** (Paper, Spigot, or Vanilla).
|
|
5. Allocate resources (RAM is critical for Minecraft performance).
|
|
6. Click **Create Server**.
|
|
|
|
## Managing Your Server
|
|
Once deployed, you can control your server from the dashboard:
|
|
- **Start/Stop/Restart**: Real-time control buttons in the server card.
|
|
- **Live Console**: Direct access to the Minecraft server process. You can execute commands (e.g., \`op username\`) directly.
|
|
- **Settings**: Adjust \`server.properties\` directly via the **Quick Settings** tab.`
|
|
},
|
|
{
|
|
id: 'minecraft-properties',
|
|
title: 'Quick Settings (server.properties)',
|
|
content: `# Understanding server.properties
|
|
|
|
The \`server.properties\` file is the heart of your Minecraft configuration. You can edit this in the **Quick Settings** tab of your server.
|
|
|
|
## Key Properties Explained
|
|
- **level-name**: The name of your world folder.
|
|
- **difficulty**: Set to \`peaceful\`, \`easy\` (1), \`normal\` (2), or \`hard\` (3).
|
|
- **gamemode**: The default mode for new players (\`survival\`, \`creative\`, etc.).
|
|
- **max-players**: Limits how many people can join at once.
|
|
- **view-distance**: How many chunks are sent to the client. **High values can cause lag!**
|
|
- **motd**: The message shown in the server list.
|
|
|
|
> **Pro Tip**: Always restart your server after changing these settings to apply them.`
|
|
},
|
|
{
|
|
id: 'minecraft-non-premium',
|
|
title: 'Allowing Non-Premium Players',
|
|
content: `# Allowing Non-Premium (Cracked) Players
|
|
|
|
By default, Minecraft servers check with Mojang to verify accounts. You can disable this to allow players with unofficial launchers to join.
|
|
|
|
## How to enable
|
|
1. Go to your **Quick Settings** or the **File Manager**.
|
|
2. Locate the \`online-mode\` setting.
|
|
3. Change it from \`true\` to \`false\`.
|
|
4. **Save** and **Restart** the server.
|
|
|
|
> **Security Warning**: When \`online-mode\` is false, anyone can join using any username (including yours). We **strongly recommend** installing an authentication plugin like \`AuthMe\` to protect your player accounts.`
|
|
},
|
|
{
|
|
id: 'minecraft-whitelist',
|
|
title: 'Whitelisting Players',
|
|
content: `# Whitelisting Players
|
|
|
|
A whitelist is the best way to keep your server private and safe from unwanted griefers.
|
|
|
|
## Step 1: Enable Whitelist
|
|
In your **Quick Settings** or console, set:
|
|
\`\`\`bash
|
|
whitelist on
|
|
\`\`\`
|
|
|
|
## Step 2: Add Players
|
|
In the **Live Console**, type:
|
|
\`\`\`bash
|
|
whitelist add PlayerName
|
|
\`\`\`
|
|
|
|
## Step 3: Reload
|
|
To ensure the list is active:
|
|
\`\`\`bash
|
|
whitelist reload
|
|
\`\`\``
|
|
},
|
|
{
|
|
id: 'minecraft-permissions',
|
|
title: 'Permissions and OP Safety',
|
|
content: `# Permissions and OP Safety
|
|
|
|
Managing who can run commands is vital for server security.
|
|
|
|
## Operator (OP) Status
|
|
Giving someone **OP** gives them full power over the server.
|
|
- **How to OP**: Type \`op PlayerName\` in the console.
|
|
- **How to de-OP**: Type \`deop PlayerName\` in the console.
|
|
|
|
## OP Safety
|
|
- **Never** OP someone you do not trust completely.
|
|
- Use **LuckPerms** (available in our Plugin Store) to give specific permissions (like \`/tp\` or \`/home\`) instead of giving full OP.
|
|
- Use different **OP levels** in \`server.properties\` to restrict what OPs can do.`
|
|
},
|
|
{
|
|
id: 'minecraft-pregen',
|
|
title: 'Chunk Pregeneration',
|
|
content: `# World Pregeneration
|
|
|
|
Generating new terrain is the most resource-intensive task for a Minecraft server. If many players explore new areas at once, it will cause lag.
|
|
|
|
## Why Pregenerate?
|
|
Pregeneration creates world chunks **before** players join, so the server only has to *load* them from disk instead of *calculating* them on the fly.
|
|
|
|
## Recommended Tool
|
|
We recommend installing the **Chunky** plugin from our Plugin Store.
|
|
1. Install **Chunky**.
|
|
2. Restart the server.
|
|
3. In console, set a radius: \`chunky radius 5000\`.
|
|
4. Start pregeneration: \`chunky start\`.
|
|
|
|
> **Note**: This process will use 100% CPU while running. It is best to do this while no one is playing.`
|
|
},
|
|
{
|
|
id: 'minecraft-plugins-files',
|
|
title: 'Plugins and File Manager',
|
|
content: `# Plugins and File Manager
|
|
|
|
## Installing Plugins
|
|
We provide an integrated **Plugin Store** powered by the Modrinth API.
|
|
1. Navigate to the **Plugin Store** in the sidebar.
|
|
2. Select your Minecraft server from the dropdown.
|
|
3. Search for plugins (e.g., EssentialsX, LuckPerms).
|
|
4. Click **Install**.
|
|
5. **Restart** your server.
|
|
|
|
## Using the File Manager
|
|
1. Open the **File Manager** for your server.
|
|
2. The root directory is \`/data\`.
|
|
3. **Plugins** are located in \`/data/plugins\`.
|
|
4. **Worlds** are stored in \`/data/world\`.
|
|
5. You can **drag and drop** files to upload custom configurations or worlds.`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'networking',
|
|
title: 'Networking and Domains',
|
|
icon: Globe,
|
|
color: colors.cyan,
|
|
articles: [
|
|
{
|
|
id: 'network-architecture',
|
|
title: 'Network Architecture',
|
|
content: `# Network Architecture
|
|
|
|
All servers deployed on ${branding.name} share a high-performance static IP but utilize unique entry points.
|
|
|
|
## External Connectivity
|
|
- **Global IP**: \`85.237.179.121\`
|
|
- **Entry Type**: NodePort (Range: 30000 - 32768)
|
|
- **Example**: If your server is assigned port \`31452\`, you connect via \`85.237.179.121:31452\`.
|
|
|
|
## Subdomain System
|
|
We provide automatic DNS management via the **deSEC API**.
|
|
- **Format**: \`{label}.u-{username}.${branding.domain}\`
|
|
- **SSL**: Automatic Let's Encrypt certificates are provisioned for all subdomains.
|
|
- **Custom Domains**: You can point your own domain (e.g., example.com) by creating an **A Record** pointing to \`85.237.179.121\`.`
|
|
},
|
|
{
|
|
id: 'exposing-ports',
|
|
title: 'Exposing Ports (NodePorts)',
|
|
content: `# Exposing Ports
|
|
|
|
To make your server accessible from the internet, you must expose its internal ports using our NodePort system.
|
|
|
|
## Step 1: Identify the Internal Port
|
|
Determine which port your application is listening on inside the container or VPS.
|
|
- **Minecraft**: 25565
|
|
- **SSH**: 22
|
|
- **Web**: 80
|
|
- **RDP**: 3389
|
|
|
|
## Step 2: Add Port via Dashboard
|
|
1. Go to your **Server Details** page.
|
|
2. Locate the **Ports** section and click **Add Port**.
|
|
3. Enter the **Internal Port** and select the protocol (**TCP** or **UDP**).
|
|
|
|
## Step 3: Connect via External Port
|
|
The system will assign a unique **External Port** (e.g., 31245). Your server is reachable at: \`85.237.179.121:31245\`.`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'ssh-access-guides',
|
|
title: 'SSH and Access',
|
|
icon: Terminal,
|
|
color: colors.magenta,
|
|
articles: [
|
|
{
|
|
id: 'vps-credentials',
|
|
title: 'Default Credentials',
|
|
content: `# Default Credentials
|
|
|
|
When you first deploy a VPS, use these default credentials to log in. **Change them immediately!**
|
|
|
|
| OS | Default User | Default Password |
|
|
|:---|:---|:---|
|
|
| **Ubuntu** | \`ubuntu\` | \`ubuntu\` |
|
|
| **Debian** | \`debian\` | \`debian\` |
|
|
| **Alpine** | \`root\` | \`alpine\` |
|
|
| **CentOS** | \`centos\` | \`centos\` |
|
|
| **Windows** | \`Administrator\` | \`Password123\` |`
|
|
},
|
|
{
|
|
id: 'ssh-cmd-putty',
|
|
title: 'Connecting via SSH (CMD/PuTTY)',
|
|
content: `# Connecting via SSH
|
|
|
|
## Using Windows CMD or PowerShell
|
|
Type the following (replace \`user\` and \`PORT\`):
|
|
\`\`\`bash
|
|
ssh -p PORT user@85.237.179.121
|
|
\`\`\`
|
|
|
|
## Using PuTTY
|
|
1. **Host Name**: \`85.237.179.121\`
|
|
2. **Port**: Your server's assigned **External Port**.
|
|
3. **Connection type**: SSH.
|
|
4. Click **Open**.`
|
|
},
|
|
{
|
|
id: 'securing-ssh',
|
|
title: 'Securing SSH (Keys vs Passwords)',
|
|
content: `# Securing SSH Access
|
|
|
|
By default, we use password authentication. However, using **SSH Keys** is significantly more secure.
|
|
|
|
## Password Login
|
|
- **Pros**: Simple to use.
|
|
- **Cons**: Vulnerable to brute-force attacks.
|
|
|
|
## SSH Key Authentication
|
|
- **Pros**: Nearly impossible to hack; no need to type passwords.
|
|
- **Cons**: Requires initial setup.
|
|
|
|
## How to switch to Keys
|
|
1. Generate a key on your PC: \`ssh-keygen -t ed25519\`.
|
|
2. Copy the contents of your \`.pub\` file.
|
|
3. On your VPS, paste it into \`~/.ssh/authorized_keys\`.
|
|
4. In \`/etc/ssh/sshd_config\`, set \`PasswordAuthentication no\`.
|
|
5. Restart SSH: \`sudo systemctl restart ssh\`.`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'vps-management',
|
|
title: 'VPS Management',
|
|
icon: Terminal,
|
|
color: colors.blue,
|
|
articles: [
|
|
{
|
|
id: 'vps-power-actions',
|
|
title: 'Power Actions and Status',
|
|
content: `# VPS Power Actions
|
|
|
|
Managing the state of your Virtual Machine is done directly through the dashboard.
|
|
|
|
## Status Indicators
|
|
- **Running** (Green): Your VPS is active and consuming resources.
|
|
- **Stopped** (Red): The VM is powered off. No CPU/RAM is used, but storage is preserved.
|
|
- **Starting/Stopping** (Yellow): The VM is transitioning between states.
|
|
|
|
## Control Buttons
|
|
1. **Start**: Boots the VM kernel.
|
|
2. **Stop**: Sends an ACPI shutdown signal. If the OS doesn't respond within 60 seconds, it is forcefully terminated.
|
|
3. **Restart**: Performs a quick power cycle.
|
|
4. **Kill**: Equivalent to pulling the power plug. Use only if the OS is completely frozen.`
|
|
},
|
|
{
|
|
id: 'vps-resources',
|
|
title: 'Scaling Resources',
|
|
content: `# Scaling Resources
|
|
|
|
${branding.name} allows you to adjust your VPS specifications as your needs grow.
|
|
|
|
## CPU and RAM
|
|
You can increase or decrease your allocated CPU cores and RAM from the **Settings** tab.
|
|
- **Note**: Most OS changes require a **Restart** to recognize the new resource limits.
|
|
- **Dynamic Scaling**: Kubernetes handles the resource shifting, but the VM guest OS must re-initialize its hardware map.
|
|
|
|
## Storage Expansion
|
|
Our storage is backed by **Longhorn**.
|
|
1. Increase the disk size in the dashboard.
|
|
2. Inside your VPS, you must resize the partition (e.g., using \`growpart\`) and the filesystem (e.g., \`resize2fs\`).
|
|
|
|
> **Warning**: You can always increase storage, but decreasing it is extremely risky and can lead to data loss.`
|
|
},
|
|
{
|
|
id: 'vps-snapshots',
|
|
title: 'Snapshots and Backups',
|
|
content: `# Snapshots and Backups
|
|
|
|
Protect your data before making major changes.
|
|
|
|
## What is a Snapshot?
|
|
A snapshot is a "frozen" state of your disk at a specific point in time. It is perfect for creating a restore point before installing complex software.
|
|
|
|
## Automated Backups
|
|
We perform daily off-site backups of all **Longhorn** volumes.
|
|
- Backups are stored in a separate data center.
|
|
- You can request a restoration of your entire VPS via a **Support Ticket** if a catastrophic failure occurs.`
|
|
},
|
|
{
|
|
id: 'vps-change-password',
|
|
title: 'Changing VPS Password',
|
|
content: `# Changing Your VPS Password
|
|
|
|
Security is paramount. You should change your password immediately after the first login.
|
|
|
|
## Linux (Ubuntu, Debian, Alpine, CentOS)
|
|
1. Log in via SSH or the Dashboard Console.
|
|
2. Type the following command:
|
|
\`\`\`bash
|
|
passwd
|
|
\`\`\`
|
|
3. Enter your **current** password.
|
|
4. Enter your **new** secure password twice.
|
|
|
|
> **Note**: For Alpine Linux logged in as root, simply type \`passwd\`. For other distros using \`sudo\`, you can change any user's password with \`sudo passwd username\`.
|
|
|
|
## Windows Server
|
|
1. Connect via RDP (Remote Desktop).
|
|
2. Press \`Ctrl+Alt+End\` (which mimics Ctrl+Alt+Del on a remote session).
|
|
3. Select **Change a password**.
|
|
4. Follow the on-screen prompts.`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'database-guides',
|
|
title: 'Database Hosting',
|
|
icon: Database,
|
|
color: colors.cyan,
|
|
articles: [
|
|
{
|
|
id: 'db-getting-started',
|
|
title: 'Getting Started with Databases',
|
|
content: `# Managed Database Hosting
|
|
|
|
${branding.name} provides pre-configured database containers for rapid development.
|
|
|
|
## Supported Engines
|
|
- **MySQL**: The world's most popular open-source database.
|
|
- **PostgreSQL**: Advanced object-relational database.
|
|
- **MongoDB**: Leading NoSQL document database.
|
|
- **Redis**: High-performance in-memory data store.
|
|
|
|
## Default Credentials
|
|
For all managed databases, the default credentials are:
|
|
- **User**: \`root\`
|
|
- **Password**: \`anchorhost123\`
|
|
|
|
> **Important**: You must expose the database port via the **Ports** tab to connect from outside the cluster.`
|
|
},
|
|
{
|
|
id: 'db-change-password',
|
|
title: 'Changing Database Passwords',
|
|
content: `# Changing Database Passwords
|
|
|
|
It is highly recommended to change the default \`anchorhost123\` password immediately.
|
|
|
|
## MySQL / MariaDB
|
|
Connect to your database and run:
|
|
\`\`\`sql
|
|
ALTER USER 'root'@'%' IDENTIFIED BY 'your_new_password';
|
|
FLUSH PRIVILEGES;
|
|
\`\`\`
|
|
|
|
## PostgreSQL
|
|
Connect via \`psql\` and run:
|
|
\`\`\`sql
|
|
ALTER USER root WITH PASSWORD 'your_new_password';
|
|
\`\`\`
|
|
|
|
## MongoDB
|
|
Connect via \`mongosh\` and run:
|
|
\`\`\`javascript
|
|
use admin
|
|
db.changeUserPassword("root", "your_new_password")
|
|
\`\`\`
|
|
|
|
## Redis
|
|
Redis uses a single password (no users by default in older versions).
|
|
1. Edit the \`redis.conf\` via the File Manager.
|
|
2. Locate the line \`# requirepass foobared\`.
|
|
3. Change it to \`requirepass your_new_password\`.
|
|
4. Restart the server.`
|
|
},
|
|
{
|
|
id: 'db-connection-strings',
|
|
title: 'Connection Strings',
|
|
content: `# Database Connection Strings
|
|
|
|
Use these templates to connect your applications. Replace \`IP\`, \`PORT\`, and \`PASSWORD\`.
|
|
|
|
### MySQL
|
|
\`mysql://root:PASSWORD@IP:PORT/dbname\`
|
|
|
|
### PostgreSQL
|
|
\`postgresql://root:PASSWORD@IP:PORT/dbname\`
|
|
|
|
### MongoDB
|
|
\`mongodb://root:PASSWORD@IP:PORT/?authSource=admin\`
|
|
|
|
### Redis
|
|
\`redis://:PASSWORD@IP:PORT\``
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'domains-dns',
|
|
title: 'Domains & DNS',
|
|
icon: Globe,
|
|
color: colors.green,
|
|
articles: [
|
|
{
|
|
id: 'subdomain-management',
|
|
title: 'Managing Subdomains',
|
|
content: `# Managing Your Subdomains
|
|
|
|
Every user on ${branding.name} has their own personal network space based on their username.
|
|
|
|
## How to create a Subdomain
|
|
1. Go to the **Domains** tab.
|
|
2. Enter your desired name (e.g., \`play\`).
|
|
3. Click **Register**.
|
|
4. Your full domain will be \`play.u-username.${branding.domain}\`.
|
|
|
|
## Linking to a Server
|
|
Once a domain is created, you can "point" it to one of your active servers.
|
|
- If linked to a **Website**, it will automatically handle SSL (HTTPS).
|
|
- If linked to a **Game Server**, you can join using the domain instead of the IP. (Note: You still need to provide the Port in the game client).`
|
|
},
|
|
{
|
|
id: 'dnssec-warnings',
|
|
title: 'Resolving DNSSEC Warnings',
|
|
content: `# Fixing DNSSEC and NS Warnings
|
|
|
|
If you see a warning saying **"No NS records found for child domain"** or **"DNSSEC configuration issues"**, don't panic!
|
|
|
|
## Why is this happening?
|
|
${branding.name} uses high-security DNSSEC to protect your subdomains from being hijacked. For this to work, our main system needs to confirm that your specific "child" space is ready.
|
|
|
|
## How to Fix
|
|
Usually, this is handled automatically within 5-10 minutes of creating your account. If the warning persists:
|
|
1. Ensure you have at least one **Subdomain** registered in the Domains tab.
|
|
2. If you are using a **Custom Domain** (e.g., \`mysite.com\`), you must point your Nameservers to:
|
|
- \`ns1.desec.io\`
|
|
- \`ns2.desec.org\`
|
|
3. Wait for propagation (can take up to 24 hours).`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'vps-tutorials',
|
|
title: 'VPS Tutorials',
|
|
icon: Code,
|
|
color: colors.orange,
|
|
articles: [
|
|
{
|
|
id: 'docker-on-vps',
|
|
title: 'Installing Docker and Compose',
|
|
content: `# Docker on VPS
|
|
|
|
Docker allows you to run applications in isolated containers, keeping your base OS clean.
|
|
|
|
## Step 1: Install Docker
|
|
Run the official convenience script:
|
|
\`\`\`bash
|
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
sudo sh get-docker.sh
|
|
\`\`\`
|
|
|
|
## Step 2: Manage as Non-Root
|
|
Add your user to the docker group so you don't need \`sudo\`:
|
|
\`\`\`bash
|
|
sudo usermod -aG docker $USER
|
|
\`\`\`
|
|
*Log out and log back in for this to take effect.*
|
|
|
|
## Step 3: Docker Compose
|
|
Modern Docker includes Compose by default. Verify it:
|
|
\`\`\`bash
|
|
docker compose version
|
|
\`\`\`
|
|
|
|
## Why use Docker?
|
|
- **Isolation**: Apps don't interfere with each other.
|
|
- **Portability**: Move your \`docker-compose.yml\` to any other VPS and it will work identically.
|
|
- **Efficiency**: Lower overhead than running multiple Virtual Machines.`
|
|
},
|
|
{
|
|
id: 'vps-firewall',
|
|
title: 'Securing with UFW (Firewall)',
|
|
content: `# Securing your VPS with UFW
|
|
|
|
A firewall is your first line of defense against hackers.
|
|
|
|
## Step 1: Check Status
|
|
\`\`\`bash
|
|
sudo ufw status
|
|
\`\`\`
|
|
|
|
## Step 2: Allow Essential Services
|
|
**CRITICAL**: Always allow SSH before enabling the firewall, or you will lock yourself out!
|
|
\`\`\`bash
|
|
sudo ufw allow 22/tcp
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
\`\`\`
|
|
|
|
## Step 3: Enable UFW
|
|
\`\`\`bash
|
|
sudo ufw enable
|
|
\`\`\`
|
|
|
|
## Step 4: Rule Management
|
|
- **Delete a rule**: \`sudo ufw delete allow 80/tcp\`
|
|
- **Allow specific IP**: \`sudo ufw allow from 1.2.3.4 to any port 22\``
|
|
},
|
|
{
|
|
id: 'vps-monitoring',
|
|
title: 'Monitoring Performance (htop/nload)',
|
|
content: `# Monitoring VPS Performance
|
|
|
|
Knowing how your resources are being used helps you troubleshoot lag and crashes.
|
|
|
|
## htop (CPU and RAM)
|
|
Install it: \`sudo apt install htop -y\`
|
|
- **CPU Bar**: Shows usage per core.
|
|
- **Mem Bar**: Shows RAM usage. Blue is actual usage, yellow is cache.
|
|
- **Process List**: See which app is eating your resources. Press \`F9\` to kill a process.
|
|
|
|
## nload (Network Traffic)
|
|
Install it: \`sudo apt install nload -y\`
|
|
- Shows real-time incoming and outgoing bandwidth.
|
|
- Useful for detecting if your server is being DDoS'd or if a download is finished.
|
|
|
|
## df (Disk Space)
|
|
\`\`\`bash
|
|
df -h
|
|
\`\`\`
|
|
Keep an eye on the **Use%** column. If it reaches 100%, your services will start crashing.`
|
|
},
|
|
{
|
|
id: 'linux-cheat-sheet',
|
|
title: 'Basic Linux Cheat Sheet',
|
|
content: `# Basic Linux Commands
|
|
|
|
New to Linux? Here are the essential commands you will need.
|
|
|
|
## Navigation
|
|
- **\`ls\`**: List files in current folder.
|
|
- **\`cd folder\`**: Enter a folder.
|
|
- **\`cd ..\`**: Go back one folder.
|
|
- **\`pwd\`**: Show exactly where you are.
|
|
|
|
## File Management
|
|
- **\`nano file.txt\`**: Edit a text file.
|
|
- **\`mkdir name\`**: Create a new folder.
|
|
- **\`rm file\`**: Delete a file.
|
|
- **\`cp source dest\`**: Copy a file.
|
|
|
|
## System Tools
|
|
- **\`sudo ...\`**: Run a command as the boss (root).
|
|
- **\`top\`**: Show CPU and RAM usage.
|
|
- **\`df -h\`**: Show free disk space.
|
|
- **\`systemctl status name\`**: Check if a service is running.`
|
|
},
|
|
{
|
|
id: 'setup-wordpress-vps',
|
|
title: 'Installing WordPress on VPS',
|
|
content: `# Installing WordPress on VPS
|
|
|
|
## Step 1: Update your system
|
|
\`\`\`bash
|
|
sudo apt update && sudo apt upgrade -y
|
|
\`\`\`
|
|
|
|
## Step 2: Install LAMP Stack
|
|
\`\`\`bash
|
|
sudo apt install apache2 mariadb-server php libapache2-mod-php php-mysql -y
|
|
\`\`\`
|
|
|
|
## Step 3: Configure Database
|
|
\`\`\`sql
|
|
sudo mysql -u root
|
|
CREATE DATABASE wordpress;
|
|
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'password';
|
|
GRANT ALL ON wordpress.* TO 'wp_user'@'localhost';
|
|
FLUSH PRIVILEGES;
|
|
EXIT;
|
|
\`\`\`
|
|
|
|
## Step 4: Install WordPress
|
|
\`\`\`bash
|
|
cd /tmp && wget https://wordpress.org/latest.tar.gz
|
|
tar -xzvf latest.tar.gz
|
|
sudo cp -R wordpress/* /var/www/html/
|
|
sudo chown -R www-data:www-data /var/www/html/
|
|
\`\`\`
|
|
|
|
## Step 5: Finish
|
|
Restart Apache: \`sudo systemctl restart apache2\`. Visit your IP in the browser.`
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
const allArticles = sections.flatMap(s => s.articles);
|
|
const activeArticle = allArticles.find(a => a.id === activeArticleId) || allArticles[0];
|
|
|
|
const filteredSections = sections.map(section => ({
|
|
...section,
|
|
articles: section.articles.filter(article =>
|
|
article.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
article.content.toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
})).filter(section => section.articles.length > 0);
|
|
|
|
const handleArticleClick = (id) => {
|
|
setActiveArticleId(id);
|
|
setIsMobileMenuOpen(false);
|
|
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
const renderText = (text) => {
|
|
const parts = text.split(/(\*\*.*?\*\*|`.*?`)/g);
|
|
return parts.map((part, index) => {
|
|
if (part.startsWith('**') && part.endsWith('**')) {
|
|
return <strong key={index} className="text-dash-text font-extrabold">{part.slice(2, -2)}</strong>;
|
|
}
|
|
if (part.startsWith('`') && part.endsWith('`')) {
|
|
return <code key={index} className="text-primary bg-primary/10 px-1.5 py-0.5 rounded-lg font-mono text-[0.9em] border border-primary/10">{part.slice(1, -1)}</code>;
|
|
}
|
|
return part;
|
|
});
|
|
};
|
|
|
|
const renderContent = (content) => {
|
|
const lines = content.split('\n');
|
|
const rendered = [];
|
|
let inCodeBlock = false;
|
|
let codeBlockContent = [];
|
|
let codeLanguage = '';
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
if (line.startsWith('```')) {
|
|
if (inCodeBlock) {
|
|
rendered.push(
|
|
<div key={`code-${i}`} className="my-6 relative group">
|
|
<div className="absolute -top-3 left-4 px-2 py-0.5 rounded-md bg-dash-surface border border-dash-border text-[10px] font-bold uppercase tracking-widest text-dash-textMuted group-hover:text-primary transition-colors z-10">
|
|
{codeLanguage || 'code'}
|
|
</div>
|
|
<pre className="p-6 rounded-2xl bg-black/40 border border-dash-border overflow-x-auto custom-scrollbar font-mono text-sm leading-relaxed text-primary/90">
|
|
{codeBlockContent.join('\n')}
|
|
</pre>
|
|
</div>
|
|
);
|
|
inCodeBlock = false;
|
|
codeBlockContent = [];
|
|
} else {
|
|
inCodeBlock = true;
|
|
codeLanguage = line.slice(3).trim();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (inCodeBlock) {
|
|
codeBlockContent.push(line);
|
|
continue;
|
|
}
|
|
|
|
if (line.startsWith('# ')) {
|
|
rendered.push(<h1 key={i} className="text-4xl font-extrabold text-dash-text mb-10 tracking-tight">{renderText(line.replace('# ', ''))}</h1>);
|
|
} else if (line.startsWith('## ')) {
|
|
rendered.push(
|
|
<h2 key={i} className="text-2xl font-bold text-dash-text mt-14 mb-6 pb-3 border-b border-dash-border flex items-center gap-3">
|
|
<div className="w-1.5 h-6 bg-primary rounded-full" />
|
|
{renderText(line.replace('## ', ''))}
|
|
</h2>
|
|
);
|
|
} else if (line.startsWith('### ')) {
|
|
rendered.push(<h3 key={i} className="text-xl font-bold text-dash-text mt-10 mb-4">{renderText(line.replace('### ', ''))}</h3>);
|
|
} else if (line.startsWith('> ')) {
|
|
rendered.push(
|
|
<div key={i} className="my-8 p-5 rounded-2xl border border-primary/20 bg-primary/5 flex gap-4 text-dash-textMuted relative overflow-hidden group">
|
|
<div className="absolute top-0 left-0 w-1 h-full bg-primary/40 group-hover:bg-primary transition-colors" />
|
|
<Info className="w-5 h-5 text-primary shrink-0 mt-0.5" />
|
|
<div className="text-sm italic leading-relaxed font-medium">{renderText(line.replace('> ', ''))}</div>
|
|
</div>
|
|
);
|
|
} else if (line.startsWith('|')) {
|
|
if (line.includes('Default User')) continue;
|
|
if (line.includes(':---')) continue;
|
|
const cells = line.split('|').filter(c => c.trim() !== '');
|
|
if (cells.length >= 3) {
|
|
rendered.push(
|
|
<div key={i} className="grid grid-cols-3 gap-4 p-4 rounded-xl border border-dash-border bg-black/20 mb-2">
|
|
<span className="font-bold text-dash-text">{renderText(cells[0].trim())}</span>
|
|
<code className="text-primary bg-primary/5 px-2 py-1 rounded text-xs text-center">{cells[1].trim()}</code>
|
|
<code className="text-brightYellow bg-brightYellow/5 px-2 py-1 rounded text-xs text-center">{cells[2].trim()}</code>
|
|
</div>
|
|
);
|
|
}
|
|
} else if (line.trim() === '') {
|
|
rendered.push(<div key={i} className="h-4"></div>);
|
|
} else {
|
|
rendered.push(<p key={i} className="mb-5 leading-relaxed text-dash-textMuted/90 font-medium">{renderText(line)}</p>);
|
|
}
|
|
}
|
|
|
|
return rendered;
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-screen overflow-hidden bg-dash-surface font-sans selection:bg-primary/30 text-dash-textMuted antialiased">
|
|
{/* Sidebar */}
|
|
<aside className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated/50 backdrop-blur-md pt-[env(safe-area-inset-top,0px)] ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}`}>
|
|
<div className="flex flex-col h-full">
|
|
<div className="p-6 border-b border-dash-border bg-black/10">
|
|
<Link to="/" className="flex items-center space-x-3 group">
|
|
<div className="p-2 rounded-xl bg-primary/10 border border-primary/20 transition-all group-hover:scale-110 shadow-lg shadow-primary/5">
|
|
<img src={branding.logoPath} alt="" className="h-7 w-auto shrink-0" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold tracking-tight text-dash-text leading-tight">Docs</h1>
|
|
<p className="text-[10px] font-bold text-dash-textMuted/40 uppercase tracking-[0.2em]">Platform Wiki</p>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="p-4 border-b border-dash-border bg-black/5">
|
|
<div className="relative group">
|
|
<Search className="absolute left-3.5 top-3.5 w-4 h-4 text-dash-textMuted group-focus-within:text-primary transition-colors" />
|
|
<input
|
|
ref={searchInputRef}
|
|
type="text"
|
|
placeholder="Search resources..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full pl-11 pr-4 py-3 rounded-xl text-xs font-bold border border-dash-border bg-black/20 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:bg-black/40 transition-all text-dash-text"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="flex-1 overflow-y-auto py-4 px-4 space-y-8 custom-scrollbar bg-black/5">
|
|
{filteredSections.map((section) => (
|
|
<div key={section.id} className="space-y-2">
|
|
<div className="px-3 py-1 flex items-center gap-2 text-[10px] font-bold uppercase tracking-[0.3em] text-dash-textMuted/30">
|
|
<section.icon className="w-3.5 h-3.5" style={{ color: section.color }} />
|
|
{section.title}
|
|
</div>
|
|
{section.articles.map((article) => (
|
|
<button
|
|
key={article.id}
|
|
onClick={() => handleArticleClick(article.id)}
|
|
className={`w-full text-left px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 border ${
|
|
activeArticleId === article.id
|
|
? 'bg-primary/10 border-primary/25 text-dash-text shadow-lg shadow-primary/5'
|
|
: 'border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10'
|
|
}`}
|
|
>
|
|
{article.title}
|
|
</button>
|
|
))}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="p-6 border-t border-dash-border bg-black/20 pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))]">
|
|
<ActionButton
|
|
variant="secondary"
|
|
onClick={() => navigate(isAuthenticated ? '/dashboard' : '/')}
|
|
className="w-full !text-xs !py-3 !rounded-xl !font-bold"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Return to {isAuthenticated ? 'Dashboard' : 'Portal'}
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Mobile Overlay */}
|
|
{isMobileMenuOpen && (
|
|
<button
|
|
type="button"
|
|
className="fixed inset-0 z-40 bg-black/60 md:hidden backdrop-blur-sm transition-opacity duration-300"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
aria-label="Close menu"
|
|
/>
|
|
)}
|
|
|
|
<main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
|
|
<header className="md:hidden flex items-center justify-between p-4 border-b border-dash-border bg-dash-surfaceElevated/80 backdrop-blur-md z-40 pt-[calc(1rem+env(safe-area-inset-top,0px))]">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20 shadow-sm">
|
|
<Book className="w-5 h-5 text-primary" />
|
|
</div>
|
|
<span className="font-bold text-dash-text tracking-tight">Documentation</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
className="p-2.5 rounded-xl text-dash-textMuted hover:bg-white/10 transition-all border border-transparent hover:border-dash-border/20 active:scale-90"
|
|
>
|
|
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
|
</button>
|
|
</header>
|
|
|
|
<div className="flex-1 overflow-auto custom-scrollbar scroll-smooth bg-dash-surface" ref={scrollRef}>
|
|
<div className="max-w-4xl mx-auto p-6 md:p-12 lg:p-16">
|
|
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700">
|
|
<article className="prose prose-invert prose-primary max-w-none prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-4xl prose-h1:mb-10 prose-h2:text-2xl prose-h2:mt-14 prose-h2:mb-6 prose-h2:pb-3 prose-h2:border-b prose-h2:border-dash-border prose-p:text-dash-textMuted prose-p:leading-relaxed prose-p:text-base prose-strong:text-dash-text prose-code:text-primary prose-code:bg-primary/5 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-pre:bg-black/40 prose-pre:border prose-pre:border-dash-border prose-pre:rounded-2xl prose-table:border-dash-border prose-th:text-dash-text prose-th:uppercase prose-th:tracking-wider prose-th:text-[10px]">
|
|
<div className="documentation-content whitespace-pre-wrap">
|
|
{renderContent(activeArticle.content)}
|
|
</div>
|
|
</article>
|
|
|
|
<div className="mt-24 pt-10 border-t border-dash-border flex flex-col md:flex-row justify-between items-center gap-8 relative bg-black/5 p-8 rounded-3xl border border-dash-border shadow-2xl">
|
|
<div className="absolute top-0 left-12 w-24 h-[1px] bg-primary shadow-[0_0_15px_rgba(var(--color-primary),0.5)]" />
|
|
<div className="flex items-center gap-6">
|
|
<div className="p-4 rounded-2xl bg-dash-surfaceElevated border border-dash-border shadow-xl shadow-black/20 group hover:border-primary/30 transition-all">
|
|
<Headphones className="w-8 h-8 text-primary group-hover:scale-110 group-hover:rotate-6 transition-transform" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xl font-bold text-dash-text">Still need help?</p>
|
|
<p className="text-sm text-dash-textMuted font-bold opacity-70">Our support team is standing by to assist you.</p>
|
|
</div>
|
|
</div>
|
|
<ActionButton variant="primary" onClick={() => navigate('/dashboard')} className="w-full md:w-auto px-12 !h-14 !text-base !font-bold shadow-lg shadow-primary/10 transition-transform hover:scale-105 active:scale-95">
|
|
<MessageSquare className="w-5 h-5 mr-3" />
|
|
Open a Support Ticket
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default DocumentationPage;
|