Modernize frontend and centralize branding
This commit is contained in:
1 parent
bc4542d0fb
commit
2d61d80550
55 files changed
+1901
-14446
No files matched your search
+14
-13
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect, useCallback, Suspense } from 'react';
|
||||
import React, { useState, useEffect, useCallback, Suspense, lazy } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
|
||||
import DashboardPage from './pages/DashboardPage.js'
|
||||
import HomePage from './pages/HomePage.js'
|
||||
import AdminPage from './pages/AdminPage.js'
|
||||
import VerifyEmail from './pages/VerifyEmail.js'
|
||||
import TermsOfService from './pages/TermsOfService.js'
|
||||
import PrivacyPolicy from './pages/PrivacyPolicy.js'
|
||||
import PluginStore from './features/plugins/PluginStore.js'
|
||||
import PluginDetails from './features/plugins/PluginDetails.js'
|
||||
import DocumentationPage from './pages/DocumentationPage.js'
|
||||
const DashboardPage = lazy(() => import('./pages/DashboardPage.jsx'));
|
||||
const HomePage = lazy(() => import('./pages/HomePage.jsx'));
|
||||
const AdminPage = lazy(() => import('./pages/AdminPage.jsx'));
|
||||
const VerifyEmail = lazy(() => import('./pages/VerifyEmail.jsx'));
|
||||
const TermsOfService = lazy(() => import('./pages/TermsOfService.jsx'));
|
||||
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy.jsx'));
|
||||
const PluginStore = lazy(() => import('./features/plugins/PluginStore.jsx'));
|
||||
const PluginDetails = lazy(() => import('./features/plugins/PluginDetails.jsx'));
|
||||
const DocumentationPage = lazy(() => import('./pages/DocumentationPage.jsx'));
|
||||
import useWebSocket from './hooks/useWebSocket.js';
|
||||
import { colors } from './theme/colors';
|
||||
import { Dropdown } from './components/dashboardPrimitives';
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { API_BASE_URL, WS_BASE_URL as WS_URL } from './config/api';
|
||||
import branding from './branding';
|
||||
|
||||
// Auth context for user management
|
||||
const AuthContext = React.createContext({
|
||||
@@ -698,7 +699,7 @@ function LoginModal({ onClose, onLogin, onSwitchToRegister, colors, authConfig }
|
||||
<div className="p-10">
|
||||
<div className="flex flex-col items-center text-center mb-10">
|
||||
<div className="p-4 rounded-3xl mb-6 shadow-xl border" style={{ backgroundColor: `${colors.blue}15`, borderColor: `${colors.blue}30` }}>
|
||||
<img src="/assets/transparent_anchorhost.png" alt="AnchorHost.eu" className="h-10 w-auto" />
|
||||
<img src={branding.logoPath} alt={branding.name} className="h-10 w-auto" />
|
||||
</div>
|
||||
<h3 className="text-3xl font-black tracking-tighter" style={{ color: colors.brightWhite }}>
|
||||
{is2faStep ? 'Verify Identity' : 'Welcome Back'}
|
||||
@@ -828,7 +829,7 @@ function LoginModal({ onClose, onLogin, onSwitchToRegister, colors, authConfig }
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-xs font-bold" style={{ color: colors.foreground, opacity: 0.5 }}>
|
||||
New to AnchorHost.eu?{' '}
|
||||
New to {branding.name}?{' '}
|
||||
<button onClick={onSwitchToRegister} className="text-primary hover:underline ml-1">Create Account</button>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1085,7 +1086,7 @@ function RegisterModal({
|
||||
<Shield className="w-10 h-10" style={{ color: colors.brightGreen }} />
|
||||
</div>
|
||||
<h3 className="text-3xl font-black tracking-tighter" style={{ color: colors.brightWhite }}>Create Account</h3>
|
||||
<p className="text-sm font-bold uppercase tracking-widest opacity-70 mt-2" style={{ color: colors.foreground }}>Join AnchorHost.eu</p>
|
||||
<p className="text-sm font-bold uppercase tracking-widest opacity-70 mt-2" style={{ color: colors.foreground }}>Join {branding.name}</p>
|
||||
{error && (
|
||||
<p className="text-sm font-semibold mt-4" style={{ color: colors.brightRed }}>{error}</p>
|
||||
)}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Shared public identity for the app. To rebrand a deployment, update the
|
||||
* root branding.json and replace the logo at the configured public path.
|
||||
*/
|
||||
import branding from '../branding.json';
|
||||
|
||||
export default branding;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useId, useCallback, useMemo } from 'react';
|
||||
import branding from '../branding';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
CheckCircle, Search,
|
||||
@@ -270,11 +271,12 @@ export const UnifiedTooltip = ({
|
||||
? 'max-w-xs text-left leading-relaxed whitespace-normal break-words'
|
||||
: 'whitespace-nowrap';
|
||||
|
||||
const tooltipContent = isVisible ? (
|
||||
const tooltipContent = (
|
||||
<div
|
||||
id={tooltipId}
|
||||
role="tooltip"
|
||||
className={`ah-tooltip fixed z-[12000] px-3.5 py-2.5 text-sm pointer-events-none ${wrapClass}`}
|
||||
hidden={!isVisible}
|
||||
style={{
|
||||
top: getTopPosition(),
|
||||
left: `${position.left}px`,
|
||||
@@ -283,7 +285,14 @@ export const UnifiedTooltip = ({
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
const describedChildren = React.Children.map(children, (child) => {
|
||||
if (!React.isValidElement(child) || child.type === React.Fragment) return child;
|
||||
const describedBy = [child.props['aria-describedby'], tooltipId]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
return React.cloneElement(child, { 'aria-describedby': describedBy || undefined });
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -295,7 +304,7 @@ export const UnifiedTooltip = ({
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
{children}
|
||||
{describedChildren}
|
||||
</div>
|
||||
{tooltipContent && createPortal(tooltipContent, document.body)}
|
||||
</>
|
||||
@@ -1115,7 +1124,7 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<SettingsPanel title="Domain Details" description="Choose a unique subdomain. It will end with .anchorhost.eu">
|
||||
<SettingsPanel title="Domain Details" description={`Choose a unique subdomain. It will end with .${branding.domain}`}>
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -1127,7 +1136,7 @@ export const DomainManager = ({ userId, username, servers, sendMessage, showAler
|
||||
style={{ color: colors.brightWhite }}
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-black opacity-30 uppercase tracking-widest pointer-events-none">
|
||||
.anchorhost.eu
|
||||
.{branding.domain}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File renamed without changes.
@@ -74,7 +74,7 @@ export function StatTile({ icon: Icon, label, value, accentColor = colors.blue,
|
||||
);
|
||||
}
|
||||
|
||||
const actionBase = `inline-flex items-center justify-center gap-2 rounded-sm text-sm font-semibold transition-colors duration-150 motion-reduce:transition-none ${focusRing} min-h-[44px] sm:min-h-[40px] px-5 py-3 sm:py-2.5 shadow-[3px_3px_0_#2F2F2F] disabled:opacity-40 disabled:pointer-events-none active:translate-x-[3px] active:translate-y-[3px] active:shadow-none`;
|
||||
const actionBase = `inline-flex items-center justify-center gap-2 rounded-sm text-sm font-semibold transition-colors duration-150 motion-reduce:transition-none ${focusRing} min-h-[44px] sm:min-h-[40px] px-5 py-3 sm:py-2.5 disabled:opacity-40 disabled:pointer-events-none`;
|
||||
|
||||
/**
|
||||
* Inset panel for settings rows (matches dash border / surface).
|
||||
File renamed without changes.
File renamed without changes.
+6
-4
@@ -1,17 +1,19 @@
|
||||
/**
|
||||
* API / WebSocket base URLs for local dev and production.
|
||||
* Set REACT_APP_API_URL and REACT_APP_WS_URL in .env (see .env.example).
|
||||
* Set VITE_API_URL, VITE_WS_URL, and VITE_SITE_URL in .env (see .env.example).
|
||||
*/
|
||||
import branding from '../branding';
|
||||
|
||||
const trimTrailingSlash = (url) => (url || '').replace(/\/$/, '');
|
||||
|
||||
export const API_BASE_URL = trimTrailingSlash(
|
||||
process.env.REACT_APP_API_URL || 'https://anchorhost.eu'
|
||||
import.meta.env.VITE_API_URL || branding.siteUrl
|
||||
);
|
||||
|
||||
export const WS_BASE_URL = trimTrailingSlash(
|
||||
process.env.REACT_APP_WS_URL || 'wss://anchorhost.eu/ws'
|
||||
import.meta.env.VITE_WS_URL || `wss://${branding.domain}/ws`
|
||||
);
|
||||
|
||||
export const SITE_BASE_URL = trimTrailingSlash(
|
||||
process.env.REACT_APP_SITE_URL || API_BASE_URL
|
||||
import.meta.env.VITE_SITE_URL || branding.siteUrl
|
||||
);
|
||||
@@ -13,6 +13,7 @@ import ServerDetailsModal from '../servers/ServerDetailsModal';
|
||||
import NodeTerminalModal from './NodeTerminalModal';
|
||||
import TicketSystem from '../support/TicketSystem';
|
||||
import { colors } from '../../theme/colors';
|
||||
import branding from '../../branding';
|
||||
import { API_BASE_URL } from '../../config/api';
|
||||
import {
|
||||
StatTile,
|
||||
@@ -1121,7 +1122,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
>
|
||||
<div className="mb-8">
|
||||
<p className="text-sm text-dash-textMuted leading-relaxed max-w-2xl">
|
||||
Select three customer sites to feature on the public homepage. Use full URLs (e.g., https://site.anchorhost.eu). Leaving a URL empty will hide that slot.
|
||||
Select three customer sites to feature on the public homepage. Use full URLs (e.g., https://site.{branding.domain}). Leaving a URL empty will hide that slot.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import branding from '../../branding';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
Terminal as TerminalIcon,
|
||||
@@ -58,7 +59,7 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
const connectTerminal = useCallback(() => {
|
||||
if (!node || !sendMessage) return;
|
||||
setIsLoading(true);
|
||||
setTerminalOutput(prev => prev + `\r\n\x1b[36m[AnchorHost.eu]\x1b[0m Requesting shell session on node \x1b[1m${node.name}\x1b[0m...\r\n`);
|
||||
setTerminalOutput(prev => prev + `\r\n\x1b[36m[${branding.name}]\x1b[0m Requesting shell session on node \x1b[1m${node.name}\x1b[0m...\r\n`);
|
||||
sendMessage({
|
||||
type: 'node_terminal_connect',
|
||||
nodeName: node.name
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { Calendar, Tag, ChevronRight, X, User } from "lucide-react";
|
||||
import { colors } from "../../theme/colors";
|
||||
import { API_BASE_URL } from '../../config/api';
|
||||
import branding from '../../branding';
|
||||
|
||||
const AnnouncementModal = ({ announcement, onClose }) => {
|
||||
const dialogRef = useRef(null);
|
||||
@@ -194,7 +195,7 @@ const AnnouncementsSection = () => {
|
||||
<section id="announcements" className="site-section site-announcements">
|
||||
<div className="site-section-heading">
|
||||
<div><p className="site-kicker">Platform updates</p><h2>Latest announcements.</h2></div>
|
||||
<p>Service changes and notes from the AnchorHost.eu team.</p>
|
||||
<p>Service changes and notes from the {branding.name} team.</p>
|
||||
</div>
|
||||
<div className="site-announcement-list">
|
||||
{displayedAnnouncements.map((item) => (
|
||||
+4
-3
@@ -4,6 +4,7 @@ import { ArrowLeft } from 'lucide-react';
|
||||
import { DashboardViewShell, PageHeaderBar, ActionButton } from '../../components/dashboardPrimitives';
|
||||
import { dashLayout } from '../../theme/dashboardTokens';
|
||||
import { colors } from '../../theme/colors';
|
||||
import branding from '../../branding';
|
||||
|
||||
/**
|
||||
* Shared chrome for Terms / Privacy: dashboard shell, dash borders, typography.
|
||||
@@ -26,9 +27,9 @@ export function LegalPageShell({ title, icon: Icon, accentColor = colors.blue, l
|
||||
className="flex items-center gap-3 min-w-0 text-left rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-white/5 border border-white/10 group-hover:scale-110 transition-transform">
|
||||
<img src="./assets/transparent_anchorhost.png" alt="" className="h-6 w-auto shrink-0" />
|
||||
<img src={branding.logoPath} alt="" className="h-6 w-auto shrink-0" />
|
||||
</div>
|
||||
<span className="text-xl font-bold text-dash-text truncate tracking-tight">AnchorHost.eu</span>
|
||||
<span className="text-xl font-bold text-dash-text truncate tracking-tight">{branding.name}</span>
|
||||
</button>
|
||||
<ActionButton variant="secondary" className="!min-h-0 !px-4 !py-2.5 !text-sm !font-bold !rounded-xl" onClick={() => navigate('/')}>
|
||||
<ArrowLeft className="w-4 h-4 shrink-0 mr-2" aria-hidden />
|
||||
@@ -51,7 +52,7 @@ export function LegalPageShell({ title, icon: Icon, accentColor = colors.blue, l
|
||||
|
||||
<div className="text-center pt-8 border-t border-dash-border">
|
||||
<p className="text-xs font-bold text-dash-textMuted uppercase tracking-[0.3em] opacity-40">
|
||||
© {new Date().getFullYear()} AnchorHost.eu • High-Performance Cloud Solutions
|
||||
© {new Date().getFullYear()} {branding.name} • {branding.footerDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,6 +5,7 @@ import { colors } from '../../theme/colors';
|
||||
import { Dropdown } from '../../components/dashboardPrimitives';
|
||||
|
||||
import { API_BASE_URL, WS_BASE_URL as WS_URL } from '../../config/api';
|
||||
import branding from '../../branding';
|
||||
|
||||
function PluginDetails() {
|
||||
const { pluginId } = useParams();
|
||||
@@ -34,7 +35,7 @@ function PluginDetails() {
|
||||
`${API_BASE_URL}/api/modrinth/project/${pluginId}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'AnchorHost.eu/1.0'
|
||||
'User-Agent': `${branding.name}/1.0`
|
||||
}
|
||||
}
|
||||
);
|
||||
File renamed without changes.
+3
-2
@@ -5,6 +5,7 @@ import {
|
||||
import { colors, Dropdown, SectionCard, minecraftVersions } from '../../components/DashboardComponents';
|
||||
|
||||
import { API_BASE_URL } from '../../config/api';
|
||||
import branding from '../../branding';
|
||||
|
||||
// Embedded Plugin Store Component for Dashboard
|
||||
const PluginStoreEmbedded = ({ serverId }) => {
|
||||
@@ -34,7 +35,7 @@ const PluginStoreEmbedded = ({ serverId }) => {
|
||||
`${API_BASE_URL}/api/modrinth/project/${pluginId}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'AnchorHost.eu/1.0'
|
||||
'User-Agent': `${branding.name}/1.0`
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -398,4 +399,4 @@ const PluginStoreEmbedded = ({ serverId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginStoreEmbedded;
|
||||
export default PluginStoreEmbedded;
|
||||
File renamed without changes.
@@ -4,7 +4,7 @@ import {
|
||||
Folder, File, FolderOpen, Upload, Download, Edit, FolderPlus, FileText, Archive, Image,
|
||||
ChevronLeft, ChevronRight, Search, Check, Trash2, Copy, X, RefreshCw, CheckCircle
|
||||
} from "lucide-react";
|
||||
import { colors, UnifiedTooltip, CustomTooltip } from '../../components/DashboardComponents.js';
|
||||
import { colors, UnifiedTooltip, CustomTooltip } from '../../components/DashboardComponents.jsx';
|
||||
|
||||
// File Manager Component with forwardRef
|
||||
const FileManager = forwardRef(({ serverId, serverImage, sendMessage, isActive, connectionStatus, showAlert }, ref) => {
|
||||
File renamed without changes.
@@ -0,0 +1,196 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Clock,
|
||||
Coins,
|
||||
Cpu,
|
||||
Database,
|
||||
Gamepad2,
|
||||
Globe,
|
||||
HardDrive,
|
||||
MemoryStick,
|
||||
Play,
|
||||
Server,
|
||||
Square,
|
||||
Terminal,
|
||||
} from 'lucide-react';
|
||||
import { ActionButton, getServerStatusInfo } from '../../components/DashboardComponents.jsx';
|
||||
import { colors } from '../../theme/colors.js';
|
||||
|
||||
const serverTypeIcons = {
|
||||
vps: { icon: Server, color: colors.blue },
|
||||
game: { icon: Gamepad2, color: colors.red },
|
||||
website: { icon: Globe, color: colors.green },
|
||||
database: { icon: Database, color: colors.magenta },
|
||||
};
|
||||
|
||||
function calculateServerCost(server, pricing) {
|
||||
if (server.is_demo === true || server.is_demo === 1) return 0;
|
||||
const cpuCost = (parseInt(server.cpu, 10) || 0) * (pricing.price_cpu || 35);
|
||||
const ramCost = (parseInt(server.ram, 10) || 0) * (pricing.price_ram || 35);
|
||||
const storageCost = (parseInt(server.storage, 10) || 0) * (pricing.price_storage || 5);
|
||||
const typeMultiplier = {
|
||||
vps: pricing.multiplier_vps || 1,
|
||||
game: pricing.multiplier_game || 0.8,
|
||||
website: pricing.multiplier_website || 1.5,
|
||||
database: pricing.multiplier_database || 1.3,
|
||||
}[server.type] || 1;
|
||||
|
||||
return Math.round((cpuCost + ramCost + storageCost) * typeMultiplier);
|
||||
}
|
||||
|
||||
export default function ServerCard({
|
||||
server,
|
||||
pricing,
|
||||
userRole,
|
||||
calculateCurrencyValue,
|
||||
getCurrencySymbol,
|
||||
onConvertDemo,
|
||||
onOpenConsole,
|
||||
onServerAction,
|
||||
}) {
|
||||
const statusInfo = getServerStatusInfo(server.status);
|
||||
const { icon: ServerIcon, color: serverColor } = serverTypeIcons[server.type] || serverTypeIcons.vps;
|
||||
const isDatabase = server.type === 'database';
|
||||
const dbType = server.image || 'Unknown';
|
||||
const isDemo = server.is_demo === true || server.is_demo === 1;
|
||||
const monthlyCost = calculateServerCost(server, pricing);
|
||||
const [timeRemaining, setTimeRemaining] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDemo || !server.demo_expires_at) {
|
||||
setTimeRemaining(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateTimer = () => {
|
||||
const expiresAt = new Date(server.demo_expires_at);
|
||||
const difference = expiresAt.getTime() - Date.now();
|
||||
if (Number.isNaN(expiresAt.getTime())) {
|
||||
setTimeRemaining(null);
|
||||
} else if (difference <= 0) {
|
||||
setTimeRemaining('Expired');
|
||||
} else {
|
||||
const hours = Math.floor(difference / 3_600_000);
|
||||
const minutes = Math.floor((difference % 3_600_000) / 60_000);
|
||||
const seconds = Math.floor((difference % 60_000) / 1000);
|
||||
setTimeRemaining(`${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`);
|
||||
}
|
||||
};
|
||||
|
||||
updateTimer();
|
||||
const interval = window.setInterval(updateTimer, 1000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [isDemo, server.demo_expires_at]);
|
||||
|
||||
return (
|
||||
<article className="ah-server-card relative h-full" style={{ '--server-accent': serverColor }}>
|
||||
<div className="ah-server-card-content">
|
||||
<header className="ah-server-card-header">
|
||||
<div className="ah-server-card-identity">
|
||||
<div className="ah-server-type-icon" aria-hidden="true">
|
||||
<ServerIcon className="w-5 h-5" style={{ color: serverColor }} />
|
||||
</div>
|
||||
<div className="ah-server-card-heading">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<h3 className="ah-server-name" title={server.name}>{server.name}</h3>
|
||||
{isDemo && <span className="ah-server-demo-tag">DEMO</span>}
|
||||
</div>
|
||||
<p className="ah-server-type-label">{isDatabase ? `${dbType} db` : `${server.type} node`}</p>
|
||||
{isDemo && timeRemaining && (
|
||||
<div className="ah-server-demo-time">
|
||||
<Clock className="w-3 h-3" aria-hidden="true" />
|
||||
<p>{timeRemaining === 'Expired' ? 'EXPIRED' : timeRemaining}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="ah-server-status"
|
||||
aria-label={`Server status: ${statusInfo.label}`}
|
||||
style={{
|
||||
backgroundColor: `${statusInfo.color}15`,
|
||||
borderColor: statusInfo.color,
|
||||
color: statusInfo.color,
|
||||
}}
|
||||
>
|
||||
{statusInfo.label}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="ah-server-resource-grid">
|
||||
<div className="ah-server-resource">
|
||||
<div className="ah-server-resource-label"><Cpu className="w-4 h-4" aria-hidden="true" /><span>CPU</span></div>
|
||||
<p>{server.cpu}</p>
|
||||
</div>
|
||||
<div className="ah-server-resource">
|
||||
<div className="ah-server-resource-label"><MemoryStick className="w-4 h-4" aria-hidden="true" /><span>Memory</span></div>
|
||||
<p>{server.ram}</p>
|
||||
</div>
|
||||
<div className="ah-server-resource">
|
||||
<div className="ah-server-resource-label"><HardDrive className="w-4 h-4" aria-hidden="true" /><span>Storage</span></div>
|
||||
<p>{server.storage}</p>
|
||||
</div>
|
||||
<div className="ah-server-resource ah-server-cost">
|
||||
<div className="ah-server-resource-label"><Coins className="w-4 h-4" aria-hidden="true" /><span>Monthly cost</span></div>
|
||||
<p className="ah-server-cost-value">
|
||||
{isDemo ? 'FREE' : `${calculateCurrencyValue(monthlyCost)} ${getCurrencySymbol()}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ah-server-card-actions">
|
||||
{isDemo && userRole !== 'admin' ? (
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
onClick={() => onConvertDemo(server.id)}
|
||||
className="flex-1 !min-h-0 !py-3"
|
||||
style={{ backgroundColor: colors.brightYellow, borderColor: colors.brightYellow, color: colors.black }}
|
||||
>
|
||||
<Coins className="w-4 h-4" />
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">Convert to paid</span>
|
||||
</ActionButton>
|
||||
) : (
|
||||
<>
|
||||
<ActionButton variant="secondary" onClick={() => onOpenConsole(server)} className="flex-1 !min-h-0 !py-3">
|
||||
<Terminal className="w-4 h-4" />
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">Console</span>
|
||||
</ActionButton>
|
||||
{isDemo && (
|
||||
<ActionButton
|
||||
variant="warning"
|
||||
onClick={() => onConvertDemo(server.id)}
|
||||
className="aspect-square !p-0 !min-w-[48px]"
|
||||
title="Convert demo to paid"
|
||||
aria-label={`Convert ${server.name} demo to paid`}
|
||||
>
|
||||
<Coins className="w-4 h-4 text-yellow-400" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{server.status === 'Running' ? (
|
||||
<ActionButton
|
||||
variant="danger"
|
||||
onClick={() => onServerAction(server.id, 'stop')}
|
||||
className="aspect-square !p-0 !min-w-[48px]"
|
||||
title={`Stop ${server.name}`}
|
||||
aria-label={`Stop ${server.name}`}
|
||||
>
|
||||
<Square className="w-4 h-4 fill-current" />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
variant="success"
|
||||
onClick={() => onServerAction(server.id, 'start')}
|
||||
className="aspect-square !p-0 !min-w-[48px]"
|
||||
title={`Start ${server.name}`}
|
||||
aria-label={`Start ${server.name}`}
|
||||
>
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
+2
-1
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import branding from '../../branding';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
Server, Terminal, Play, Square, RefreshCw, Trash2, Maximize2, Minimize2, X,
|
||||
@@ -888,7 +889,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Automated Firewall</p>
|
||||
<p className="text-[10px] opacity-40 leading-relaxed">The AnchorHost.eu firewall automatically opens all exposed ports in real-time.</p>
|
||||
<p className="text-[10px] opacity-40 leading-relaxed">The {branding.name} firewall automatically opens all exposed ports in real-time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File renamed without changes.
@@ -16,6 +16,7 @@ import {
|
||||
} from '../../components/DashboardComponents';
|
||||
|
||||
import { API_BASE_URL } from '../../config/api';
|
||||
import branding from '../../branding';
|
||||
|
||||
const currencyFlags = {
|
||||
EUR: '🇪🇺',
|
||||
@@ -203,7 +204,7 @@ function StoreSection({ userCredits, onAddCredits, onRefreshCredits, selectedCur
|
||||
<SectionCard title="What are Credits?" icon={Info} color={colors.blue}>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm leading-relaxed" style={{ color: colors.foreground }}>
|
||||
Credits are the <span className="text-brightWhite font-bold">universal currency</span> of AnchorHost.eu.
|
||||
Credits are the <span className="text-brightWhite font-bold">universal currency</span> of {branding.name}.
|
||||
They allow you to pay for all our services with maximum flexibility.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
@@ -1,7 +1,7 @@
|
||||
// TicketSystem.js - Updated with auto-reloading feature
|
||||
// TicketSystem.jsx - Updated with auto-reloading feature
|
||||
import React, { useState, useEffect, useContext, useRef, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { AuthContext } from '../../App.js';
|
||||
import { AuthContext } from '../../App.jsx';
|
||||
import useWebSocket from '../../hooks/useWebSocket.js';
|
||||
import {
|
||||
MessageCircle, Plus, Search, Filter, Clock, AlertCircle, CheckCircle,
|
||||
+9
-11
@@ -1,6 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@@ -244,11 +243,11 @@ a.focus-skip-global:focus-visible {
|
||||
--ah-border: #2F2F2F;
|
||||
--ah-focus: #454545;
|
||||
--ah-text: #D0D0D0;
|
||||
--ah-muted: #757575;
|
||||
--ah-muted: #898989;
|
||||
--ah-dim: #505050;
|
||||
--ah-emphasis: #F5F5F5;
|
||||
--ah-accent: #6C99BB;
|
||||
--ah-accent-secondary: #547C99;
|
||||
--ah-accent-secondary: #6C99BB;
|
||||
--ah-warning: #D3A04D;
|
||||
--ah-shadow: 4px 4px 0 var(--ah-border);
|
||||
}
|
||||
@@ -375,7 +374,7 @@ body { color: var(--ah-text); background: var(--ah-background); }
|
||||
.ah-dropdown-empty { padding: 14px 12px; color: var(--ah-muted); font-size: 13px; }
|
||||
.ah-inline-dropdown { display: flex; align-items: center; gap: 10px; width: 100%; min-width: 0; }
|
||||
.ah-inline-dropdown-actions { display: flex; flex: 0 0 auto; gap: 6px; }
|
||||
.ah-dropdown-action { width: 42px; height: 42px; display: grid; place-items: center; border: 2px solid var(--ah-border); border-radius: 2px; background: var(--ah-panel); box-shadow: 3px 3px 0 var(--ah-border); cursor: pointer; }
|
||||
.ah-dropdown-action { width: 42px; height: 42px; display: grid; place-items: center; border: 2px solid var(--ah-border); border-radius: 2px; background: var(--ah-panel); cursor: pointer; }
|
||||
.ah-dropdown-action--save { color: #7B9246; }
|
||||
.ah-dropdown-action--cancel { color: #D3A04D; }
|
||||
.ah-dropdown-action:hover { background: var(--ah-hover); }
|
||||
@@ -442,7 +441,7 @@ body { color: var(--ah-text); background: var(--ah-background); }
|
||||
.app-container { background: var(--ah-background); color: var(--ah-text); }
|
||||
.app-container [class*="backdrop-blur"] { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
|
||||
.app-container [class*="rounded-"] { border-radius: 2px !important; }
|
||||
.app-container [class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
|
||||
.app-container :not(button):not(a)[class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
|
||||
.app-container [class*="hover:scale"] { transform: none !important; }
|
||||
.app-container .uppercase { letter-spacing: .02em; }
|
||||
.app-container .text-\[10px\] { font-size: 12px; }
|
||||
@@ -451,7 +450,8 @@ body { color: var(--ah-text); background: var(--ah-background); }
|
||||
.app-container :is(button, a):focus-visible, .app-container :is(input, textarea, select):focus-visible { outline: 2px solid var(--ah-accent); outline-offset: 2px; }
|
||||
/* React portals (menus and dialogs) mount beside #root, so carry the same theme there. */
|
||||
body > div:not(#root) [class*="rounded-"] { border-radius: 2px !important; }
|
||||
body > div:not(#root) [class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
|
||||
body > div:not(#root) :not(button):not(a)[class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
|
||||
body > div:not(#root) :is(button, a)[class*="shadow-"] { box-shadow: none !important; transform: none !important; }
|
||||
body > div:not(#root) [class*="backdrop-blur"] { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
|
||||
body > div:not(#root) :is(button, a, input, textarea, select):focus-visible { outline: 2px solid var(--ah-accent); outline-offset: 2px; }
|
||||
.dashboard-app, .admin-app { background: var(--ah-background) !important; color: var(--ah-text); }
|
||||
@@ -485,9 +485,7 @@ body > div:not(#root) :is(button, a, input, textarea, select):focus-visible { ou
|
||||
.ah-server-cost-value { color: var(--ah-emphasis) !important; }
|
||||
.ah-server-card-actions { display: flex; gap: 8px; padding: 14px 16px; border-top: 2px solid var(--ah-border); background: var(--ah-surface); }
|
||||
.ah-server-card-actions > button { min-height: 42px; }
|
||||
.app-container .ah-server-card-actions .ah-server-action-button { box-shadow: none !important; transform: none !important; }
|
||||
.app-container .ah-server-card-actions .ah-server-action-button:hover { box-shadow: none !important; transform: none !important; }
|
||||
.app-container .ah-server-card-actions .ah-server-action-button:active { box-shadow: none !important; transform: none !important; }
|
||||
.app-container :is(button, a)[class*="shadow-"] { box-shadow: none !important; transform: none !important; }
|
||||
|
||||
/* Opaque tooltip panels use the same ink border and offset shadow as other surfaces. */
|
||||
.ah-tooltip { max-width: min(24rem, calc(100vw - 2rem)); border: 2px solid var(--ah-border); background: var(--ah-panel); color: var(--ah-emphasis); box-shadow: 5px 5px 0 var(--ah-border); line-height: 1.5; overflow-wrap: anywhere; }
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -26,6 +26,7 @@ import AdminSection from '../features/admin/AdminSection';
|
||||
import { colors } from '../theme/colors';
|
||||
import useWebSocket from '../hooks/useWebSocket';
|
||||
import { API_BASE_URL, WS_BASE_URL } from '../config/api';
|
||||
import branding from '../branding';
|
||||
import {
|
||||
ActionButton,
|
||||
UnifiedTooltip
|
||||
@@ -130,7 +131,7 @@ const AdminPage = () => {
|
||||
<div className="p-6 border-b border-dash-border bg-black/10">
|
||||
<div className="flex items-center space-x-3 group cursor-pointer" onClick={() => navigate('/')}>
|
||||
<div className="p-2 rounded-xl bg-brightRed/10 border border-brightRed/20 transition-all group-hover:scale-110 shadow-lg shadow-brightRed/5">
|
||||
<img src="/assets/transparent_anchorhost.png" alt="" className="h-7 w-auto shrink-0" />
|
||||
<img src={branding.logoPath} alt="" className="h-7 w-auto shrink-0" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-bold tracking-tight text-dash-text truncate">
|
||||
@@ -23,12 +23,13 @@ import {
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
|
||||
import ServerCreationForm from '../features/servers/ServerCreationForm.js'
|
||||
import StoreSection from '../features/store/StoreSection.js'
|
||||
import TicketSystem from '../features/support/TicketSystem.js'
|
||||
import ServerCreationForm from '../features/servers/ServerCreationForm.jsx'
|
||||
import ServerCard from '../features/servers/ServerCard.jsx'
|
||||
import StoreSection from '../features/store/StoreSection.jsx'
|
||||
import TicketSystem from '../features/support/TicketSystem.jsx'
|
||||
import useWebSocket from '../hooks/useWebSocket.js';
|
||||
import { AuthContext } from '../App.js';
|
||||
import GameConsole from '../features/servers/GameConsole.js';
|
||||
import { AuthContext } from '../App.jsx';
|
||||
import GameConsole from '../features/servers/GameConsole.jsx';
|
||||
import {
|
||||
colors,
|
||||
parseServerDate,
|
||||
@@ -51,15 +52,16 @@ import {
|
||||
DashboardEmptyState,
|
||||
DashboardLoadingSpinner,
|
||||
SettingsPanel,
|
||||
} from '../components/DashboardComponents.js';
|
||||
} from '../components/DashboardComponents.jsx';
|
||||
import { dashLayout } from '../theme/dashboardTokens.js';
|
||||
import FileManager from '../features/servers/FileManager.js';
|
||||
import BackupSection from '../features/servers/BackupSection.js';
|
||||
import PluginStoreEmbedded from '../features/plugins/PluginStoreEmbedded.js';
|
||||
import FileManager from '../features/servers/FileManager.jsx';
|
||||
import BackupSection from '../features/servers/BackupSection.jsx';
|
||||
import PluginStoreEmbedded from '../features/plugins/PluginStoreEmbedded.jsx';
|
||||
import { usePanelCurrency } from '../hooks/usePanelCurrency.js';
|
||||
|
||||
|
||||
import { API_BASE_URL, WS_BASE_URL as WS_URL } from '../config/api';
|
||||
import branding from '../branding';
|
||||
|
||||
const NODE_IP = '85.237.179.121';
|
||||
|
||||
@@ -1885,212 +1887,6 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Server tiles keep the name and status in separate columns so long names cannot hide state.
|
||||
const ServerCard = ({ server }) => {
|
||||
const statusInfo = getServerStatusInfo(server.status);
|
||||
const serverTypeIcons = {
|
||||
'vps': { icon: Server, color: colors.blue },
|
||||
'game': { icon: Gamepad2, color: colors.red },
|
||||
'website': { icon: Globe, color: colors.green },
|
||||
'database': { icon: Database, color: colors.magenta }
|
||||
};
|
||||
const ServerIcon = serverTypeIcons[server.type]?.icon || Server;
|
||||
const serverColor = serverTypeIcons[server.type]?.color || colors.blue;
|
||||
|
||||
const isDatabase = server.type === 'database';
|
||||
const dbType = server.image || 'Unknown';
|
||||
const isDemo = server.is_demo === true || server.is_demo === 1;
|
||||
|
||||
const calculateServerCost = (server) => {
|
||||
if (isDemo) return 0;
|
||||
const cpuCores = parseInt(server.cpu) || 0;
|
||||
const ramGb = parseInt(server.ram) || 0;
|
||||
const storageGb = parseInt(server.storage) || 0;
|
||||
const cpuCost = cpuCores * (pricing.price_cpu || 35);
|
||||
const ramCost = ramGb * (pricing.price_ram || 35);
|
||||
const storageCost = storageGb * (pricing.price_storage || 5);
|
||||
const typeMultiplier = {
|
||||
'vps': pricing.multiplier_vps || 1.0,
|
||||
'game': pricing.multiplier_game || 0.8,
|
||||
'website': pricing.multiplier_website || 1.5,
|
||||
'database': pricing.multiplier_database || 1.3
|
||||
}[server.type] || 1.0;
|
||||
return Math.round((cpuCost + ramCost + storageCost) * typeMultiplier);
|
||||
};
|
||||
const serverMonthlyCost = calculateServerCost(server);
|
||||
|
||||
// Demo countdown timer
|
||||
const [timeRemaining, setTimeRemaining] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDemo || !server.demo_expires_at) {
|
||||
setTimeRemaining(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateTimer = () => {
|
||||
const expiresAt = new Date(server.demo_expires_at);
|
||||
const now = new Date();
|
||||
if (isNaN(expiresAt.getTime())) { setTimeRemaining(null); return; }
|
||||
const diff = expiresAt - now;
|
||||
if (diff <= 0) { setTimeRemaining('Expired'); return; }
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
setTimeRemaining(`${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
|
||||
};
|
||||
|
||||
updateTimer();
|
||||
const interval = setInterval(updateTimer, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isDemo, server.demo_expires_at]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ah-server-card relative h-full"
|
||||
style={{
|
||||
'--server-accent': serverColor,
|
||||
}}
|
||||
>
|
||||
<div className="ah-server-card-content">
|
||||
<div className="ah-server-card-header">
|
||||
<div className="ah-server-card-identity">
|
||||
<div className="ah-server-type-icon" aria-hidden="true">
|
||||
<ServerIcon className="w-5 h-5" style={{ color: serverColor }} />
|
||||
</div>
|
||||
<div className="ah-server-card-heading">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="ah-server-name" title={server.name}>{server.name}</h3>
|
||||
{isDemo && (
|
||||
<span className="ah-server-demo-tag">
|
||||
DEMO
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="ah-server-type-label">
|
||||
{isDatabase ? `${dbType} db` : `${server.type} node`}
|
||||
</p>
|
||||
{isDemo && timeRemaining && (
|
||||
<div className="ah-server-demo-time">
|
||||
<Clock className="w-3 h-3" style={{ color: colors.brightYellow }} />
|
||||
<p>
|
||||
{timeRemaining === 'Expired' ? 'EXPIRED' : timeRemaining}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ah-server-status"
|
||||
aria-label={`Server status: ${statusInfo.label}`}
|
||||
style={{
|
||||
backgroundColor: `${statusInfo.color}15`,
|
||||
borderColor: statusInfo.color,
|
||||
color: statusInfo.color,
|
||||
}}
|
||||
>
|
||||
{statusInfo.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ah-server-resource-grid">
|
||||
<div className="ah-server-resource">
|
||||
<div className="ah-server-resource-label">
|
||||
<Cpu className="w-4 h-4" aria-hidden="true" />
|
||||
<span>CPU</span>
|
||||
</div>
|
||||
<p>{server.cpu}</p>
|
||||
</div>
|
||||
|
||||
<div className="ah-server-resource">
|
||||
<div className="ah-server-resource-label">
|
||||
<MemoryStick className="w-4 h-4" aria-hidden="true" />
|
||||
<span>Memory</span>
|
||||
</div>
|
||||
<p>{server.ram}</p>
|
||||
</div>
|
||||
|
||||
<div className="ah-server-resource">
|
||||
<div className="ah-server-resource-label">
|
||||
<HardDrive className="w-4 h-4" aria-hidden="true" />
|
||||
<span>Storage</span>
|
||||
</div>
|
||||
<p>{server.storage}</p>
|
||||
</div>
|
||||
|
||||
<div className="ah-server-resource ah-server-cost">
|
||||
<div className="ah-server-resource-label">
|
||||
<Coins className="w-4 h-4" aria-hidden="true" />
|
||||
<span>Monthly cost</span>
|
||||
</div>
|
||||
<p className="ah-server-cost-value">
|
||||
{isDemo ? 'FREE' : `${calculateCurrencyValue(serverMonthlyCost)} ${getCurrencySymbol()}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ah-server-card-actions">
|
||||
{isDemo && user?.role !== 'admin' ? (
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
onClick={() => handleConvertDemoToPaid(server.id)}
|
||||
className="ah-server-action-button flex-1 !min-h-0 !py-3"
|
||||
style={{ backgroundColor: colors.brightYellow, borderColor: colors.brightYellow, color: colors.black }}
|
||||
>
|
||||
<Coins className="w-4 h-4" />
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">Convert to paid</span>
|
||||
</ActionButton>
|
||||
) : (
|
||||
<>
|
||||
<ActionButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setSelectedServer(server);
|
||||
setActiveServerDetailTab('overview');
|
||||
fetchServerLogs(server.id);
|
||||
}}
|
||||
className="ah-server-action-button flex-1 !min-h-0 !py-3"
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">Console</span>
|
||||
</ActionButton>
|
||||
|
||||
{isDemo && (
|
||||
<ActionButton
|
||||
variant="warning"
|
||||
onClick={() => handleConvertDemoToPaid(server.id)}
|
||||
className="ah-server-action-button aspect-square !p-0 !min-w-[48px]"
|
||||
title="Convert demo to paid"
|
||||
>
|
||||
<Coins className="w-4 h-4 text-yellow-400" />
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
{server.status === 'Running' ? (
|
||||
<ActionButton
|
||||
variant="danger"
|
||||
onClick={() => handleServerAction(server.id, 'stop')}
|
||||
className="ah-server-action-button aspect-square !p-0 !min-w-[48px]"
|
||||
>
|
||||
<Square className="w-4 h-4 fill-current" />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
variant="success"
|
||||
onClick={() => handleServerAction(server.id, 'start')}
|
||||
className="ah-server-action-button aspect-square !p-0 !min-w-[48px]"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const dashboardSidebarItems = useMemo(
|
||||
() => [
|
||||
{ id: 'servers', label: 'My Servers', icon: Server, color: colors.blue },
|
||||
@@ -2119,7 +1915,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<div className="p-6 border-b border-dash-border bg-black/10">
|
||||
<div className="flex items-center space-x-3 group cursor-pointer" onClick={() => navigate('/')}>
|
||||
<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="/assets/transparent_anchorhost.png" alt="" className="h-7 w-auto shrink-0" />
|
||||
<img src={branding.logoPath} alt="" className="h-7 w-auto shrink-0" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-bold tracking-tight text-dash-text leading-tight">Dashboard</h1>
|
||||
@@ -2279,7 +2075,21 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{servers.map((server) => (
|
||||
<ServerCard key={server.id} server={server} />
|
||||
<ServerCard
|
||||
key={server.id}
|
||||
server={server}
|
||||
pricing={pricing}
|
||||
userRole={user?.role}
|
||||
calculateCurrencyValue={calculateCurrencyValue}
|
||||
getCurrencySymbol={getCurrencySymbol}
|
||||
onConvertDemo={handleConvertDemoToPaid}
|
||||
onOpenConsole={(targetServer) => {
|
||||
setSelectedServer(targetServer);
|
||||
setActiveServerDetailTab('overview');
|
||||
fetchServerLogs(targetServer.id);
|
||||
}}
|
||||
onServerAction={handleServerAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -2548,7 +2358,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<div className="rounded-lg border border-dash-border bg-brightBlack/40 px-3 py-2.5 text-sm">
|
||||
{user?.google_id ? (
|
||||
<p className="text-dash-textMuted">
|
||||
No AnchorHost.eu password — you sign in with Google.
|
||||
No {branding.name} password — you sign in with Google.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -2564,7 +2374,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<UnifiedTooltip
|
||||
content={
|
||||
user?.google_id
|
||||
? 'Google sign-in accounts do not use an AnchorHost.eu password. Manage security in Google.'
|
||||
? `Google sign-in accounts do not use a ${branding.name} password. Manage security in Google.`
|
||||
: 'Choose a new password. You will need your current password to confirm.'
|
||||
}
|
||||
multiline
|
||||
@@ -2846,33 +2656,33 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<div className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: colors.black }}>
|
||||
<span style={{ color: colors.foreground }}>Email Support:</span>
|
||||
<a
|
||||
href="mailto:support@anchorhost.eu"
|
||||
href={`mailto:${branding.supportEmailAddress}`}
|
||||
className="font-semibold hover:underline transition-all duration-200 hover:scale-105"
|
||||
style={{ color: colors.magenta }}
|
||||
>
|
||||
support@anchorhost.eu
|
||||
{branding.supportEmailAddress}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: colors.black }}>
|
||||
<span style={{ color: colors.foreground }}>Sales Inquiries:</span>
|
||||
<a
|
||||
href="mailto:sales@anchorhost.eu"
|
||||
href={`mailto:${branding.salesEmail}`}
|
||||
className="font-semibold hover:underline transition-all duration-200 hover:scale-105"
|
||||
style={{ color: colors.magenta }}
|
||||
>
|
||||
sales@anchorhost.eu
|
||||
{branding.salesEmail}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: colors.black }}>
|
||||
<span style={{ color: colors.foreground }}>Business Contact:</span>
|
||||
<a
|
||||
href="mailto:contact@anchorhost.eu"
|
||||
href={`mailto:${branding.supportEmail}`}
|
||||
className="font-semibold hover:underline transition-all duration-200 hover:scale-105"
|
||||
style={{ color: colors.magenta }}
|
||||
>
|
||||
contact@anchorhost.eu
|
||||
{branding.supportEmail}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2902,7 +2712,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="https://docs.anchorhost.eu"
|
||||
href={branding.documentationUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-4 rounded-xl border transition-all duration-200 hover:scale-105 text-center"
|
||||
@@ -3580,7 +3390,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<div className="px-5 sm:px-6 pb-6 border-t border-white/5 animate-in slide-in-from-top-2 duration-200">
|
||||
<p className="text-sm leading-relaxed opacity-60 mb-6 mt-5 max-w-3xl">
|
||||
Your server listens on an <span className="text-white font-semibold">internal port</span> inside its container.
|
||||
AnchorHost.eu maps it to a unique <span className="text-white font-semibold">external port</span> on our public IP so people on the internet can reach it — without exposing your server directly.
|
||||
{branding.name} maps it to a unique <span className="text-white font-semibold">external port</span> on our public IP so people on the internet can reach it — without exposing your server directly.
|
||||
</p>
|
||||
<div className="flex flex-col lg:flex-row items-stretch justify-center gap-3 lg:gap-4">
|
||||
<div className="flex-1 p-5 rounded-2xl border text-center" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
|
||||
@@ -3592,7 +3402,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<ArrowRight className="w-5 h-5 text-primary rotate-90 lg:rotate-0 shrink-0 opacity-60" />
|
||||
</div>
|
||||
<div className="flex-1 p-5 rounded-2xl border text-center" style={{ backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }}>
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-primary mb-3">AnchorHost.eu gateway</p>
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-primary mb-3">{branding.name} gateway</p>
|
||||
<NetworkIcon className="w-6 h-6 mx-auto text-primary mb-1" />
|
||||
<p className="text-[10px] opacity-50 mt-2 leading-relaxed">Secure NodePort + firewall</p>
|
||||
</div>
|
||||
@@ -3653,7 +3463,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
icon: Shield,
|
||||
iconColor: colors.brightGreen,
|
||||
title: 'Firewall opens automatically',
|
||||
body: 'When you expose a port, AnchorHost.eu opens it on the edge firewall in real time — no manual rules needed.',
|
||||
body: `When you expose a port, ${branding.name} opens it on the edge firewall in real time — no manual rules needed.`,
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
@@ -3850,7 +3660,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
<div className="p-6 sm:p-8 rounded-2xl border-2 border-dashed flex flex-col items-center justify-center space-y-5 w-full" style={{ borderColor: `${colors.primary}25`, backgroundColor: `${colors.primary}05` }}>
|
||||
<div className="text-center">
|
||||
<h5 className="text-sm font-black uppercase tracking-[0.2em] mb-2" style={{ color: colors.primary }}>Expose New Port</h5>
|
||||
<p className="text-xs opacity-45 font-medium max-w-md">Enter the port your app listens on inside the server. AnchorHost.eu assigns a public external port automatically.</p>
|
||||
<p className="text-xs opacity-45 font-medium max-w-md">Enter the port your app listens on inside the server. {branding.name} assigns a public external port automatically.</p>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={(e) => { e.preventDefault(); submitNetworkExposePort(); }}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
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';
|
||||
@@ -45,13 +46,13 @@ function DocumentationPage() {
|
||||
title: 'Creating Your Account',
|
||||
content: `# Creating Your Account
|
||||
|
||||
Creating an account on AnchorHost.eu gives you access to our Kubernetes-based cloud hosting platform.
|
||||
Creating an account on ${branding.name} gives you access to our Kubernetes-based cloud hosting platform.
|
||||
|
||||
## Step 1: Registration
|
||||
Navigate to AnchorHost.eu and click the Register button. You can also sign up using Google or Apple for a faster experience.
|
||||
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}.anchorhost.eu\`.
|
||||
- **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.
|
||||
|
||||
@@ -69,7 +70,7 @@ When you click "Create Account":
|
||||
title: 'Authentication and Security',
|
||||
content: `# Authentication and Security
|
||||
|
||||
AnchorHost.eu utilizes a hybrid approach of REST and WebSockets for secure, real-time management.
|
||||
${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.
|
||||
@@ -112,7 +113,7 @@ Think of our main server as a **Large Apartment Building**.
|
||||
**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 AnchorHost.eu, you will almost always use this 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.`
|
||||
@@ -125,7 +126,7 @@ When connecting to anything on AnchorHost.eu, you will almost always use this fo
|
||||
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.anchorhost.eu\`) via the **Domains** tab.
|
||||
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.
|
||||
@@ -258,7 +259,7 @@ Imagine a large apartment building. The building itself is the physical server.
|
||||
- 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 AnchorHost.eu, 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.`
|
||||
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',
|
||||
@@ -290,7 +291,7 @@ Managed containers for application stacks.
|
||||
title: 'How Billing Works',
|
||||
content: `# How Billing Works
|
||||
|
||||
AnchorHost.eu operates on a transparent **pre-paid credit system**. There are no hidden fees or surprise invoices.
|
||||
${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**.
|
||||
@@ -337,7 +338,7 @@ If you wish to host **Minecraft Bedrock** (for Console, Mobile, or Windows 10 Ed
|
||||
title: 'Installing and Managing Minecraft',
|
||||
content: `# Minecraft Server Management
|
||||
|
||||
AnchorHost.eu provides a specialized environment for Minecraft servers using high-performance containers.
|
||||
${branding.name} provides a specialized environment for Minecraft servers using high-performance containers.
|
||||
|
||||
## Installation
|
||||
1. Go to **Create Server**.
|
||||
@@ -479,7 +480,7 @@ We provide an integrated **Plugin Store** powered by the Modrinth API.
|
||||
title: 'Network Architecture',
|
||||
content: `# Network Architecture
|
||||
|
||||
All servers deployed on AnchorHost.eu share a high-performance static IP but utilize unique entry points.
|
||||
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\`
|
||||
@@ -488,7 +489,7 @@ All servers deployed on AnchorHost.eu share a high-performance static IP but uti
|
||||
|
||||
## Subdomain System
|
||||
We provide automatic DNS management via the **deSEC API**.
|
||||
- **Format**: \`{label}.u-{username}.anchorhost.eu\`
|
||||
- **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\`.`
|
||||
},
|
||||
@@ -607,7 +608,7 @@ Managing the state of your Virtual Machine is done directly through the dashboar
|
||||
title: 'Scaling Resources',
|
||||
content: `# Scaling Resources
|
||||
|
||||
AnchorHost.eu allows you to adjust your VPS specifications as your needs grow.
|
||||
${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.
|
||||
@@ -673,7 +674,7 @@ passwd
|
||||
title: 'Getting Started with Databases',
|
||||
content: `# Managed Database Hosting
|
||||
|
||||
AnchorHost.eu provides pre-configured database containers for rapid development.
|
||||
${branding.name} provides pre-configured database containers for rapid development.
|
||||
|
||||
## Supported Engines
|
||||
- **MySQL**: The world's most popular open-source database.
|
||||
@@ -754,13 +755,13 @@ Use these templates to connect your applications. Replace \`IP\`, \`PORT\`, and
|
||||
title: 'Managing Subdomains',
|
||||
content: `# Managing Your Subdomains
|
||||
|
||||
Every user on AnchorHost.eu has their own personal network space based on their username.
|
||||
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.anchorhost.eu\`.
|
||||
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.
|
||||
@@ -775,7 +776,7 @@ Once a domain is created, you can "point" it to one of your active servers.
|
||||
If you see a warning saying **"No NS records found for child domain"** or **"DNSSEC configuration issues"**, don't panic!
|
||||
|
||||
## Why is this happening?
|
||||
AnchorHost.eu 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.
|
||||
${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:
|
||||
@@ -1059,7 +1060,7 @@ Restart Apache: \`sudo systemctl restart apache2\`. Visit your IP in the browser
|
||||
<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="/assets/transparent_anchorhost.png" alt="" className="h-7 w-auto shrink-0" />
|
||||
<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>
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
ArrowDown, ArrowLeft, ArrowRight, Check, ChevronDown, Cpu, Database, ExternalLink,
|
||||
Gamepad2, Globe2, Menu, Monitor, X,
|
||||
} from 'lucide-react';
|
||||
import { AuthContext } from '../App.js';
|
||||
import { AuthContext } from '../App.jsx';
|
||||
import AnnouncementsSection from '../features/announcements/AnnouncementsSection';
|
||||
import { Dropdown } from '../components/dashboardPrimitives';
|
||||
import { API_BASE_URL, SITE_BASE_URL } from '../config/api';
|
||||
import branding from '../branding';
|
||||
|
||||
const serviceOptions = [
|
||||
{ id: 'game', label: 'Game server', description: 'Run a game server and manage it from the control panel.', icon: Gamepad2 },
|
||||
@@ -19,7 +20,7 @@ const serviceDropdownOptions = serviceOptions.map(({ id, label }) => ({ value: i
|
||||
|
||||
const questions = [
|
||||
{
|
||||
question: 'What can I host with AnchorHost.eu?',
|
||||
question: `What can I host with ${branding.name}?`,
|
||||
answer: 'The panel supports game servers, virtual private servers, websites, and databases. The available options depend on your account and current platform capacity.',
|
||||
},
|
||||
{
|
||||
@@ -44,13 +45,13 @@ const heroGallery = [
|
||||
},
|
||||
{
|
||||
src: '/assets/server-2.jpg',
|
||||
alt: 'Rows of server equipment in a data center',
|
||||
title: 'Infrastructure, ready',
|
||||
alt: 'Network hardware and cabling inside a blue server rack',
|
||||
title: 'Servers and network hardware',
|
||||
},
|
||||
{
|
||||
src: '/assets/pricing-bg.jpg',
|
||||
alt: 'Calculator and notes beside a pricing plan',
|
||||
title: 'Clear costs up front',
|
||||
alt: 'Ethernet cables connected to a network switch',
|
||||
title: 'Connections, made visible',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -154,9 +155,9 @@ function HomePage({ setIsLoginOpen, setIsRegisterOpen, pricing }) {
|
||||
<main className="site-page">
|
||||
<header className={`site-header ${scrolled ? 'site-header--scrolled' : ''}`}>
|
||||
<div className="site-nav">
|
||||
<a className="site-brand" href="#top" onClick={(event) => { event.preventDefault(); goTo('top'); }} aria-label="AnchorHost.eu home">
|
||||
<img src="/assets/transparent_anchorhost.png" alt="" />
|
||||
<span>AnchorHost.eu</span>
|
||||
<a className="site-brand" href="#top" onClick={(event) => { event.preventDefault(); goTo('top'); }} aria-label={`${branding.name} home`}>
|
||||
<img src={branding.logoPath} alt="" />
|
||||
<span>{branding.name}</span>
|
||||
</a>
|
||||
|
||||
<nav className={`site-nav-links ${menuOpen ? 'site-nav-links--open' : ''}`} aria-label="Main navigation">
|
||||
@@ -206,10 +207,10 @@ function HomePage({ setIsLoginOpen, setIsRegisterOpen, pricing }) {
|
||||
|
||||
<section className="site-hero" id="top">
|
||||
<div className="site-hero-copy">
|
||||
<p className="site-kicker"><span /> Hosting, with room to work</p>
|
||||
<h1>A control panel for the things you host.</h1>
|
||||
<p className="site-kicker"><span /> {branding.tagline}</p>
|
||||
<h1>{branding.heroTitle}</h1>
|
||||
<p className="site-hero-description">
|
||||
Run game servers, virtual machines, websites, and databases from one place. Keep the tools close and the busywork out of the way.
|
||||
{branding.heroDescription}
|
||||
</p>
|
||||
<div className="site-hero-actions">
|
||||
<button type="button" className="site-button site-button--primary site-button--large" onClick={start}>
|
||||
@@ -225,7 +226,7 @@ function HomePage({ setIsLoginOpen, setIsRegisterOpen, pricing }) {
|
||||
{accountCount !== null && <><i aria-hidden="true" /><span>{accountCount.toLocaleString()} accounts</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<figure className="site-hero-image" aria-label="AnchorHost.eu hosting gallery">
|
||||
<figure className="site-hero-image" aria-label={`${branding.name} hosting gallery`}>
|
||||
<div className="site-gallery-viewport">
|
||||
<img src={currentGalleryImage.src} alt={currentGalleryImage.alt} />
|
||||
<button type="button" className="site-gallery-arrow site-gallery-arrow--previous" onClick={() => showGalleryImage(galleryIndex - 1)} aria-label="Show previous photo">
|
||||
@@ -238,7 +239,7 @@ function HomePage({ setIsLoginOpen, setIsRegisterOpen, pricing }) {
|
||||
<figcaption>
|
||||
<div className="site-gallery-caption" aria-live="polite" aria-atomic="true">
|
||||
<strong>{currentGalleryImage.title}</strong>
|
||||
<span>AnchorHost.eu infrastructure</span>
|
||||
<span>{branding.name} infrastructure</span>
|
||||
</div>
|
||||
<div className="site-gallery-thumbnails" aria-label="Choose a gallery photo">
|
||||
{heroGallery.map((photo, index) => (
|
||||
@@ -401,21 +402,21 @@ function HomePage({ setIsLoginOpen, setIsRegisterOpen, pricing }) {
|
||||
<div className="site-footer-main">
|
||||
<div>
|
||||
<a className="site-brand" href="#top" onClick={(event) => { event.preventDefault(); goTo('top'); }}>
|
||||
<img src="/assets/transparent_anchorhost.png" alt="" /><span>AnchorHost.eu</span>
|
||||
<img src={branding.logoPath} alt="" /><span>{branding.name}</span>
|
||||
</a>
|
||||
<p>Hosting services managed from one place.</p>
|
||||
<p>{branding.footerTagline}</p>
|
||||
</div>
|
||||
<nav className="site-footer-links" aria-label="Footer">
|
||||
<a href={`${SITE_BASE_URL}/discord`} target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
<a href="https://git.anchorhost.eu/anchorhost" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
<a href="mailto:contact@anchorhost.eu">Email support</a>
|
||||
<a href={branding.sourceCodeUrl} target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
<a href={`mailto:${branding.supportEmail}`}>Email support</a>
|
||||
<Link to="/documentation">Documentation</Link>
|
||||
<Link to="/terms">Terms</Link>
|
||||
<Link to="/privacy">Privacy</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="site-footer-bottom">
|
||||
<span>© {new Date().getFullYear()} AnchorHost.eu by <a href="https://kvj.ovh" target="_blank" rel="noopener noreferrer">Wiktor Falkowski</a></span>
|
||||
<span>© {new Date().getFullYear()} {branding.name} by <a href={branding.footerCredit.url} target="_blank" rel="noopener noreferrer">{branding.footerCredit.name}</a></span>
|
||||
<span>Account and service controls live in the dashboard.</span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Shield } from 'lucide-react';
|
||||
import { colors } from '../theme/colors';
|
||||
import { LegalPageShell, LegalSection, LegalList, LegalLink } from '../features/legal/LegalDocumentLayout';
|
||||
import branding from '../branding';
|
||||
|
||||
const LAST_UPDATED = 'April 5, 2026';
|
||||
|
||||
@@ -14,16 +15,16 @@ function PrivacyPolicy() {
|
||||
lastUpdated={LAST_UPDATED}
|
||||
>
|
||||
<LegalSection title="1. General Information">
|
||||
<p>This Privacy Policy explains how AnchorHost.eu processes personal data.</p>
|
||||
<p>This Privacy Policy explains how {branding.name} processes personal data.</p>
|
||||
<p>
|
||||
<strong className="text-dash-text">Data Controller:</strong>
|
||||
<br />
|
||||
Wiktor Falkowski (unregistered business activity under Polish law)
|
||||
{branding.legalProviderName} ({branding.legalProviderStatus})
|
||||
</p>
|
||||
<p>
|
||||
<strong className="text-dash-text">Contact:</strong>
|
||||
<br />
|
||||
<LegalLink href="mailto:privacy@anchorhost.eu">privacy@anchorhost.eu</LegalLink>
|
||||
<LegalLink href={`mailto:${branding.privacyEmail}`}>{branding.privacyEmail}</LegalLink>
|
||||
</p>
|
||||
<p>We process data in accordance with GDPR.</p>
|
||||
</LegalSection>
|
||||
@@ -99,7 +100,7 @@ function PrivacyPolicy() {
|
||||
]}
|
||||
/>
|
||||
<p>
|
||||
Requests: <LegalLink href="mailto:privacy@anchorhost.eu">privacy@anchorhost.eu</LegalLink>
|
||||
Requests: <LegalLink href={`mailto:${branding.privacyEmail}`}>{branding.privacyEmail}</LegalLink>
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
@@ -124,7 +125,7 @@ function PrivacyPolicy() {
|
||||
|
||||
<LegalSection title="12. Contact">
|
||||
<p>
|
||||
<LegalLink href="mailto:privacy@anchorhost.eu">privacy@anchorhost.eu</LegalLink>
|
||||
<LegalLink href={`mailto:${branding.privacyEmail}`}>{branding.privacyEmail}</LegalLink>
|
||||
</p>
|
||||
</LegalSection>
|
||||
</LegalPageShell>
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { colors } from '../theme/colors';
|
||||
import { LegalPageShell, LegalSection, LegalList, LegalLink } from '../features/legal/LegalDocumentLayout';
|
||||
import branding from '../branding';
|
||||
|
||||
const LAST_UPDATED = 'April 5, 2026';
|
||||
|
||||
@@ -15,18 +16,18 @@ function TermsOfService() {
|
||||
>
|
||||
<LegalSection title="1. General Information">
|
||||
<p>
|
||||
These Terms of Service ("Terms") govern the use of services provided under the brand AnchorHost.eu via the website{' '}
|
||||
<LegalLink href="https://www.anchorhost.eu">https://www.anchorhost.eu</LegalLink>.
|
||||
These Terms of Service ("Terms") govern the use of services provided under the brand {branding.name} via the website{' '}
|
||||
<LegalLink href={branding.siteUrl}>{branding.siteUrl}</LegalLink>.
|
||||
</p>
|
||||
<p>
|
||||
<strong className="text-dash-text">Service Provider:</strong>
|
||||
<br />
|
||||
Wiktor Falkowski (unregistered business activity under Polish law)
|
||||
{branding.legalProviderName} ({branding.legalProviderStatus})
|
||||
</p>
|
||||
<p>
|
||||
<strong className="text-dash-text">Contact:</strong>
|
||||
<br />
|
||||
<LegalLink href="mailto:legal@anchorhost.eu">legal@anchorhost.eu</LegalLink>
|
||||
<LegalLink href={`mailto:${branding.legalEmail}`}>{branding.legalEmail}</LegalLink>
|
||||
</p>
|
||||
<p>By using the Service, you agree to these Terms.</p>
|
||||
</LegalSection>
|
||||
@@ -191,7 +192,7 @@ function TermsOfService() {
|
||||
|
||||
<LegalSection title="17. Contact">
|
||||
<p>
|
||||
<LegalLink href="mailto:legal@anchorhost.eu">legal@anchorhost.eu</LegalLink>
|
||||
<LegalLink href={`mailto:${branding.legalEmail}`}>{branding.legalEmail}</LegalLink>
|
||||
</p>
|
||||
</LegalSection>
|
||||
</LegalPageShell>
|
||||
File renamed without changes.
@@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
/** AnchorHost.eu palette: colors.ini is the canonical source; keep Tailwind aligned. */
|
||||
/** Product palette: colors.ini is the canonical source; keep Tailwind aligned. */
|
||||
export const colors = {
|
||||
foreground: '#D0D0D0',
|
||||
background: '#151515',
|
||||
@@ -8,24 +8,24 @@ export const colors = {
|
||||
green: '#7B9246',
|
||||
yellow: '#D3A04D',
|
||||
orange: '#D3A04D',
|
||||
blue: '#547C99',
|
||||
magenta: '#547C99',
|
||||
blue: '#6C99BB',
|
||||
magenta: '#6C99BB',
|
||||
cyan: '#5EA8A2',
|
||||
white: '#757575',
|
||||
white: '#898989',
|
||||
brightBlack: '#2F2F2F',
|
||||
brightRed: '#A53C23',
|
||||
brightGreen: '#7B9246',
|
||||
brightYellow: '#D3A04D',
|
||||
brightBlue: '#6C99BB',
|
||||
brightMagenta: '#547C99',
|
||||
brightMagenta: '#6C99BB',
|
||||
brightCyan: '#5EA8A2',
|
||||
brightWhite: '#F5F5F5',
|
||||
primary: '#6C99BB',
|
||||
darkBlue: '#547C99',
|
||||
darkBlue: '#6C99BB',
|
||||
darkCyan: '#5EA8A2',
|
||||
darkWhite: '#505050',
|
||||
darkRed: '#A53C23',
|
||||
darkGreen: '#7B9246',
|
||||
darkYellow: '#D3A04D',
|
||||
darkMagenta: '#547C99',
|
||||
darkMagenta: '#6C99BB',
|
||||
};
|
||||
Reference in new issue
Block a user