feat: complete in-game commands, GUIs, admin panel fixes, and launcher enhancements
This commit is contained in:
1 parent
a9ff92e1d1
commit
752f0cc43d
34 files changed
+4170
-181
No files matched your search
@@ -163,6 +163,7 @@ export interface LandingBlock {
|
||||
enabled: boolean;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface LandingConfig {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Plus, Pencil, Trash2, Trophy, Sparkles, Shield, Swords, Pickaxe, Compass, Users } from '@lucide/svelte';
|
||||
import Modal from '../components/Modal.svelte';
|
||||
import { get, post, put, del } from '../lib/api';
|
||||
@@ -61,7 +62,21 @@
|
||||
async function loadAchievements() {
|
||||
loading = true;
|
||||
try {
|
||||
achievements = await get<Achievement[]>('/api/admin/achievements');
|
||||
const res = await get<any[]>('/api/admin/achievements');
|
||||
achievements = (res || []).map((a: any) => ({
|
||||
id: a.id || '',
|
||||
title: a.title || '',
|
||||
description: a.description || '',
|
||||
category: a.category || 'progression',
|
||||
xp_reward: a.xp_reward || 0,
|
||||
frame_type: a.frame_type || a.icon_frame || 'task',
|
||||
icon_item: a.icon_item || 'diamond_pickaxe',
|
||||
icon_bg: a.icon_bg || 'deepslate',
|
||||
icon_border: a.icon_border || 'gold',
|
||||
stat_type: a.stat_type || a.requirement_key || 'blocks_broken',
|
||||
target_count: a.target_count || a.requirement_value || 1,
|
||||
secret: !!a.secret,
|
||||
}));
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
@@ -69,7 +84,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
loadAchievements();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Shield, Users, MapPin, Trash2, Search, ExternalLink, Flag } from '@lucide/svelte';
|
||||
import Modal from '../components/Modal.svelte';
|
||||
import { get, del } from '../lib/api';
|
||||
@@ -17,11 +18,28 @@
|
||||
loading = true;
|
||||
try {
|
||||
const [g, inst] = await Promise.all([
|
||||
get<Guild[]>('/api/admin/guilds'),
|
||||
get<Instance[]>('/api/v1/instances'),
|
||||
get<any[]>('/api/admin/guilds').catch(() => []),
|
||||
get<Instance[]>('/api/admin/instances').catch(() => []),
|
||||
]);
|
||||
guilds = g;
|
||||
instances = inst;
|
||||
guilds = (g || []).map((x: any) => ({
|
||||
id: x.id || '',
|
||||
instance_id: x.instance_id || '',
|
||||
name: x.name || 'Unnamed Guild',
|
||||
tag: x.tag || '',
|
||||
description: x.description || '',
|
||||
motd: x.motd || '',
|
||||
leader_uuid: x.leader_uuid || '',
|
||||
icon_url: x.icon_url || null,
|
||||
banner_url: x.banner_url || null,
|
||||
level: x.level || 1,
|
||||
xp: x.xp || 0,
|
||||
max_claims: x.max_claims || 10,
|
||||
member_count: x.member_count ?? 1,
|
||||
max_members: x.max_members || 50,
|
||||
claims_count: x.claims_count ?? 0,
|
||||
created_at: x.created_at || new Date().toISOString(),
|
||||
}));
|
||||
instances = inst || [];
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
@@ -29,7 +47,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Save, LoaderCircle, Plus, Trash2, ArrowUp, ArrowDown, ExternalLink,
|
||||
Upload, Globe, Sparkles, HelpCircle, Server, Trophy, BarChart3,
|
||||
Boxes, Newspaper, MessageSquare, Download, Check, Eye, Code,
|
||||
ChevronDown, ChevronUp, Sliders,
|
||||
} from '@lucide/svelte';
|
||||
import Toggle from '../components/Toggle.svelte';
|
||||
import { api, get, put, formatBytes } from '../lib/api';
|
||||
@@ -14,6 +15,11 @@
|
||||
let saving = $state(false);
|
||||
let uploading = $state(false);
|
||||
let activeTab = $state<'blocks' | 'downloads' | 'faq' | 'css'>('blocks');
|
||||
let openBlocks = $state<Record<string, boolean>>({});
|
||||
|
||||
function toggleBlockOptions(id: string) {
|
||||
openBlocks[id] = !openBlocks[id];
|
||||
}
|
||||
|
||||
const blockMeta: Record<BlockType, { label: string; icon: any; desc: string }> = {
|
||||
hero: { label: 'Hero Header', icon: Sparkles, desc: 'Top visual headline with CTA buttons and particle effects' },
|
||||
@@ -200,39 +206,222 @@
|
||||
{#each config.blocks as block, idx (block.id)}
|
||||
{@const meta = blockMeta[block.type as BlockType] || { label: block.type, icon: Sparkles, desc: '' }}
|
||||
{@const Icon = meta.icon}
|
||||
<div class="block-row" class:disabled={!block.enabled}>
|
||||
<div class="block-reorder">
|
||||
<button class="icon-btn" disabled={idx === 0} onclick={() => moveBlock(idx, -1)} title="Move up">
|
||||
<ArrowUp size={14} />
|
||||
</button>
|
||||
<button class="icon-btn" disabled={idx === config.blocks.length - 1} onclick={() => moveBlock(idx, 1)} title="Move down">
|
||||
<ArrowDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{@const isOpen = !!openBlocks[block.id]}
|
||||
<div class="block-item-container" class:disabled={!block.enabled}>
|
||||
<div class="block-row">
|
||||
<div class="block-reorder">
|
||||
<button class="icon-btn" disabled={idx === 0} onclick={() => moveBlock(idx, -1)} title="Move up">
|
||||
<ArrowUp size={14} />
|
||||
</button>
|
||||
<button class="icon-btn" disabled={idx === config.blocks.length - 1} onclick={() => moveBlock(idx, 1)} title="Move down">
|
||||
<ArrowDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="block-icon">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<div class="block-icon">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
|
||||
<div class="block-info grow">
|
||||
<div class="row align-center gap-xs">
|
||||
<strong>{meta.label}</strong>
|
||||
<span class="block-type-tag tiny muted">{block.type}</span>
|
||||
<div class="block-info grow" role="button" tabindex="0" onclick={() => toggleBlockOptions(block.id)} onkeydown={(e) => e.key === 'Enter' && toggleBlockOptions(block.id)}>
|
||||
<div class="row align-center gap-xs">
|
||||
<strong>{meta.label}</strong>
|
||||
<span class="block-type-tag tiny muted">{block.type}</span>
|
||||
{#if block.title}
|
||||
<span class="badge-sub small muted">"{block.title}"</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="tiny muted">{meta.desc}</span>
|
||||
</div>
|
||||
|
||||
<div class="block-inputs row gap-sm align-center">
|
||||
<button class="btn secondary sm" onclick={() => toggleBlockOptions(block.id)} title="Configure Module Settings">
|
||||
<Sliders size={13} /> {isOpen ? 'Hide Options' : 'Configure'}
|
||||
{#if isOpen}<ChevronUp size={13} />{:else}<ChevronDown size={13} />{/if}
|
||||
</button>
|
||||
<Toggle bind:checked={block.enabled} label="" />
|
||||
</div>
|
||||
<span class="tiny muted">{meta.desc}</span>
|
||||
</div>
|
||||
|
||||
<div class="block-inputs row gap-sm align-center">
|
||||
{#if block.type !== 'hero'}
|
||||
<input
|
||||
bind:value={block.title}
|
||||
placeholder="Section title"
|
||||
class="sm-input"
|
||||
title="Custom section title"
|
||||
/>
|
||||
{/if}
|
||||
<Toggle bind:checked={block.enabled} label="" />
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<div class="block-config-pane">
|
||||
{#if !block.options}
|
||||
{((block.options = {}), '')}
|
||||
{/if}
|
||||
|
||||
<div class="grid-2">
|
||||
<label class="field">
|
||||
<span>Section Title</span>
|
||||
<input bind:value={block.title} placeholder="Custom Section Title" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Section Subtitle</span>
|
||||
<input bind:value={block.subtitle} placeholder="Sub-heading or explanatory text" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if block.type === 'hero'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Headline Override</span>
|
||||
<input bind:value={block.options.headline} placeholder="Defaults to General Hero Headline" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Top Badge Text</span>
|
||||
<input bind:value={block.options.badge_text} placeholder="e.g. Season 2 Now Live!" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Primary CTA Button Text</span>
|
||||
<input bind:value={block.options.cta_text} placeholder="e.g. Download Launcher" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Custom CTA URL (Overrides auto-download)</span>
|
||||
<input bind:value={block.options.cta_url} placeholder="https://..." />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Server IP Copy Button</span>
|
||||
<Toggle bind:checked={block.options.show_ip_copy} label="Enable 1-Click Server IP Copy" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'download'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Display Layout</span>
|
||||
<select bind:value={block.options.layout}>
|
||||
<option value="cards">Feature Cards (Default)</option>
|
||||
<option value="compact">Compact List</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Badge Highlight Note</span>
|
||||
<input bind:value={block.options.badge_note} placeholder="e.g. Java 21 Bundled · Auto-Sync" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show System Requirements</span>
|
||||
<Toggle bind:checked={block.options.show_specs} label="Display OS requirements info" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'servers'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Show Offline Servers</span>
|
||||
<Toggle bind:checked={block.options.show_offline} label="List servers even when offline" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Version / Software Badges</span>
|
||||
<Toggle bind:checked={block.options.show_badges} label="Display MC version and loader" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Action Button Text</span>
|
||||
<input bind:value={block.options.cta_label} placeholder="e.g. Copy Server IP" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'leaderboard'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Default Metric</span>
|
||||
<select bind:value={block.options.default_metric}>
|
||||
<option value="playtime_secs">Playtime</option>
|
||||
<option value="global_level">Global Level</option>
|
||||
<option value="player_kills">Player Kills</option>
|
||||
<option value="mob_kills">Mob Kills</option>
|
||||
<option value="blocks_broken">Blocks Mined</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Leaderboard Size</span>
|
||||
<select bind:value={block.options.limit}>
|
||||
<option value={5}>Top 5</option>
|
||||
<option value={10}>Top 10 (Default)</option>
|
||||
<option value={20}>Top 20</option>
|
||||
<option value={50}>Top 50</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Top 3 Podium</span>
|
||||
<Toggle bind:checked={block.options.show_podium} label="Highlight Gold, Silver, and Bronze winners" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'stats'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Show Players Online Counter</span>
|
||||
<Toggle bind:checked={block.options.show_players} label="Display total registered / online players" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Total Playtime Counter</span>
|
||||
<Toggle bind:checked={block.options.show_playtime} label="Display cumulative playtime" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Guilds Counter</span>
|
||||
<Toggle bind:checked={block.options.show_guilds} label="Display active guilds count" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Custom Stat (Label & Value)</span>
|
||||
<div class="row gap-xs">
|
||||
<input bind:value={block.options.custom_stat1_label} placeholder="e.g. Blocks Placed" />
|
||||
<input bind:value={block.options.custom_stat1_value} placeholder="e.g. 1.2M+" style="max-width: 100px" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'instances'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Grid Columns</span>
|
||||
<select bind:value={block.options.columns}>
|
||||
<option value={2}>2 Columns</option>
|
||||
<option value={3}>3 Columns (Default)</option>
|
||||
<option value={4}>4 Columns</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Mod Loader Badges</span>
|
||||
<Toggle bind:checked={block.options.show_loaders} label="Display Fabric / Forge / NeoForge pills" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'news'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Max Articles Shown</span>
|
||||
<select bind:value={block.options.max_items}>
|
||||
<option value={3}>3 Articles</option>
|
||||
<option value={5}>5 Articles (Default)</option>
|
||||
<option value={10}>10 Articles</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Show Date Badges</span>
|
||||
<Toggle bind:checked={block.options.show_dates} label="Show publication date on posts" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'faq'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Allow Multiple Open Questions</span>
|
||||
<Toggle bind:checked={block.options.allow_multiple} label="Visitors can expand multiple FAQs at once" />
|
||||
</label>
|
||||
</div>
|
||||
{:else if block.type === 'socials'}
|
||||
<div class="module-fields grid-2">
|
||||
<label class="field">
|
||||
<span>Discord Invite URL</span>
|
||||
<input bind:value={block.options.discord_url} placeholder="https://discord.gg/..." />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Server Webstore URL</span>
|
||||
<input bind:value={block.options.store_url} placeholder="https://store.myserver.com" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>YouTube / Twitch URL</span>
|
||||
<input bind:value={block.options.media_url} placeholder="https://youtube.com/@..." />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Twitter / X or Bluesky URL</span>
|
||||
<input bind:value={block.options.social_url} placeholder="https://x.com/..." />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -378,11 +567,17 @@
|
||||
.field input, .field textarea { background: var(--bg); border: 1px solid var(--line); color: var(--text); padding: 8px 12px; border-radius: var(--radius-sm); font-size: 14px; }
|
||||
.field input:focus, .field textarea:focus { border-color: var(--accent); outline: none; }
|
||||
.blocks-list { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
|
||||
.block-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--line); transition: opacity 0.15s, border-color 0.15s; }
|
||||
.block-row.disabled { opacity: 0.55; }
|
||||
.block-item-container { border-radius: var(--radius-sm); border: 1px solid var(--line); background: var(--bg); overflow: hidden; transition: opacity 0.15s, border-color 0.15s; }
|
||||
.block-item-container.disabled { opacity: 0.55; }
|
||||
.block-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; }
|
||||
.block-config-pane { border-top: 1px dashed var(--line); padding: 16px; background: color-mix(in srgb, var(--surface) 50%, transparent); display: flex; flex-direction: column; gap: 14px; }
|
||||
.module-fields { border-top: 1px solid var(--line); padding-top: 12px; }
|
||||
.badge-sub { font-style: italic; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
select { background: var(--bg); border: 1px solid var(--line); color: var(--text); padding: 8px 12px; border-radius: var(--radius-sm); font-size: 14px; outline: none; }
|
||||
select:focus { border-color: var(--accent); }
|
||||
.block-reorder { display: flex; flex-direction: column; gap: 2px; }
|
||||
.block-icon { width: 34px; height: 34px; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); display: grid; place-items: center; flex-shrink: 0; }
|
||||
.block-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.block-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; cursor: pointer; }
|
||||
.block-type-tag { text-transform: uppercase; font-size: 10px; letter-spacing: 0.05em; }
|
||||
.sm-input { width: 180px; padding: 6px 10px; font-size: 12px; }
|
||||
.downloads-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; margin-top: 6px; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Sparkles, Trophy, Plus, Pencil, Trash2, Award, Shield, Crown } from '@lucide/svelte';
|
||||
import Modal from '../components/Modal.svelte';
|
||||
import { get, post, put, del } from '../lib/api';
|
||||
@@ -26,11 +27,18 @@
|
||||
loading = true;
|
||||
try {
|
||||
const [r, lb] = await Promise.all([
|
||||
get<LevelReward[]>('/api/v1/levels/rewards'),
|
||||
get<any[]>('/api/v1/levels/rewards'),
|
||||
get<UserLevelInfo[]>('/api/v1/levels/leaderboard?limit=10'),
|
||||
]);
|
||||
rewards = r.sort((a, b) => a.level - b.level);
|
||||
leaderboard = lb;
|
||||
rewards = (r || []).map((x: any) => ({
|
||||
id: x.id,
|
||||
level: Number(x.level ?? x.level_req ?? 1),
|
||||
reward_type: x.reward_type || 'title',
|
||||
reward_value: x.reward_value ?? x.reward_name ?? '',
|
||||
description: x.description ?? '',
|
||||
icon: x.icon ?? '🎁',
|
||||
})).sort((a, b) => a.level - b.level);
|
||||
leaderboard = lb || [];
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
@@ -38,7 +46,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
Download, Server, Users, Trophy, Sparkles, HelpCircle, Newspaper,
|
||||
Copy, Check, ExternalLink, ChevronDown, ChevronUp, Swords, Flame,
|
||||
Pickaxe, Clock, Box, Shield, Zap, ArrowRight, LogIn, Crown,
|
||||
Pickaxe, Clock, Box, Shield, Zap, ArrowRight, LogIn, Crown, MessageSquare,
|
||||
} from '@lucide/svelte';
|
||||
import { get, formatBytes } from '../lib/api';
|
||||
import { session } from '../lib/session.svelte';
|
||||
@@ -171,18 +171,25 @@
|
||||
<div class="container hero-content">
|
||||
<div class="hero-badge">
|
||||
<span class="hero-badge-dot"></span>
|
||||
<span>{totalOnline} Players Online Now</span>
|
||||
<span>{block.options?.badge_text || `${totalOnline} Players Online Now`}</span>
|
||||
</div>
|
||||
|
||||
<h1 class="hero-title">{config.hero_title}</h1>
|
||||
<p class="hero-sub">{config.hero_subtitle}</p>
|
||||
<h1 class="hero-title">{block.options?.headline || config.hero_title}</h1>
|
||||
<p class="hero-sub">{block.options?.subtitle || config.hero_subtitle}</p>
|
||||
|
||||
<div class="hero-cta-group">
|
||||
{#if primaryDownload}
|
||||
{#if block.options?.cta_url}
|
||||
<a href={block.options.cta_url} target="_blank" class="btn-hero-primary">
|
||||
<Download size={20} />
|
||||
<div class="cta-text-group">
|
||||
<span class="cta-main">{block.options?.cta_text || config.hero_cta_text}</span>
|
||||
</div>
|
||||
</a>
|
||||
{:else if primaryDownload}
|
||||
<a href={primaryDownload.file_url} class="btn-hero-primary" download>
|
||||
<Download size={20} />
|
||||
<div class="cta-text-group">
|
||||
<span class="cta-main">{config.hero_cta_text}</span>
|
||||
<span class="cta-main">{block.options?.cta_text || config.hero_cta_text}</span>
|
||||
<span class="cta-sub">for {primaryDownload.label} ({formatBytes(primaryDownload.size)})</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -190,18 +197,18 @@
|
||||
<a href={config.external_download_url} target="_blank" class="btn-hero-primary">
|
||||
<Download size={20} />
|
||||
<div class="cta-text-group">
|
||||
<span class="cta-main">{config.hero_cta_text}</span>
|
||||
<span class="cta-main">{block.options?.cta_text || config.hero_cta_text}</span>
|
||||
<span class="cta-sub">Download Latest Release</span>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<button type="button" class="btn-hero-primary" onclick={() => scrollToSection('download')}>
|
||||
<Download size={20} />
|
||||
<span class="cta-main">{config.hero_cta_text}</span>
|
||||
<span class="cta-main">{block.options?.cta_text || config.hero_cta_text}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if config.server_ip}
|
||||
{#if block.options?.show_ip_copy !== false && config.server_ip}
|
||||
<button class="btn-hero-secondary" onclick={() => copyIp(config?.server_ip ?? '')}>
|
||||
{#if copied}<Check size={18} /> Copied Server IP{:else}<Copy size={18} /> Copy Server IP{/if}
|
||||
</button>
|
||||
@@ -217,9 +224,12 @@
|
||||
<div class="section-head">
|
||||
<h2>{block.title || 'Get the Launcher'}</h2>
|
||||
<p class="muted">{block.subtitle || 'Custom modpack synchronisation, fast Java auto-updates, and HD skin wardrobe.'}</p>
|
||||
{#if block.options?.badge_note}
|
||||
<div class="badge-note-pill">{block.options.badge_note}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="downloads-cards-grid">
|
||||
<div class="downloads-cards-grid" class:compact-list={block.options?.layout === 'compact'}>
|
||||
{#each config.hosted_downloads as d}
|
||||
<div class="download-card" class:recommended={d.platform === detectedOS}>
|
||||
{#if d.platform === detectedOS}
|
||||
@@ -260,16 +270,16 @@
|
||||
<p class="muted">{block.subtitle || 'Connect to our high-performance Minecraft worlds.'}</p>
|
||||
</div>
|
||||
|
||||
{#if publicServers.length}
|
||||
{#if publicServers.filter((s) => (block.options?.show_offline === false ? s.online : true)).length}
|
||||
<div class="servers-grid">
|
||||
{#each publicServers as s}
|
||||
{#each publicServers.filter((s) => (block.options?.show_offline === false ? s.online : true)) as s}
|
||||
<div class="server-card">
|
||||
<div class="server-card-top">
|
||||
<div class="server-status-pill" class:online={s.online}>
|
||||
<span class="dot"></span>
|
||||
<span>{s.online ? `${s.players_online} / ${s.players_max} Online` : 'Offline'}</span>
|
||||
</div>
|
||||
{#if s.mc_version}
|
||||
{#if block.options?.show_badges !== false && s.mc_version}
|
||||
<span class="mc-ver-tag tiny">{s.mc_version}</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -277,7 +287,7 @@
|
||||
<h3 class="server-card-title">{s.name}</h3>
|
||||
<div class="server-card-bottom">
|
||||
<button class="server-copy-btn" onclick={() => copyIp(config?.server_ip ?? '')}>
|
||||
<span>{config?.server_ip || 'play.server.com'}</span>
|
||||
<span>{block.options?.cta_label || config?.server_ip || 'play.server.com'}</span>
|
||||
<Copy size={13} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -442,22 +452,34 @@
|
||||
</div>
|
||||
|
||||
<div class="stats-counter-grid">
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{formatNumber(totalPlayers)}</span>
|
||||
<span class="counter-label">Total Players</span>
|
||||
</div>
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{formatTime(totalPlaytime)}</span>
|
||||
<span class="counter-label">Time Explored</span>
|
||||
</div>
|
||||
{#if block.options?.show_players !== false}
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{formatNumber(totalPlayers)}</span>
|
||||
<span class="counter-label">Total Players</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if block.options?.show_playtime !== false}
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{formatTime(totalPlaytime)}</span>
|
||||
<span class="counter-label">Time Explored</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if block.options?.show_guilds !== false && publicGuilds.length > 0}
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{publicGuilds.length}</span>
|
||||
<span class="counter-label">Active Guilds</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{publicServers.length}</span>
|
||||
<span class="counter-label">Connected Servers</span>
|
||||
</div>
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{instances.length}</span>
|
||||
<span class="counter-label">Modpack Instances</span>
|
||||
</div>
|
||||
{#if block.options?.custom_stat1_label && block.options?.custom_stat1_value}
|
||||
<div class="counter-card">
|
||||
<span class="counter-num">{block.options.custom_stat1_value}</span>
|
||||
<span class="counter-label">{block.options.custom_stat1_label}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -542,6 +564,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{:else if block.type === 'socials'}
|
||||
<!-- Socials Block -->
|
||||
<section class="section" id="socials">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<h2>{block.title || 'Join Our Community'}</h2>
|
||||
<p class="muted">{block.subtitle || 'Connect with players, join our Discord, and follow news.'}</p>
|
||||
</div>
|
||||
|
||||
<div class="socials-buttons-grid">
|
||||
{#if block.options?.discord_url}
|
||||
<a href={block.options.discord_url} target="_blank" rel="noreferrer" class="social-btn discord">
|
||||
<MessageSquare size={18} /> Join our Discord
|
||||
</a>
|
||||
{/if}
|
||||
{#if block.options?.store_url}
|
||||
<a href={block.options.store_url} target="_blank" rel="noreferrer" class="social-btn store">
|
||||
<Sparkles size={18} /> Visit Webstore
|
||||
</a>
|
||||
{/if}
|
||||
{#if block.options?.media_url}
|
||||
<a href={block.options.media_url} target="_blank" rel="noreferrer" class="social-btn media">
|
||||
<Flame size={18} /> YouTube / Twitch
|
||||
</a>
|
||||
{/if}
|
||||
{#if block.options?.social_url}
|
||||
<a href={block.options.social_url} target="_blank" rel="noreferrer" class="social-btn twitter">
|
||||
<ExternalLink size={18} /> Follow on Twitter / X
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -670,6 +726,15 @@
|
||||
.footer { border-top: 1px solid rgba(255, 255, 255, 0.08); padding: 40px 0; margin-top: auto; }
|
||||
.footer-content { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
|
||||
.empty-state-box { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 40px; }
|
||||
.badge-note-pill { display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 99rem; background: rgba(109, 106, 245, 0.15); color: #a5a3ff; font-size: 13px; font-weight: 600; margin-top: 10px; }
|
||||
.downloads-cards-grid.compact-list { grid-template-columns: 1fr; }
|
||||
.socials-buttons-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 24px; }
|
||||
.social-btn { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 16px 20px; border-radius: 12px; font-weight: 600; text-decoration: none; transition: transform 0.15s, opacity 0.15s; color: #fff; font-size: 15px; }
|
||||
.social-btn:hover { transform: translateY(-2px); opacity: 0.95; }
|
||||
.social-btn.discord { background: #5865F2; }
|
||||
.social-btn.store { background: #8b5cf6; }
|
||||
.social-btn.media { background: #ef4444; }
|
||||
.social-btn.twitter { background: #0284c7; }
|
||||
@media (max-width: 768px) {
|
||||
.nav-links { display: none; }
|
||||
.hero-title { font-size: 34px; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Search, Plus, Pencil, Trash2, CheckCircle2, XCircle, Award, Sparkles, Filter } from '@lucide/svelte';
|
||||
import Modal from '../components/Modal.svelte';
|
||||
import { get, post, put, del } from '../lib/api';
|
||||
@@ -33,7 +34,19 @@
|
||||
async function loadQuests() {
|
||||
loading = true;
|
||||
try {
|
||||
quests = await get<Quest[]>('/api/admin/quests');
|
||||
const res = await get<any[]>('/api/admin/quests');
|
||||
quests = (res || []).map((q: any) => ({
|
||||
id: q.id || '',
|
||||
title: q.title || '',
|
||||
description: q.description || '',
|
||||
quest_type: q.quest_type || q.period || 'daily',
|
||||
category: q.category || 'mining',
|
||||
xp_reward: q.xp_reward || 0,
|
||||
stat_type: q.stat_type || q.target_stat || 'blocks_broken',
|
||||
target_count: q.target_count || 1,
|
||||
icon: q.icon || '⛏️',
|
||||
active: q.active !== undefined ? q.active : (q.enabled !== undefined ? q.enabled : true),
|
||||
}));
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
@@ -41,7 +54,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
loadQuests();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user