fixed bad links
152
Panel/modules/website/README.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Gameservers.World Website Module
|
||||
|
||||
This module is the public Gameservers.World sales and documentation website.
|
||||
|
||||
## Purpose
|
||||
|
||||
- Keep the public site inside the GSP repository at `Panel/modules/website/`
|
||||
- Remove location-dependent include logic from the old standalone `Website/` tree
|
||||
- Keep marketing pages working even when billing configuration is missing
|
||||
- Centralize URL and filesystem path handling so the site is portable
|
||||
|
||||
## Structure
|
||||
|
||||
```text
|
||||
Panel/modules/website/
|
||||
index.php
|
||||
serverlist.php
|
||||
docs.php
|
||||
login.php
|
||||
pricing.php
|
||||
locations.php
|
||||
support.php
|
||||
doc_asset.php
|
||||
includes/
|
||||
bootstrap.php
|
||||
footer.php
|
||||
header.php
|
||||
navigation.php
|
||||
paths.php
|
||||
assets/
|
||||
css/
|
||||
js/
|
||||
images/
|
||||
pages/
|
||||
home.php
|
||||
game_servers.php
|
||||
documentation.php
|
||||
pricing.php
|
||||
locations.php
|
||||
support.php
|
||||
config/
|
||||
config.example.php
|
||||
```
|
||||
|
||||
## URL helpers
|
||||
|
||||
The website uses a central bootstrap instead of scattered relative paths.
|
||||
|
||||
- `website_url('serverlist.php')`
|
||||
- `website_asset('css/site.css')`
|
||||
- `panel_url()`
|
||||
- `login_url()`
|
||||
- `billing_url('order.php?service_id=1')`
|
||||
- `documentation_url('minecraft')`
|
||||
|
||||
## Billing and database behavior
|
||||
|
||||
The public site does not include `Panel/modules/billing/includes/config.inc.php` directly.
|
||||
|
||||
Instead it:
|
||||
|
||||
1. Parses `Panel/includes/config.inc.php` when present
|
||||
2. Parses `Panel/modules/billing/includes/config.inc.php` when present
|
||||
3. Uses the discovered database values only for pages that need catalog data
|
||||
4. Falls back cleanly when no usable billing or panel DB config is available
|
||||
|
||||
Effects:
|
||||
|
||||
- `index.php`, `docs.php`, `locations.php`, `pricing.php`, and `support.php` still load
|
||||
- `serverlist.php` shows a clean fallback message instead of a fatal include error
|
||||
- shared navigation never crashes because billing config is missing
|
||||
|
||||
## Documentation source
|
||||
|
||||
Customer documentation is read from the existing billing docs directory:
|
||||
|
||||
- `Panel/modules/billing/docs/`
|
||||
|
||||
This keeps the website portable without duplicating the documentation tree.
|
||||
|
||||
## Deployment
|
||||
|
||||
Preferred Apache approach:
|
||||
|
||||
1. Point the Gameservers.World vhost `DocumentRoot` to `Panel/modules/website`
|
||||
2. Expose billing separately under `/billing/` with an Apache `Alias`
|
||||
3. Set `panel_url`, `login_url`, and `billing_base_url` in `config/config.php` or `config/config.local.php`
|
||||
|
||||
Example:
|
||||
|
||||
```apacheconf
|
||||
<VirtualHost *:80>
|
||||
ServerName gameservers.world
|
||||
ServerAlias www.gameservers.world
|
||||
DocumentRoot /var/www/html/GSP/Panel/modules/website
|
||||
|
||||
<Directory /var/www/html/GSP/Panel/modules/website>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.php
|
||||
</Directory>
|
||||
|
||||
Alias /billing /var/www/html/GSP/Panel/modules/billing
|
||||
<Directory /var/www/html/GSP/Panel/modules/billing>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.php
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Alternate front-controller approach:
|
||||
|
||||
- Keep a tiny public `index.php` outside the repo tree that requires `Panel/modules/website/index.php`
|
||||
- Do not duplicate the full website into a second location
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Copy `config/config.example.php` to `config/config.php` or `config/config.local.php`
|
||||
2. Set:
|
||||
- `public_base_url`
|
||||
- `billing_base_url`
|
||||
- `panel_url`
|
||||
- `login_url`
|
||||
- support links
|
||||
3. Keep any environment-specific config out of Git
|
||||
|
||||
## Manual link checks
|
||||
|
||||
Verify:
|
||||
|
||||
1. `index.php`
|
||||
2. `serverlist.php`
|
||||
3. `docs.php`
|
||||
4. `pricing.php`
|
||||
5. `locations.php`
|
||||
6. `support.php`
|
||||
7. `login.php`
|
||||
8. header navigation
|
||||
9. footer links
|
||||
10. mobile navigation
|
||||
11. asset URLs
|
||||
12. `billing_url()` destinations
|
||||
|
||||
## Portability notes
|
||||
|
||||
- No `/var/www/html/...` filesystem assumptions
|
||||
- No `gameservers.world/panel` assumptions
|
||||
- No repeated `../../../` path climbing
|
||||
- Internal page links and assets route through helpers
|
||||
- Database config is optional for non-catalog pages
|
||||
|
||||
743
Panel/modules/website/assets/css/site.css
Normal file
|
|
@ -0,0 +1,743 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #06111f;
|
||||
--bg-alt: #0b1729;
|
||||
--panel: rgba(9, 19, 35, 0.9);
|
||||
--panel-strong: #0f1d33;
|
||||
--line: rgba(143, 174, 214, 0.18);
|
||||
--line-strong: rgba(143, 174, 214, 0.28);
|
||||
--text: #e8f0ff;
|
||||
--muted: #93a8cb;
|
||||
--accent: #54a6ff;
|
||||
--accent-strong: #7bc0ff;
|
||||
--accent-soft: rgba(84, 166, 255, 0.12);
|
||||
--success: #89d77b;
|
||||
--danger: #ff8c7a;
|
||||
--shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
|
||||
--radius: 8px;
|
||||
--content-width: 1180px;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(4, 10, 18, 0.72) 0%, rgba(4, 10, 18, 0.92) 55%, rgba(4, 10, 18, 1) 100%),
|
||||
url("../images/dark.jpg") center/cover fixed no-repeat,
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(calc(100% - 32px), var(--content-width));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.site-main {
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
backdrop-filter: blur(16px);
|
||||
background: rgba(5, 12, 23, 0.88);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.header-shell {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.brand,
|
||||
.footer-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.brand-logo,
|
||||
.footer-logo {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.primary-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.is-active {
|
||||
color: var(--text);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 0 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
font-weight: 600;
|
||||
transition: 0.18s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: linear-gradient(180deg, #58abff 0%, #3589e8 100%);
|
||||
color: #051120;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.button-ghost {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-toggle span {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
margin: 4px auto;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 72px 0 40px;
|
||||
}
|
||||
|
||||
.hero-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(84, 166, 255, 0.12);
|
||||
border: 1px solid rgba(84, 166, 255, 0.22);
|
||||
color: #b6d8ff;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0 0 16px;
|
||||
font-size: clamp(2.5rem, 4vw, 4.4rem);
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 0;
|
||||
max-width: 62ch;
|
||||
color: #c3d2ea;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.hero-actions,
|
||||
.stack-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.hero-points {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.hero-point,
|
||||
.info-card,
|
||||
.stat-card,
|
||||
.feature-card,
|
||||
.location-card,
|
||||
.service-card,
|
||||
.support-card,
|
||||
.doc-card,
|
||||
.empty-state,
|
||||
.panel-preview,
|
||||
.flow-step,
|
||||
.summary-card {
|
||||
background: linear-gradient(180deg, rgba(13, 24, 43, 0.92) 0%, rgba(10, 18, 32, 0.92) 100%);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero-point {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.hero-point strong,
|
||||
.section-heading h2,
|
||||
.service-card h3,
|
||||
.feature-card h3,
|
||||
.location-card h3,
|
||||
.doc-card h3,
|
||||
.support-card h3,
|
||||
.flow-step h3,
|
||||
.summary-card h3,
|
||||
.panel-preview h3 {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-visual img {
|
||||
width: 100%;
|
||||
min-height: 420px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero-visual-card {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
padding: 18px;
|
||||
background: rgba(5, 12, 23, 0.82);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.hero-visual-card strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.hero-visual-card p {
|
||||
color: var(--muted);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.section-heading h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.6rem, 2vw, 2.2rem);
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
max-width: 72ch;
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.feature-grid,
|
||||
.location-grid,
|
||||
.service-grid,
|
||||
.support-grid,
|
||||
.doc-grid,
|
||||
.flow-grid,
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.location-grid,
|
||||
.service-grid,
|
||||
.support-grid,
|
||||
.doc-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flow-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.stat-card,
|
||||
.feature-card,
|
||||
.location-card,
|
||||
.service-card,
|
||||
.support-card,
|
||||
.doc-card,
|
||||
.flow-step,
|
||||
.summary-card,
|
||||
.panel-preview,
|
||||
.empty-state {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
|
||||
.stat-card span,
|
||||
.feature-card p,
|
||||
.location-card p,
|
||||
.service-card p,
|
||||
.doc-card p,
|
||||
.support-card p,
|
||||
.panel-preview p,
|
||||
.flow-step p,
|
||||
.summary-card p,
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
margin: 16px 0 8px;
|
||||
background: linear-gradient(90deg, rgba(84, 166, 255, 0) 0%, rgba(84, 166, 255, 0.35) 50%, rgba(84, 166, 255, 0) 100%);
|
||||
}
|
||||
|
||||
.location-card small,
|
||||
.service-meta,
|
||||
.doc-meta {
|
||||
color: #b8cae5;
|
||||
}
|
||||
|
||||
.service-card img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--line);
|
||||
margin-bottom: 16px;
|
||||
background: #08111f;
|
||||
}
|
||||
|
||||
.service-card header,
|
||||
.doc-card header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.service-price {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(137, 215, 123, 0.12);
|
||||
border: 1px solid rgba(137, 215, 123, 0.22);
|
||||
color: #b6efad;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 16px 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 140, 122, 0.22);
|
||||
background: rgba(255, 140, 122, 0.08);
|
||||
color: #ffd1c8;
|
||||
}
|
||||
|
||||
.alert.info {
|
||||
border-color: rgba(84, 166, 255, 0.2);
|
||||
background: rgba(84, 166, 255, 0.08);
|
||||
color: #cbe1ff;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
padding: 54px 0 18px;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: clamp(2rem, 3vw, 3rem);
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin: 0;
|
||||
max-width: 74ch;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel-preview {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr);
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-preview-shell {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-preview-shell img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.panel-preview-stats {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.flow-step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
background: rgba(84, 166, 255, 0.16);
|
||||
color: var(--accent-strong);
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.doc-list-group {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.doc-list-group h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.doc-view {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.doc-view h1,
|
||||
.doc-view h2,
|
||||
.doc-view h3,
|
||||
.doc-view h4 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.doc-view a {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.doc-view pre,
|
||||
.doc-view code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
.doc-view pre {
|
||||
overflow-x: auto;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.doc-view table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.doc-view th,
|
||||
.doc-view td {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.cta-band {
|
||||
padding: 28px 0 0;
|
||||
}
|
||||
|
||||
.cta-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 24px 26px;
|
||||
background: linear-gradient(135deg, rgba(29, 72, 138, 0.55) 0%, rgba(12, 25, 45, 0.95) 100%);
|
||||
border: 1px solid rgba(113, 176, 255, 0.28);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
border-top: 1px solid var(--line);
|
||||
background: rgba(4, 10, 18, 0.78);
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.footer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-grid h2 {
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.footer-copy {
|
||||
color: var(--muted);
|
||||
max-width: 32ch;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.hero-layout,
|
||||
.panel-preview,
|
||||
.footer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.feature-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.location-grid,
|
||||
.service-grid,
|
||||
.support-grid,
|
||||
.doc-grid,
|
||||
.flow-grid,
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.header-shell {
|
||||
grid-template-columns: auto auto;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: inline-block;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.primary-nav,
|
||||
.header-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.primary-nav.is-open,
|
||||
.header-actions.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-shell {
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.site-header .container {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.primary-nav.is-open {
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.header-actions.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-actions .button {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.page-heading {
|
||||
padding-top: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
width: min(calc(100% - 24px), var(--content-width));
|
||||
}
|
||||
|
||||
.hero-points,
|
||||
.stats-grid,
|
||||
.feature-grid,
|
||||
.location-grid,
|
||||
.service-grid,
|
||||
.support-grid,
|
||||
.doc-grid,
|
||||
.flow-grid,
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.cta-panel,
|
||||
.site-footer {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
BIN
Panel/modules/website/assets/images/banner.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
Panel/modules/website/assets/images/dark.jpg
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
Panel/modules/website/assets/images/games/7dtd.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
Panel/modules/website/assets/images/games/arkse.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
Panel/modules/website/assets/images/games/arma2.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 52 KiB |
BIN
Panel/modules/website/assets/images/games/arma_3.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
Panel/modules/website/assets/images/games/asseto.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
Panel/modules/website/assets/images/games/avorion.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
Panel/modules/website/assets/images/games/brainbread_2.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
Panel/modules/website/assets/images/games/chivalry.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
Panel/modules/website/assets/images/games/citadel.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
Panel/modules/website/assets/images/games/colonysurvival.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
Panel/modules/website/assets/images/games/conanexiles.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Panel/modules/website/assets/images/games/cs_go.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Panel/modules/website/assets/images/games/cstrike.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Panel/modules/website/assets/images/games/cstrikesource.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
Panel/modules/website/assets/images/games/day_z.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
Panel/modules/website/assets/images/games/dayz_epochmod.jpg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
Panel/modules/website/assets/images/games/dayz_mod.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Panel/modules/website/assets/images/games/deathmatch_classic.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
Panel/modules/website/assets/images/games/dst.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
Panel/modules/website/assets/images/games/eco.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
Panel/modules/website/assets/images/games/eurotruck2.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
Panel/modules/website/assets/images/games/fistful_of_frags.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
Panel/modules/website/assets/images/games/garrys_mod.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
Panel/modules/website/assets/images/games/harsh.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
Panel/modules/website/assets/images/games/hurt_world.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
Panel/modules/website/assets/images/games/insurgency.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
Panel/modules/website/assets/images/games/killing_floor.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Panel/modules/website/assets/images/games/killing_floor_2.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
Panel/modules/website/assets/images/games/left_4_dead.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
Panel/modules/website/assets/images/games/left_4_dead_2.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
Panel/modules/website/assets/images/games/minecraft.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
Panel/modules/website/assets/images/games/miscreated_server.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
Panel/modules/website/assets/images/games/mordhau.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
Panel/modules/website/assets/images/games/nomoreroominhell.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
Panel/modules/website/assets/images/games/ootow.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
Panel/modules/website/assets/images/games/rust_header.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
Panel/modules/website/assets/images/games/scp.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
Panel/modules/website/assets/images/games/squad.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
Panel/modules/website/assets/images/games/starbound.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
Panel/modules/website/assets/images/games/stationeers.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
Panel/modules/website/assets/images/games/team_fortress_2.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
Panel/modules/website/assets/images/games/terraria.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
Panel/modules/website/assets/images/games/urt.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
Panel/modules/website/assets/images/games/valheim.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
Panel/modules/website/assets/images/games/wurmu.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
Panel/modules/website/assets/images/logo-sm.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Panel/modules/website/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
18
Panel/modules/website/assets/js/site.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggle = document.querySelector('[data-nav-toggle]');
|
||||
const menu = document.querySelector('[data-nav-menu]');
|
||||
const actions = document.querySelector('[data-header-actions]');
|
||||
|
||||
if (!toggle || !menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||||
toggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
|
||||
menu.classList.toggle('is-open', !expanded);
|
||||
if (actions) {
|
||||
actions.classList.toggle('is-open', !expanded);
|
||||
}
|
||||
});
|
||||
});
|
||||
3
Panel/modules/website/config/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
config.php
|
||||
config.local.php
|
||||
|
||||
39
Panel/modules/website/config/config.example.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'site_name' => 'Gameservers.World',
|
||||
'site_tagline' => 'Virtual private game servers with dedicated resources and full configuration access.',
|
||||
'meta_description' => 'Virtual private game servers with dedicated resources, predictable performance, full configuration access, mod support, and real human support.',
|
||||
|
||||
// Leave null to derive the base path from the current request.
|
||||
// Example: '/sales'
|
||||
'base_path' => null,
|
||||
|
||||
// Optional absolute public base URL without trailing slash.
|
||||
// Example: 'https://gameservers.world'
|
||||
'public_base_url' => 'https://gameservers.world',
|
||||
|
||||
// Public billing/catalog/order surface. Recommended Apache alias: /billing
|
||||
'billing_base_url' => 'https://gameservers.world/billing',
|
||||
|
||||
// Active panel URL. Do not point the public site at /panel/ unless that route is real.
|
||||
'panel_url' => 'https://panel.iaregamer.com/',
|
||||
'login_url' => 'https://panel.iaregamer.com/',
|
||||
|
||||
// Optional support links.
|
||||
'discord_url' => 'https://discord.gg/replace-me',
|
||||
'support_url' => 'https://gameservers.world/support',
|
||||
'support_email' => 'support@gameservers.world',
|
||||
|
||||
// Public fallback copy for catalog outages or missing billing config.
|
||||
'admin_notice' => 'Server catalog is currently unavailable. Please contact support.',
|
||||
|
||||
'locations' => [
|
||||
['name' => 'Los Angeles, USA', 'region' => 'West Coast coverage', 'host' => 'la-game-1.iaregamer.com'],
|
||||
['name' => 'Kansas City, USA', 'region' => 'Central US coverage', 'host' => 'kc-game-2.iaregamer.com'],
|
||||
['name' => 'Dallas, USA', 'region' => 'Southern US coverage', 'host' => 'dal-game-1.iaregamer.com'],
|
||||
['name' => 'New York City, USA', 'region' => 'East Coast coverage', 'host' => 'nyc-game-1.iaregamer.com'],
|
||||
['name' => 'Dublin, Ireland', 'region' => 'EU coverage', 'host' => 'dub-game-1.iaregamer.com'],
|
||||
],
|
||||
];
|
||||
|
||||
31
Panel/modules/website/doc_asset.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$doc = trim((string)($_GET['doc'] ?? ''));
|
||||
$file = trim((string)($_GET['file'] ?? ''));
|
||||
|
||||
if (!website_is_valid_doc_slug($doc) || !preg_match('/^[a-z0-9._-]+\.(png|jpe?g|webp)$/i', $file)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$assetPath = website_doc_path($doc, $file);
|
||||
if ($assetPath === null || !is_readable($assetPath)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mimeType = match (strtolower(pathinfo($assetPath, PATHINFO_EXTENSION))) {
|
||||
'png' => 'image/png',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . (string)filesize($assetPath));
|
||||
readfile($assetPath);
|
||||
|
||||
42
Panel/modules/website/docs.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$docSlug = trim((string)($_GET['doc'] ?? ''));
|
||||
$docIndex = website_fetch_doc_index();
|
||||
$docContent = null;
|
||||
$docEntry = null;
|
||||
|
||||
if ($docSlug !== '' && website_is_valid_doc_slug($docSlug)) {
|
||||
foreach ($docIndex as $entry) {
|
||||
if ($entry['slug'] === $docSlug) {
|
||||
$docEntry = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($docEntry !== null) {
|
||||
$docPath = website_doc_path($docSlug, 'index.php');
|
||||
if ($docPath !== null) {
|
||||
ob_start();
|
||||
include $docPath;
|
||||
$docContent = (string)ob_get_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
website_render(
|
||||
'documentation.php',
|
||||
[
|
||||
'activePage' => 'docs',
|
||||
'pageTitle' => $docEntry !== null ? ($docEntry['name'] . ' - Documentation - Gameservers.World') : 'Documentation - Gameservers.World',
|
||||
'metaDescription' => $docEntry !== null ? $docEntry['description'] : 'Browse server setup, panel usage, and troubleshooting guides for Gameservers.World customers.',
|
||||
'canonicalPath' => $docEntry !== null ? ('docs.php?doc=' . rawurlencode($docEntry['slug'])) : 'docs.php',
|
||||
'docIndex' => $docIndex,
|
||||
'docEntry' => $docEntry,
|
||||
'docContent' => $docContent,
|
||||
]
|
||||
);
|
||||
|
||||
506
Panel/modules/website/includes/bootstrap.php
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/paths.php';
|
||||
|
||||
if (defined('GSP_WEBSITE_BOOTSTRAPPED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
define('GSP_WEBSITE_BOOTSTRAPPED', true);
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
$websiteConfig = [];
|
||||
$websiteConfigFiles = [
|
||||
WEBSITE_CONFIG_DIR . '/config.php',
|
||||
WEBSITE_CONFIG_DIR . '/config.local.php',
|
||||
];
|
||||
|
||||
foreach ($websiteConfigFiles as $configFile) {
|
||||
if (!is_readable($configFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$loaded = require $configFile;
|
||||
if (is_array($loaded)) {
|
||||
$websiteConfig = array_replace_recursive($websiteConfig, $loaded);
|
||||
}
|
||||
}
|
||||
|
||||
$websiteDefaults = [
|
||||
'site_name' => 'Gameservers.World',
|
||||
'site_tagline' => 'Virtual private game servers with dedicated resources and full configuration access.',
|
||||
'meta_description' => 'Virtual private game servers with dedicated resources, predictable performance, full configuration access, mod support, and real human support.',
|
||||
'base_path' => null,
|
||||
'public_base_url' => null,
|
||||
'billing_base_url' => '/billing',
|
||||
'panel_url' => 'https://panel.iaregamer.com/',
|
||||
'login_url' => 'https://panel.iaregamer.com/',
|
||||
'discord_url' => null,
|
||||
'support_url' => null,
|
||||
'support_email' => null,
|
||||
'admin_notice' => 'Server catalog is currently unavailable. Please contact support.',
|
||||
'locations' => [
|
||||
['name' => 'Los Angeles, USA', 'region' => 'West Coast coverage', 'host' => 'la-game-1.iaregamer.com'],
|
||||
['name' => 'Kansas City, USA', 'region' => 'Central US coverage', 'host' => 'kc-game-2.iaregamer.com'],
|
||||
['name' => 'Dallas, USA', 'region' => 'Southern US coverage', 'host' => 'dal-game-1.iaregamer.com'],
|
||||
['name' => 'New York City, USA', 'region' => 'East Coast coverage', 'host' => 'nyc-game-1.iaregamer.com'],
|
||||
['name' => 'Dublin, Ireland', 'region' => 'EU coverage', 'host' => 'dub-game-1.iaregamer.com'],
|
||||
],
|
||||
];
|
||||
|
||||
$websiteConfig = array_replace_recursive($websiteDefaults, $websiteConfig);
|
||||
|
||||
function website_config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
global $websiteConfig;
|
||||
|
||||
if ($key === null) {
|
||||
return $websiteConfig;
|
||||
}
|
||||
|
||||
return $websiteConfig[$key] ?? $default;
|
||||
}
|
||||
|
||||
function website_log(string $message): void
|
||||
{
|
||||
error_log('[website] ' . $message);
|
||||
}
|
||||
|
||||
function website_escape(mixed $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function website_normalize_base_path(?string $path): string
|
||||
{
|
||||
$path = trim((string)$path);
|
||||
if ($path === '' || $path === '/') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
function website_request_scheme(): string
|
||||
{
|
||||
$https = $_SERVER['HTTPS'] ?? '';
|
||||
if ($https !== '' && strtolower((string)$https) !== 'off') {
|
||||
return 'https';
|
||||
}
|
||||
|
||||
$forwarded = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '';
|
||||
if ($forwarded !== '') {
|
||||
return strtolower((string)$forwarded) === 'https' ? 'https' : 'http';
|
||||
}
|
||||
|
||||
return 'http';
|
||||
}
|
||||
|
||||
function website_base_path(): string
|
||||
{
|
||||
static $basePath = null;
|
||||
if ($basePath !== null) {
|
||||
return $basePath;
|
||||
}
|
||||
|
||||
$configured = website_config('base_path');
|
||||
if (is_string($configured) && $configured !== '') {
|
||||
$basePath = website_normalize_base_path($configured);
|
||||
return $basePath;
|
||||
}
|
||||
|
||||
$scriptName = (string)($_SERVER['SCRIPT_NAME'] ?? '');
|
||||
if ($scriptName === '') {
|
||||
$basePath = '';
|
||||
return $basePath;
|
||||
}
|
||||
|
||||
$dir = str_replace('\\', '/', dirname($scriptName));
|
||||
$basePath = ($dir === '/' || $dir === '.' || $dir === '') ? '' : website_normalize_base_path($dir);
|
||||
return $basePath;
|
||||
}
|
||||
|
||||
function website_public_base_url(): string
|
||||
{
|
||||
static $baseUrl = null;
|
||||
if ($baseUrl !== null) {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
$configured = trim((string)website_config('public_base_url', ''));
|
||||
if ($configured !== '') {
|
||||
$baseUrl = rtrim($configured, '/');
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
$host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
|
||||
if ($host === '') {
|
||||
$baseUrl = '';
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
$baseUrl = website_request_scheme() . '://' . $host . website_base_path();
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
function website_url(string $path = ''): string
|
||||
{
|
||||
$basePath = rtrim(website_base_path(), '/');
|
||||
$path = ltrim($path, '/');
|
||||
if ($path === '') {
|
||||
return $basePath === '' ? '/' : $basePath . '/';
|
||||
}
|
||||
|
||||
return ($basePath === '' ? '' : $basePath) . '/' . $path;
|
||||
}
|
||||
|
||||
function website_asset(string $path): string
|
||||
{
|
||||
return website_url('assets/' . ltrim($path, '/'));
|
||||
}
|
||||
|
||||
function website_join_external_url(string $base, string $path = ''): string
|
||||
{
|
||||
$base = trim($base);
|
||||
if ($base === '') {
|
||||
return website_url($path);
|
||||
}
|
||||
|
||||
$base = rtrim($base, '/');
|
||||
$path = ltrim($path, '/');
|
||||
if ($path === '') {
|
||||
return $base . '/';
|
||||
}
|
||||
|
||||
return $base . '/' . $path;
|
||||
}
|
||||
|
||||
function panel_url(string $path = ''): string
|
||||
{
|
||||
return website_join_external_url((string)website_config('panel_url', ''), $path);
|
||||
}
|
||||
|
||||
function login_url(string $path = ''): string
|
||||
{
|
||||
return website_join_external_url((string)website_config('login_url', website_config('panel_url', '')), $path);
|
||||
}
|
||||
|
||||
function billing_url(string $path = ''): string
|
||||
{
|
||||
return website_join_external_url((string)website_config('billing_base_url', ''), $path);
|
||||
}
|
||||
|
||||
function documentation_url(?string $docSlug = null): string
|
||||
{
|
||||
if ($docSlug === null || $docSlug === '') {
|
||||
return website_url('docs.php');
|
||||
}
|
||||
|
||||
return website_url('docs.php?doc=' . rawurlencode($docSlug));
|
||||
}
|
||||
|
||||
function website_canonical_url(string $path = ''): string
|
||||
{
|
||||
$base = website_public_base_url();
|
||||
if ($base === '') {
|
||||
return website_url($path);
|
||||
}
|
||||
|
||||
$path = ltrim($path, '/');
|
||||
if ($path === '') {
|
||||
return $base . '/';
|
||||
}
|
||||
|
||||
return rtrim($base, '/') . '/' . $path;
|
||||
}
|
||||
|
||||
function website_read_php_assignments(string $filePath, array $variableNames): array
|
||||
{
|
||||
if (!is_readable($filePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = @file_get_contents($filePath);
|
||||
if ($content === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($variableNames as $variableName) {
|
||||
$patternDouble = '/^\s*\$' . preg_quote($variableName, '/') . '\s*=\s*"([^"]*)"/m';
|
||||
$patternSingle = '/^\s*\$' . preg_quote($variableName, '/') . "\s*=\s*'([^']*)'/m";
|
||||
if (preg_match($patternDouble, $content, $match) === 1 || preg_match($patternSingle, $content, $match) === 1) {
|
||||
$result[$variableName] = $match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function website_database_settings(): ?array
|
||||
{
|
||||
static $settings = null;
|
||||
static $resolved = false;
|
||||
|
||||
if ($resolved) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$resolved = true;
|
||||
$keys = ['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'];
|
||||
$merged = [];
|
||||
|
||||
$panelConfig = WEBSITE_PANEL_INCLUDE_DIR . '/config.inc.php';
|
||||
if (is_readable($panelConfig)) {
|
||||
$merged = array_replace($merged, website_read_php_assignments($panelConfig, $keys));
|
||||
}
|
||||
|
||||
$billingConfig = WEBSITE_BILLING_ROOT . '/includes/config.inc.php';
|
||||
if (is_readable($billingConfig)) {
|
||||
$merged = array_replace($merged, website_read_php_assignments($billingConfig, $keys));
|
||||
}
|
||||
|
||||
foreach (['db_host', 'db_user', 'db_name', 'table_prefix'] as $requiredKey) {
|
||||
if (empty($merged[$requiredKey])) {
|
||||
$settings = null;
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
|
||||
$settings = $merged;
|
||||
return $settings;
|
||||
}
|
||||
|
||||
function website_billing_config_present(): bool
|
||||
{
|
||||
return is_readable(WEBSITE_BILLING_ROOT . '/includes/config.inc.php');
|
||||
}
|
||||
|
||||
function website_db(): ?mysqli
|
||||
{
|
||||
static $connection = false;
|
||||
if ($connection instanceof mysqli) {
|
||||
return $connection;
|
||||
}
|
||||
if ($connection === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$settings = website_database_settings();
|
||||
if ($settings === null) {
|
||||
$connection = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = isset($settings['db_port']) && $settings['db_port'] !== '' ? (int)$settings['db_port'] : null;
|
||||
$mysqli = @mysqli_connect(
|
||||
(string)$settings['db_host'],
|
||||
(string)($settings['db_user'] ?? ''),
|
||||
(string)($settings['db_pass'] ?? ''),
|
||||
(string)$settings['db_name'],
|
||||
$port
|
||||
);
|
||||
|
||||
if (!$mysqli instanceof mysqli) {
|
||||
website_log('Database connection failed for public website.');
|
||||
$connection = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@mysqli_set_charset($mysqli, 'utf8mb4');
|
||||
$connection = $mysqli;
|
||||
return $connection;
|
||||
}
|
||||
|
||||
function website_table_prefix(): string
|
||||
{
|
||||
$settings = website_database_settings();
|
||||
return (string)($settings['table_prefix'] ?? '');
|
||||
}
|
||||
|
||||
function website_billing_available(): bool
|
||||
{
|
||||
return website_db() instanceof mysqli;
|
||||
}
|
||||
|
||||
function website_billing_docs_root(): ?string
|
||||
{
|
||||
if (is_dir(WEBSITE_BILLING_DOCS_DIR)) {
|
||||
return WEBSITE_BILLING_DOCS_DIR;
|
||||
}
|
||||
|
||||
$legacyDocs = WEBSITE_LEGACY_SITE_ROOT . '/docs';
|
||||
if (is_dir($legacyDocs)) {
|
||||
return $legacyDocs;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function website_is_valid_doc_slug(string $slug): bool
|
||||
{
|
||||
return (bool)preg_match('/^[a-z0-9][a-z0-9_-]*$/i', $slug);
|
||||
}
|
||||
|
||||
function website_doc_path(string $slug, string $fileName = 'index.php'): ?string
|
||||
{
|
||||
if (!website_is_valid_doc_slug($slug)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$docsRoot = website_billing_docs_root();
|
||||
if ($docsRoot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidate = realpath($docsRoot . '/' . $slug . '/' . $fileName);
|
||||
if ($candidate === false || strpos($candidate, realpath($docsRoot) ?: $docsRoot) !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
function website_doc_icon_url(string $slug): ?string
|
||||
{
|
||||
foreach (['icon.png', 'icon.jpg', 'icon.jpeg', 'icon.webp'] as $fileName) {
|
||||
if (website_doc_path($slug, $fileName) !== null) {
|
||||
return website_url('doc_asset.php?doc=' . rawurlencode($slug) . '&file=' . rawurlencode($fileName));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function website_service_image_url(string $imageValue): string
|
||||
{
|
||||
$imageValue = trim($imageValue);
|
||||
if ($imageValue === '') {
|
||||
return website_asset('images/banner.png');
|
||||
}
|
||||
|
||||
if (preg_match('#^https?://#i', $imageValue) === 1) {
|
||||
return $imageValue;
|
||||
}
|
||||
|
||||
$fileName = basename($imageValue);
|
||||
if ($fileName === '') {
|
||||
return website_asset('images/banner.png');
|
||||
}
|
||||
|
||||
return website_asset('images/games/' . $fileName);
|
||||
}
|
||||
|
||||
function website_fetch_services(int $limit = 0): array
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$prefix = website_table_prefix();
|
||||
if ($prefix === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = "SELECT bs.service_id,
|
||||
bs.service_name,
|
||||
bs.description,
|
||||
bs.img_url,
|
||||
bs.price_monthly,
|
||||
bs.remote_server_id,
|
||||
ch.game_name AS cfg_game_name,
|
||||
ch.game_key AS cfg_game_key,
|
||||
ch.home_cfg_file AS cfg_file
|
||||
FROM `{$prefix}billing_services` bs
|
||||
LEFT JOIN `{$prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
WHERE bs.enabled = 1
|
||||
AND bs.remote_server_id <> ''
|
||||
AND bs.remote_server_id IS NOT NULL
|
||||
ORDER BY bs.service_name ASC";
|
||||
|
||||
if ($limit > 0) {
|
||||
$sql .= ' LIMIT ' . max(1, $limit);
|
||||
}
|
||||
|
||||
$result = @$db->query($sql);
|
||||
if (!$result instanceof mysqli_result) {
|
||||
website_log('Failed to query billing services for website catalog.');
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
$result->free();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function website_fetch_doc_index(): array
|
||||
{
|
||||
$docsRoot = website_billing_docs_root();
|
||||
if ($docsRoot === null || !is_dir($docsRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach (array_diff(scandir($docsRoot) ?: [], ['.', '..']) as $folder) {
|
||||
$docFolder = $docsRoot . '/' . $folder;
|
||||
if (!is_dir($docFolder)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexPath = website_doc_path($folder, 'index.php');
|
||||
$metadataPath = website_doc_path($folder, 'metadata.json');
|
||||
if ($indexPath === null || $metadataPath === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metadataContent = @file_get_contents($metadataPath);
|
||||
$metadataContent = $metadataContent === false ? '' : preg_replace('/^\xEF\xBB\xBF/', '', $metadataContent);
|
||||
$metadata = json_decode((string)$metadataContent, true);
|
||||
if (!is_array($metadata)) {
|
||||
$metadata = [];
|
||||
}
|
||||
|
||||
$entries[] = [
|
||||
'slug' => $folder,
|
||||
'name' => (string)($metadata['name'] ?? ucwords(str_replace(['-', '_'], ' ', $folder))),
|
||||
'description' => (string)($metadata['description'] ?? ''),
|
||||
'category' => (string)($metadata['category'] ?? 'other'),
|
||||
'order' => (int)($metadata['order'] ?? 999),
|
||||
'complete' => (bool)($metadata['complete'] ?? true),
|
||||
'icon_url' => website_doc_icon_url($folder),
|
||||
];
|
||||
}
|
||||
|
||||
usort(
|
||||
$entries,
|
||||
static function (array $left, array $right): int {
|
||||
if ($left['category'] !== $right['category']) {
|
||||
return strcmp($left['category'], $right['category']);
|
||||
}
|
||||
if ($left['order'] !== $right['order']) {
|
||||
return $left['order'] <=> $right['order'];
|
||||
}
|
||||
return strcasecmp($left['name'], $right['name']);
|
||||
}
|
||||
);
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
function website_render(string $pageTemplate, array $context = []): void
|
||||
{
|
||||
extract($context, EXTR_SKIP);
|
||||
require WEBSITE_INCLUDE_DIR . '/header.php';
|
||||
require WEBSITE_ROOT_DIR . '/pages/' . $pageTemplate;
|
||||
require WEBSITE_INCLUDE_DIR . '/footer.php';
|
||||
}
|
||||
|
||||
57
Panel/modules/website/includes/footer.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$discordUrl = trim((string)website_config('discord_url', ''));
|
||||
$supportUrl = trim((string)website_config('support_url', ''));
|
||||
$supportEmail = trim((string)website_config('support_email', ''));
|
||||
?>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-grid">
|
||||
<div>
|
||||
<a class="footer-brand" href="<?= website_escape(website_url('index.php')) ?>">
|
||||
<img src="<?= website_escape(website_asset('images/logo-sm.png')) ?>" alt="Gameservers.World logo" class="footer-logo">
|
||||
<span><?= website_escape(website_config('site_name')) ?></span>
|
||||
</a>
|
||||
<p class="footer-copy"><?= website_escape(website_config('site_tagline')) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Explore</h2>
|
||||
<ul class="footer-links">
|
||||
<li><a href="<?= website_escape(website_url('serverlist.php')) ?>">Game Servers</a></li>
|
||||
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a></li>
|
||||
<li><a href="<?= website_escape(website_url('pricing.php')) ?>">Pricing</a></li>
|
||||
<li><a href="<?= website_escape(website_url('locations.php')) ?>">Locations</a></li>
|
||||
<li><a href="<?= website_escape(website_url('support.php')) ?>">Support</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Customer Access</h2>
|
||||
<ul class="footer-links">
|
||||
<li><a href="<?= website_escape(login_url()) ?>">Customer Login</a></li>
|
||||
<li><a href="<?= website_escape(panel_url()) ?>">Control Panel</a></li>
|
||||
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Server Guides</a></li>
|
||||
<?php if ($discordUrl !== ''): ?>
|
||||
<li><a href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Need Help?</h2>
|
||||
<ul class="footer-links">
|
||||
<?php if ($supportUrl !== ''): ?>
|
||||
<li><a href="<?= website_escape($supportUrl) ?>" target="_blank" rel="noopener noreferrer">Support Portal</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if ($supportEmail !== ''): ?>
|
||||
<li><a href="mailto:<?= website_escape($supportEmail) ?>"><?= website_escape($supportEmail) ?></a></li>
|
||||
<?php endif; ?>
|
||||
<li><a href="<?= website_escape(website_url('support.php')) ?>">Contact Options</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="<?= website_escape(website_asset('js/site.js')) ?>" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
34
Panel/modules/website/includes/header.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$pageTitle = $pageTitle ?? website_config('site_name');
|
||||
$metaDescription = $metaDescription ?? website_config('meta_description');
|
||||
$canonicalPath = $canonicalPath ?? '';
|
||||
$socialImage = $socialImage ?? website_asset('images/banner.png');
|
||||
$bodyClass = $bodyClass ?? '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= website_escape($pageTitle) ?></title>
|
||||
<meta name="description" content="<?= website_escape($metaDescription) ?>">
|
||||
<link rel="canonical" href="<?= website_escape(website_canonical_url($canonicalPath)) ?>">
|
||||
<meta property="og:title" content="<?= website_escape($pageTitle) ?>">
|
||||
<meta property="og:description" content="<?= website_escape($metaDescription) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="<?= website_escape(website_canonical_url($canonicalPath)) ?>">
|
||||
<meta property="og:image" content="<?= website_escape($socialImage) ?>">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="<?= website_escape($pageTitle) ?>">
|
||||
<meta name="twitter:description" content="<?= website_escape($metaDescription) ?>">
|
||||
<meta name="twitter:image" content="<?= website_escape($socialImage) ?>">
|
||||
<link rel="icon" type="image/png" href="<?= website_escape(website_asset('images/logo-sm.png')) ?>">
|
||||
<link rel="stylesheet" href="<?= website_escape(website_asset('css/site.css')) ?>">
|
||||
</head>
|
||||
<body class="<?= website_escape($bodyClass) ?>">
|
||||
<?php require WEBSITE_INCLUDE_DIR . '/navigation.php'; ?>
|
||||
<main class="site-main">
|
||||
|
||||
45
Panel/modules/website/includes/navigation.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$activePage = $activePage ?? '';
|
||||
$navLinks = [
|
||||
['key' => 'home', 'label' => 'Home', 'href' => website_url('index.php')],
|
||||
['key' => 'servers', 'label' => 'Game Servers', 'href' => website_url('serverlist.php')],
|
||||
['key' => 'docs', 'label' => 'Documentation', 'href' => website_url('docs.php')],
|
||||
['key' => 'pricing', 'label' => 'Pricing', 'href' => website_url('pricing.php')],
|
||||
['key' => 'locations', 'label' => 'Locations', 'href' => website_url('locations.php')],
|
||||
['key' => 'support', 'label' => 'Support', 'href' => website_url('support.php')],
|
||||
];
|
||||
?>
|
||||
<header class="site-header">
|
||||
<div class="container header-shell">
|
||||
<a class="brand" href="<?= website_escape(website_url('index.php')) ?>">
|
||||
<img src="<?= website_escape(website_asset('images/logo-sm.png')) ?>" alt="Gameservers.World logo" class="brand-logo">
|
||||
<span class="brand-copy">
|
||||
<span class="brand-name"><?= website_escape(website_config('site_name')) ?></span>
|
||||
<span class="brand-tagline">Virtual private game servers</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<button class="nav-toggle" type="button" aria-expanded="false" aria-controls="primary-nav" data-nav-toggle>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</button>
|
||||
|
||||
<nav class="primary-nav" id="primary-nav" data-nav-menu>
|
||||
<?php foreach ($navLinks as $link): ?>
|
||||
<a class="nav-link<?= $activePage === $link['key'] ? ' is-active' : '' ?>" href="<?= website_escape($link['href']) ?>">
|
||||
<?= website_escape($link['label']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="header-actions" data-header-actions>
|
||||
<a class="button button-secondary" href="<?= website_escape(login_url()) ?>">Login</a>
|
||||
<a class="button button-primary" href="<?= website_escape(panel_url()) ?>">Control Panel</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
20
Panel/modules/website/includes/paths.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (defined('GSP_WEBSITE_PATHS_LOADED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
define('GSP_WEBSITE_PATHS_LOADED', true);
|
||||
define('WEBSITE_ROOT_DIR', realpath(dirname(__DIR__)) ?: dirname(__DIR__));
|
||||
define('WEBSITE_INCLUDE_DIR', __DIR__);
|
||||
define('WEBSITE_CONFIG_DIR', WEBSITE_ROOT_DIR . '/config');
|
||||
define('WEBSITE_ASSET_DIR', WEBSITE_ROOT_DIR . '/assets');
|
||||
define('WEBSITE_PANEL_ROOT', realpath(dirname(dirname(WEBSITE_ROOT_DIR))) ?: dirname(dirname(WEBSITE_ROOT_DIR)));
|
||||
define('WEBSITE_PANEL_INCLUDE_DIR', WEBSITE_PANEL_ROOT . '/includes');
|
||||
define('WEBSITE_MODULES_DIR', WEBSITE_PANEL_ROOT . '/modules');
|
||||
define('WEBSITE_BILLING_ROOT', WEBSITE_MODULES_DIR . '/billing');
|
||||
define('WEBSITE_BILLING_DOCS_DIR', WEBSITE_BILLING_ROOT . '/docs');
|
||||
define('WEBSITE_LEGACY_SITE_ROOT', realpath(dirname(WEBSITE_PANEL_ROOT) . '/Website') ?: dirname(WEBSITE_PANEL_ROOT) . '/Website');
|
||||
|
||||
21
Panel/modules/website/index.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$services = website_fetch_services(6);
|
||||
$locations = website_config('locations', []);
|
||||
|
||||
website_render(
|
||||
'home.php',
|
||||
[
|
||||
'activePage' => 'home',
|
||||
'pageTitle' => 'Gameservers.World - Virtual private game servers',
|
||||
'metaDescription' => website_config('meta_description'),
|
||||
'canonicalPath' => 'index.php',
|
||||
'services' => $services,
|
||||
'locations' => is_array($locations) ? $locations : [],
|
||||
]
|
||||
);
|
||||
|
||||
17
Panel/modules/website/locations.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
website_render(
|
||||
'locations.php',
|
||||
[
|
||||
'activePage' => 'locations',
|
||||
'pageTitle' => 'Locations - Gameservers.World',
|
||||
'metaDescription' => 'Current Gameservers.World hosting regions and deployment guidance.',
|
||||
'canonicalPath' => 'locations.php',
|
||||
'locations' => website_config('locations', []),
|
||||
]
|
||||
);
|
||||
|
||||
23
Panel/modules/website/login.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$destination = login_url();
|
||||
if ($destination !== '') {
|
||||
header('Location: ' . $destination, true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
website_render(
|
||||
'support.php',
|
||||
[
|
||||
'activePage' => 'support',
|
||||
'pageTitle' => 'Login - Gameservers.World',
|
||||
'metaDescription' => 'Customer login routing for Gameservers.World.',
|
||||
'canonicalPath' => 'login.php',
|
||||
'loginUnavailable' => true,
|
||||
]
|
||||
);
|
||||
|
||||
71
Panel/modules/website/pages/documentation.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$categoryLabels = [
|
||||
'panel' => 'Panel Documentation',
|
||||
'game' => 'Game Servers',
|
||||
'mods' => 'Mods and Plugins',
|
||||
'troubleshooting' => 'Troubleshooting',
|
||||
'other' => 'Other',
|
||||
];
|
||||
|
||||
$groupedDocs = [];
|
||||
foreach ($docIndex as $entry) {
|
||||
$groupedDocs[$entry['category']][] = $entry;
|
||||
}
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1><?= $docEntry !== null ? website_escape($docEntry['name']) : 'Documentation' ?></h1>
|
||||
<p><?= $docEntry !== null ? website_escape((string)($docEntry['description'] ?? '')) : 'Browse customer guides, panel references, and game-specific setup documentation without depending on billing configuration.' ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<?php if ($docEntry !== null && $docContent !== null): ?>
|
||||
<div class="stack-actions" style="margin-bottom: 18px;">
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Back to documentation</a>
|
||||
<a class="button button-ghost" href="<?= website_escape(website_url('index.php')) ?>">Home</a>
|
||||
</div>
|
||||
<article class="doc-view">
|
||||
<?= $docContent ?>
|
||||
</article>
|
||||
<?php elseif (!empty($docIndex)): ?>
|
||||
<div class="doc-list-group">
|
||||
<?php foreach ($groupedDocs as $category => $entries): ?>
|
||||
<section>
|
||||
<div class="section-heading">
|
||||
<h2><?= website_escape($categoryLabels[$category] ?? ucwords($category)) ?></h2>
|
||||
<p><?= website_escape(($categoryLabels[$category] ?? $category) . ' available through the shared GSP documentation set.') ?></p>
|
||||
</div>
|
||||
<div class="doc-grid">
|
||||
<?php foreach ($entries as $entry): ?>
|
||||
<article class="doc-card">
|
||||
<header>
|
||||
<?php if (!empty($entry['icon_url'])): ?>
|
||||
<img src="<?= website_escape((string)$entry['icon_url']) ?>" alt="" style="width:56px;height:56px;border-radius:6px;border:1px solid var(--line);margin-bottom:14px;object-fit:cover;">
|
||||
<?php endif; ?>
|
||||
<h3><?= website_escape((string)$entry['name']) ?></h3>
|
||||
<div class="doc-meta"><?= website_escape((string)$entry['category']) ?></div>
|
||||
</header>
|
||||
<p><?= website_escape((string)$entry['description']) ?></p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(documentation_url((string)$entry['slug'])) ?>">Open Guide</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<h2>Documentation is unavailable</h2>
|
||||
<p>No documentation source directory was found. Check that the billing module docs directory is present alongside the website module.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
61
Panel/modules/website/pages/game_servers.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$hasBilling = website_billing_available();
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Game Servers</h1>
|
||||
<p>Available services are pulled from the existing billing catalog when the database is configured. This page no longer reaches into billing includes with hardcoded filesystem paths, so it stays up even when billing config is missing.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<?php if ($hasBilling && !empty($services)): ?>
|
||||
<div class="service-grid">
|
||||
<?php foreach ($services as $service): ?>
|
||||
<?php
|
||||
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
|
||||
$description = trim((string)($service['description'] ?? ''));
|
||||
$price = (float)($service['price_monthly'] ?? 0);
|
||||
$orderHref = billing_url('order.php?service_id=' . rawurlencode((string)$service['service_id']));
|
||||
$docSlug = trim((string)($service['cfg_game_key'] ?? ''));
|
||||
?>
|
||||
<article class="service-card">
|
||||
<img src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="<?= website_escape($serviceName) ?>">
|
||||
<header>
|
||||
<h3><?= website_escape($serviceName) ?></h3>
|
||||
<div class="service-meta"><?= website_escape((string)($service['cfg_game_key'] ?? '')) ?></div>
|
||||
</header>
|
||||
<p><?= website_escape($description !== '' ? $description : 'Dedicated resources, full configuration access, and GSP panel management.') ?></p>
|
||||
<div class="section-divider"></div>
|
||||
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape($orderHref) ?>">Order Now</a>
|
||||
<?php if ($docSlug !== '' && website_doc_path($docSlug) !== null): ?>
|
||||
<a class="button button-secondary" href="<?= website_escape(documentation_url($docSlug)) ?>">Documentation</a>
|
||||
<?php else: ?>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<h2>Server catalog unavailable</h2>
|
||||
<p><?= website_escape((string)website_config('admin_notice')) ?></p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('index.php')) ?>">Return Home</a>
|
||||
</div>
|
||||
<?php if (!website_billing_config_present()): ?>
|
||||
<p class="muted">Billing configuration is missing or unreadable. Public pages still load, but service data cannot be queried until billing or panel database credentials are configured.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
245
Panel/modules/website/pages/home.php
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$discordUrl = trim((string)website_config('discord_url', ''));
|
||||
?>
|
||||
<section class="hero">
|
||||
<div class="container hero-layout">
|
||||
<div>
|
||||
<div class="eyebrow">Dedicated resources. Full control. Real support.</div>
|
||||
<h1>Virtual private game servers with predictable resources and full configuration access.</h1>
|
||||
<p><?= website_escape(website_config('site_tagline')) ?> Gameservers.World focuses on stable capacity, older community-favorite titles, mod support, and a control panel that gives customers practical access instead of a stripped-down toy interface.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">View Game Servers</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Open Control Panel</a>
|
||||
</div>
|
||||
<div class="hero-points">
|
||||
<div class="hero-point">
|
||||
<strong>Never oversold</strong>
|
||||
<span class="muted">Dedicated resources stay dedicated. Predictable performance is the product, not an upgrade.</span>
|
||||
</div>
|
||||
<div class="hero-point">
|
||||
<strong>Mod and Workshop support</strong>
|
||||
<span class="muted">Common modding workflows, Workshop content, and file access stay available where the game allows it.</span>
|
||||
</div>
|
||||
<div class="hero-point">
|
||||
<strong>Control and customization</strong>
|
||||
<span class="muted">Startup parameters, configs, backups, and updates are managed through the GSP panel without hiding the important controls.</span>
|
||||
</div>
|
||||
<div class="hero-point">
|
||||
<strong>Real people</strong>
|
||||
<span class="muted">Documentation exists, but you can still talk to someone when an old modpack or legacy title gets awkward.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<img src="<?= website_escape(website_asset('images/banner.png')) ?>" alt="Gameservers.World supported games collage">
|
||||
<div class="hero-visual-card">
|
||||
<strong>Built for hosted communities that want actual control</strong>
|
||||
<p>Use the GSP panel for server start/stop, config editing, Workshop or mod workflows, logs, file access, and routine maintenance without depending on fragile manual shell work.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<strong>Dedicated</strong>
|
||||
<span>No noisy-neighbor overselling.</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<strong>Full access</strong>
|
||||
<span>Configs, startup parameters, files, backups, and updates.</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<strong>Legacy-friendly</strong>
|
||||
<span>Older and community-favorite games stay part of the catalog.</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<strong>Operational help</strong>
|
||||
<span>Human support when the docs are not enough.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<h2>Why Gameservers.World</h2>
|
||||
<p>The website now makes the actual sales proposition explicit: dedicated capacity, access to the knobs that matter, and hosting for both current and older multiplayer titles without burying customers behind fragile shared-hosting abstractions.</p>
|
||||
</div>
|
||||
<div class="feature-grid">
|
||||
<article class="feature-card">
|
||||
<h3>Dedicated resources</h3>
|
||||
<p>Capacity is reserved for the server you pay for. CPU and memory planning are aimed at stable behavior, not squeezing one more tenant onto the node.</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h3>Mod support</h3>
|
||||
<p>Workshop and addon workflows are supported where the underlying game tooling allows it, with file access and update paths that customers can actually use.</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h3>Panel control</h3>
|
||||
<p>Customers manage their service through GSP rather than opening tickets for every config tweak, reboot, backup, or startup parameter change.</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<h3>Real support</h3>
|
||||
<p>When an old title, community mod, or provisioning edge case gets weird, support is available through documentation plus direct assistance.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<h2>Current locations</h2>
|
||||
<p>Locations are configured data, not marketing filler. These reflect the currently documented host regions from the existing Gameservers.World site content.</p>
|
||||
</div>
|
||||
<div class="location-grid">
|
||||
<?php foreach ($locations as $location): ?>
|
||||
<article class="location-card">
|
||||
<h3><?= website_escape((string)($location['name'] ?? '')) ?></h3>
|
||||
<p><?= website_escape((string)($location['region'] ?? '')) ?></p>
|
||||
<small><?= website_escape((string)($location['host'] ?? '')) ?></small>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<h2>Popular and supported game servers</h2>
|
||||
<p>These cards are driven from the existing billing catalog when configuration is available. The site does not invent fake availability if the billing catalog is offline.</p>
|
||||
</div>
|
||||
<?php if (!empty($services)): ?>
|
||||
<div class="service-grid">
|
||||
<?php foreach ($services as $service): ?>
|
||||
<?php
|
||||
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
|
||||
$price = (float)($service['price_monthly'] ?? 0);
|
||||
$orderUrl = billing_url('order.php?service_id=' . rawurlencode((string)$service['service_id']));
|
||||
?>
|
||||
<article class="service-card">
|
||||
<img src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="<?= website_escape($serviceName) ?>">
|
||||
<header>
|
||||
<h3><?= website_escape($serviceName) ?></h3>
|
||||
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
|
||||
</header>
|
||||
<p><?= website_escape(trim((string)($service['description'] ?? 'Dedicated hosting with full configuration access.')) ?: 'Dedicated hosting with full configuration access.') ?></p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape($orderUrl) ?>">Order</a>
|
||||
<a class="button button-ghost" href="<?= website_escape(website_url('serverlist.php')) ?>">Catalog</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<h3>Catalog visibility depends on billing data</h3>
|
||||
<p><?= website_escape((string)website_config('admin_notice')) ?></p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
|
||||
<a class="button button-ghost" href="<?= website_escape(website_url('docs.php')) ?>">Read Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<h2>How it works</h2>
|
||||
<p>The public site and the control plane are now separated cleanly: the website explains the service, the billing catalog handles order-specific data, and GSP runs the operational lifecycle.</p>
|
||||
</div>
|
||||
<div class="flow-grid">
|
||||
<article class="flow-step">
|
||||
<span class="flow-step-number">1</span>
|
||||
<h3>Choose a server</h3>
|
||||
<p>Browse the supported game catalog, confirm the available plan, and select a region that matches your community.</p>
|
||||
</article>
|
||||
<article class="flow-step">
|
||||
<span class="flow-step-number">2</span>
|
||||
<h3>Configure it</h3>
|
||||
<p>Use the GSP panel to set startup parameters, edit configs, install supported content, and keep backups or updates under control.</p>
|
||||
</article>
|
||||
<article class="flow-step">
|
||||
<span class="flow-step-number">3</span>
|
||||
<h3>Manage through GSP</h3>
|
||||
<p>Operate the service through the panel for lifecycle, monitoring, logs, file access, and routine administrative work.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="panel-preview">
|
||||
<div class="panel-preview-shell">
|
||||
<img src="<?= website_escape(website_asset('images/dark.jpg')) ?>" alt="GSP control panel preview background">
|
||||
</div>
|
||||
<div class="panel-preview-stats">
|
||||
<div class="section-heading">
|
||||
<h2>GSP control panel</h2>
|
||||
<p>Customers do not get a fake storefront-only experience. They manage hosted servers through the same GSP platform that handles starts, stops, files, logs, addons, scheduling, and provisioning workflows.</p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>Operational access</h3>
|
||||
<p>Start and stop servers, manage files, inspect logs, update Workshop content, and adjust configuration safely from the panel.</p>
|
||||
</div>
|
||||
<div class="stack-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(panel_url()) ?>">Open Control Panel</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Read Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="summary-grid">
|
||||
<article class="summary-card">
|
||||
<h3>Documentation and support</h3>
|
||||
<p>Game-specific docs, panel guidance, and troubleshooting live in the same repo and can be served without depending on billing configuration.</p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a>
|
||||
<a class="button button-ghost" href="<?= website_escape(website_url('support.php')) ?>">Support</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<h3>Panel and login routing</h3>
|
||||
<p>The Control Panel and Login actions are configuration-driven. The public site no longer assumes a fragile `/panel/` path under the marketing domain.</p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(login_url()) ?>">Login</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Control Panel</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta-band">
|
||||
<div class="container">
|
||||
<div class="cta-panel">
|
||||
<div>
|
||||
<h2>Ready to deploy a server?</h2>
|
||||
<p>Browse the current game catalog, confirm the right location, and manage the result through GSP.</p>
|
||||
</div>
|
||||
<div class="stack-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">View Game Servers</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Open Control Panel</a>
|
||||
<?php if ($discordUrl !== ''): ?>
|
||||
<a class="button button-ghost" href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
42
Panel/modules/website/pages/locations.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$locations = is_array($locations) ? $locations : [];
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Locations</h1>
|
||||
<p>Deployment regions are configured data. Large or latency-sensitive communities should choose a region that matches where the players actually are, not just where the cheapest machine happened to be.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="location-grid">
|
||||
<?php foreach ($locations as $location): ?>
|
||||
<article class="location-card">
|
||||
<h3><?= website_escape((string)($location['name'] ?? '')) ?></h3>
|
||||
<p><?= website_escape((string)($location['region'] ?? '')) ?></p>
|
||||
<small><?= website_escape((string)($location['host'] ?? '')) ?></small>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="summary-grid">
|
||||
<article class="summary-card">
|
||||
<h3>Regional fit matters</h3>
|
||||
<p>Pick a region based on player distribution and the specific game. Older titles and mod-heavy servers can be more sensitive to latency and disk performance than the average commodity hosting page admits.</p>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<h3>Need a different region?</h3>
|
||||
<p>If the catalog does not list the region you need yet, contact support. The site copy now makes this explicit instead of hiding the conversation behind a dead link.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
55
Panel/modules/website/pages/pricing.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Pricing</h1>
|
||||
<p>Gameservers.World pricing is catalog-driven. The public site does not hardcode fake plan availability; it shows live service rows when the billing catalog is available and falls back to guidance when it is not.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="summary-grid">
|
||||
<article class="summary-card">
|
||||
<h3>What you are paying for</h3>
|
||||
<p>Dedicated resources, full configuration access, mod and Workshop support where available, control through GSP, backups, and support from people who know the game hosting side of the product.</p>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<h3>How pricing is presented</h3>
|
||||
<p>Plan data comes from the billing catalog. That keeps the marketing site aligned with the actual services table instead of drifting into stale hardcoded prices.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<?php if (!empty($services)): ?>
|
||||
<div class="service-grid">
|
||||
<?php foreach ($services as $service): ?>
|
||||
<?php
|
||||
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
|
||||
$price = (float)($service['price_monthly'] ?? 0);
|
||||
?>
|
||||
<article class="service-card">
|
||||
<h3><?= website_escape($serviceName) ?></h3>
|
||||
<p><?= website_escape(trim((string)($service['description'] ?? '')) ?: 'Dedicated hosting with full configuration access.') ?></p>
|
||||
<div class="section-divider"></div>
|
||||
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(billing_url('order.php?service_id=' . rawurlencode((string)$service['service_id']))) ?>">Order</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Catalog</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert info">
|
||||
<?= website_escape((string)website_config('admin_notice')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
58
Panel/modules/website/pages/support.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$discordUrl = trim((string)website_config('discord_url', ''));
|
||||
$supportUrl = trim((string)website_config('support_url', ''));
|
||||
$supportEmail = trim((string)website_config('support_email', ''));
|
||||
$loginUnavailable = $loginUnavailable ?? false;
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Support</h1>
|
||||
<p>Documentation should handle the routine cases. Support exists for the ones that are still operationally messy: mods, legacy engines, provisioning issues, and the inevitable edge cases around community-maintained titles.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<?php if ($loginUnavailable): ?>
|
||||
<div class="alert">
|
||||
Login URL is not configured for this deployment. Set <code>login_url</code> in the website config to route customers to the active panel login.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="support-grid">
|
||||
<article class="support-card">
|
||||
<h3>Customer login</h3>
|
||||
<p>Access existing servers, files, logs, and administrative workflows through the configured GSP panel login.</p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(login_url()) ?>">Login</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Panel</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="support-card">
|
||||
<h3>Documentation</h3>
|
||||
<p>Server guides, panel references, and troubleshooting content are available even if billing configuration is missing.</p>
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(website_url('docs.php')) ?>">Open Docs</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="support-card">
|
||||
<h3>Direct help</h3>
|
||||
<p>Use Discord, a support portal, or email depending on what is configured for this deployment.</p>
|
||||
<div class="card-actions">
|
||||
<?php if ($discordUrl !== ''): ?>
|
||||
<a class="button button-primary" href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($supportUrl !== ''): ?>
|
||||
<a class="button button-secondary" href="<?= website_escape($supportUrl) ?>" target="_blank" rel="noopener noreferrer">Support Portal</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($supportEmail !== ''): ?>
|
||||
<a class="button button-ghost" href="mailto:<?= website_escape($supportEmail) ?>">Email</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
17
Panel/modules/website/pricing.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
website_render(
|
||||
'pricing.php',
|
||||
[
|
||||
'activePage' => 'pricing',
|
||||
'pageTitle' => 'Pricing - Gameservers.World',
|
||||
'metaDescription' => 'Pricing guidance and available game server plans for Gameservers.World.',
|
||||
'canonicalPath' => 'pricing.php',
|
||||
'services' => website_fetch_services(),
|
||||
]
|
||||
);
|
||||
|
||||
19
Panel/modules/website/serverlist.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$services = website_fetch_services();
|
||||
|
||||
website_render(
|
||||
'game_servers.php',
|
||||
[
|
||||
'activePage' => 'servers',
|
||||
'pageTitle' => 'Game Servers - Gameservers.World',
|
||||
'metaDescription' => 'Browse supported game server packages, locations, and ordering options for Gameservers.World.',
|
||||
'canonicalPath' => 'serverlist.php',
|
||||
'services' => $services,
|
||||
]
|
||||
);
|
||||
|
||||
16
Panel/modules/website/support.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
website_render(
|
||||
'support.php',
|
||||
[
|
||||
'activePage' => 'support',
|
||||
'pageTitle' => 'Support - Gameservers.World',
|
||||
'metaDescription' => 'Support, Discord, documentation, and control panel access for Gameservers.World customers.',
|
||||
'canonicalPath' => 'support.php',
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -42,6 +42,7 @@ This is the master module inventory for the Panel. Use it as the first stop befo
|
|||
| [`user_admin`](user_admin.md) | User management | Production | Keep / Improve |
|
||||
| [`user_games`](user_games.md) | Server provisioning and assignment | Production / functional | Keep / Improve |
|
||||
| [`util`](util.md) | Miscellaneous utility tools | Functional / mixed | Keep / Rewrite selectively |
|
||||
| [`website`](website.md) | Public Gameservers.World sales and documentation website | Functional / newly modularized | Keep / Improve |
|
||||
|
||||
## Shared Dependencies
|
||||
|
||||
|
|
|
|||
59
docs/modules/website.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Website
|
||||
|
||||
Workspace reference: [`GSP-WORKSPACE.md`](../../../GSP-WORKSPACE.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
Public Gameservers.World sales, documentation, and customer-entry website.
|
||||
|
||||
## Runtime Location
|
||||
|
||||
- Canonical public site: `Panel/modules/website/`
|
||||
- Billing catalog and order flow: `Panel/modules/billing/`
|
||||
- Legacy compatibility site: `Website/`
|
||||
|
||||
## Design Rules
|
||||
|
||||
- Public pages must load without fatal errors when billing config is missing
|
||||
- Path and URL handling must be centralized
|
||||
- Panel and login URLs must be configuration-driven
|
||||
- Documentation must be readable without depending on billing database access
|
||||
- Catalog pages may degrade gracefully when billing data is unavailable
|
||||
|
||||
## Helpers
|
||||
|
||||
The website module centralizes these helpers in `includes/bootstrap.php`:
|
||||
|
||||
- `website_url()`
|
||||
- `website_asset()`
|
||||
- `panel_url()`
|
||||
- `login_url()`
|
||||
- `billing_url()`
|
||||
- `documentation_url()`
|
||||
|
||||
## Billing Interaction
|
||||
|
||||
The website does not include the billing config loader directly. It reads panel or billing DB values safely, uses them only when needed, and avoids public fatal errors tied to missing config files.
|
||||
|
||||
## Deployment
|
||||
|
||||
Recommended:
|
||||
|
||||
1. `DocumentRoot` -> `Panel/modules/website`
|
||||
2. Apache `Alias /billing` -> `Panel/modules/billing`
|
||||
3. Configure:
|
||||
- `public_base_url`
|
||||
- `billing_base_url`
|
||||
- `panel_url`
|
||||
- `login_url`
|
||||
|
||||
## Key Public Pages
|
||||
|
||||
- `index.php`
|
||||
- `serverlist.php`
|
||||
- `docs.php`
|
||||
- `pricing.php`
|
||||
- `locations.php`
|
||||
- `support.php`
|
||||
- `login.php`
|
||||
|
||||