All checks were successful
Test Asgard Runner / test (push) Successful in 3s
18 admin views re-skinned onto design-system components + tokens: Server/Players/Plugins/ChatLog, Wipes/WipeProfiles/Maps/Schedules/Alerts, StoreConfig/StoreItems/ModuleStore, Analytics/WipeAnalytics/MapAnalytics/PlayerRetention/StoreRevenue. ECharts now read var(--accent) (token-driven, follows game skin). 14 icons added to the registry. All logic/store/router/handlers/API calls preserved; presentation-only re-skin. Build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
288 lines
9.4 KiB
Vue
288 lines
9.4 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useApi } from '@/composables/useApi'
|
|
import { useToastStore } from '@/stores/toast'
|
|
import type { StoreConfig } from '@/types'
|
|
import Panel from '@/components/ds/data/Panel.vue'
|
|
import Button from '@/components/ds/core/Button.vue'
|
|
import Alert from '@/components/ds/feedback/Alert.vue'
|
|
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
|
|
import Input from '@/components/ds/forms/Input.vue'
|
|
import Select from '@/components/ds/forms/Select.vue'
|
|
import Switch from '@/components/ds/forms/Switch.vue'
|
|
|
|
const api = useApi()
|
|
const toast = useToastStore()
|
|
|
|
const config = ref<StoreConfig>({
|
|
store_name: '',
|
|
description: null,
|
|
currency: 'USD',
|
|
paypal_client_id: null,
|
|
sandbox_mode: true,
|
|
enabled: false,
|
|
})
|
|
|
|
const paypalSecret = ref<string>('')
|
|
const isLoading = ref(false)
|
|
const saving = ref(false)
|
|
const isConfigured = ref(false)
|
|
|
|
// DS Input uses defineModel<string>() which emits string | undefined, but
|
|
// paypal_client_id is string | null in the DB type. Bridge null ↔ undefined
|
|
// so we never change what gets saved to the API.
|
|
const paypalClientId = computed({
|
|
get: () => config.value.paypal_client_id ?? undefined,
|
|
set: (v: string | undefined) => { config.value.paypal_client_id = v ?? null },
|
|
})
|
|
|
|
const currencyOptions = [
|
|
{ value: 'USD', label: 'USD — US Dollar' },
|
|
{ value: 'EUR', label: 'EUR — Euro' },
|
|
{ value: 'GBP', label: 'GBP — British Pound' },
|
|
]
|
|
|
|
const showPayPalWarning = computed(() => {
|
|
return config.value.enabled && (!config.value.paypal_client_id || !paypalSecret.value)
|
|
})
|
|
|
|
async function fetchConfig() {
|
|
isLoading.value = true
|
|
try {
|
|
const data = await api.get<{ config: StoreConfig }>('/webstore/config')
|
|
config.value = data.config
|
|
isConfigured.value = !!data.config.store_name
|
|
} catch (err: any) {
|
|
if (err.response?.status === 404) {
|
|
isConfigured.value = false
|
|
} else {
|
|
toast.error('Failed to load store configuration')
|
|
}
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
if (!config.value.store_name.trim()) {
|
|
toast.error('Store name is required')
|
|
return
|
|
}
|
|
|
|
if (config.value.enabled && !config.value.paypal_client_id) {
|
|
toast.error('PayPal Client ID is required to enable the store')
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
try {
|
|
const payload: any = {
|
|
store_name: config.value.store_name,
|
|
description: config.value.description,
|
|
currency: config.value.currency,
|
|
paypal_client_id: config.value.paypal_client_id,
|
|
sandbox_mode: config.value.sandbox_mode,
|
|
enabled: config.value.enabled,
|
|
}
|
|
|
|
if (paypalSecret.value.trim()) {
|
|
payload.paypal_client_secret = paypalSecret.value
|
|
}
|
|
|
|
await api.put('/webstore/config', payload)
|
|
toast.success('Store configuration saved successfully')
|
|
isConfigured.value = true
|
|
paypalSecret.value = ''
|
|
} catch (err: any) {
|
|
const message = err.response?.data?.error || 'Failed to save configuration'
|
|
toast.error(message)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchConfig()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="sc-page">
|
|
<!-- Page head -->
|
|
<div class="page__head">
|
|
<div>
|
|
<div class="t-eyebrow">Management</div>
|
|
<h1 class="page__title">Store configuration</h1>
|
|
<p class="page__sub">Configure your integrated webstore and PayPal settings.</p>
|
|
</div>
|
|
<Button :loading="saving" :disabled="isLoading" icon="save" @click="saveConfig">
|
|
Save configuration
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Loading skeleton -->
|
|
<div v-if="isLoading" class="sc-loading">
|
|
<EmptyState icon="loader" title="Loading configuration" description="Fetching your store settings…" />
|
|
</div>
|
|
|
|
<!-- Empty state — no config yet -->
|
|
<Panel v-else-if="!isConfigured && !config.store_name">
|
|
<EmptyState
|
|
icon="shopping-cart"
|
|
title="No store configured"
|
|
description="Set up your integrated webstore to start selling in-game items, ranks, and currency to your players. Fill out the form below to get started."
|
|
>
|
|
<template #action>
|
|
<Button size="sm" variant="outline" icon="arrow-down">Complete the form below</Button>
|
|
</template>
|
|
</EmptyState>
|
|
</Panel>
|
|
|
|
<!-- Configuration form -->
|
|
<div v-else class="sc-form">
|
|
<!-- Enable toggle -->
|
|
<Panel title="Webstore status" subtitle="Allow players to purchase items from your store">
|
|
<div class="sc-toggle-row">
|
|
<div class="sc-toggle-row__info">
|
|
<span class="sc-toggle-row__label">Enable webstore</span>
|
|
<span class="sc-toggle-row__hint">Players will be able to browse and purchase items when enabled.</span>
|
|
</div>
|
|
<Switch v-model="config.enabled" />
|
|
</div>
|
|
|
|
<Alert
|
|
v-if="showPayPalWarning"
|
|
tone="warn"
|
|
title="PayPal configuration required"
|
|
class="sc-alert"
|
|
>
|
|
Configure PayPal credentials in the section below before the store can process transactions.
|
|
</Alert>
|
|
</Panel>
|
|
|
|
<!-- Store information -->
|
|
<Panel title="Store information" eyebrow="General">
|
|
<div class="sc-fields">
|
|
<Input
|
|
v-model="config.store_name"
|
|
label="Store name"
|
|
placeholder="My Rust Server Store"
|
|
:required="true"
|
|
hint="Displayed to players on the store page"
|
|
/>
|
|
|
|
<label class="cc-field">
|
|
<span class="cc-field__label">Description <span class="sc-opt">(optional)</span></span>
|
|
<textarea
|
|
v-model="config.description"
|
|
class="cc-textarea"
|
|
rows="3"
|
|
placeholder="Welcome to our server store! Support us and get awesome in-game items…"
|
|
/>
|
|
<span class="cc-field__hint">Brief description shown on the store page.</span>
|
|
</label>
|
|
|
|
<Select
|
|
v-model="config.currency"
|
|
label="Currency"
|
|
:options="currencyOptions"
|
|
hint="Currency used for all transactions"
|
|
/>
|
|
</div>
|
|
</Panel>
|
|
|
|
<!-- PayPal configuration -->
|
|
<Panel title="PayPal configuration" eyebrow="Payments" subtitle="Get your API credentials from the PayPal Developer Dashboard.">
|
|
<div class="sc-fields">
|
|
<Input
|
|
v-model="paypalClientId"
|
|
label="PayPal Client ID"
|
|
placeholder="AXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
:required="true"
|
|
:mono="true"
|
|
/>
|
|
|
|
<Input
|
|
v-model="paypalSecret"
|
|
label="PayPal Client Secret"
|
|
type="password"
|
|
placeholder="Enter to update (stored encrypted)"
|
|
:mono="true"
|
|
hint="Stored encrypted. Leave blank to keep existing secret."
|
|
:required="true"
|
|
/>
|
|
|
|
<!-- Sandbox mode toggle -->
|
|
<div class="sc-sandbox">
|
|
<div class="sc-toggle-row">
|
|
<div class="sc-toggle-row__info">
|
|
<span class="sc-toggle-row__label">Sandbox mode</span>
|
|
<span class="sc-toggle-row__hint">Use PayPal sandbox for testing — no real transactions.</span>
|
|
</div>
|
|
<Switch v-model="config.sandbox_mode" />
|
|
</div>
|
|
</div>
|
|
|
|
<Alert
|
|
v-if="!config.sandbox_mode && config.enabled"
|
|
tone="danger"
|
|
title="Production mode active"
|
|
>
|
|
Real transactions will be processed. Ensure your PayPal credentials are correct.
|
|
</Alert>
|
|
</div>
|
|
</Panel>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.sc-page { max-width: 860px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; }
|
|
|
|
.page__head {
|
|
display: flex; align-items: flex-start; justify-content: space-between;
|
|
gap: 16px; flex-wrap: wrap; row-gap: 12px;
|
|
}
|
|
.page__title {
|
|
font-size: var(--text-3xl); font-weight: 700; letter-spacing: -0.02em;
|
|
color: var(--text-primary); margin-top: 5px;
|
|
}
|
|
.page__sub { font-size: var(--text-sm); color: var(--text-tertiary); margin-top: 3px; }
|
|
|
|
.sc-loading { padding: 20px 0; }
|
|
|
|
.sc-form { display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
.sc-fields { display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
/* Shared toggle row */
|
|
.sc-toggle-row {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
.sc-toggle-row__info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
.sc-toggle-row__label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
|
|
.sc-toggle-row__hint { font-size: var(--text-xs); color: var(--text-tertiary); }
|
|
|
|
.sc-sandbox {
|
|
background: var(--surface-raised); border-radius: var(--radius-md);
|
|
box-shadow: var(--ring-default); padding: 14px 16px;
|
|
}
|
|
|
|
.sc-alert { margin-top: 14px; }
|
|
|
|
.sc-opt { font-weight: 400; color: var(--text-muted); }
|
|
|
|
/* Textarea token style */
|
|
.cc-textarea {
|
|
width: 100%; min-height: 80px; padding: 9px 11px;
|
|
background: var(--surface-inset); border: none; border-radius: var(--radius-md);
|
|
box-shadow: var(--ring-default); resize: vertical;
|
|
font-family: var(--font-sans); font-size: var(--text-sm); color: var(--text-primary);
|
|
line-height: var(--leading-normal); outline: none;
|
|
transition: var(--transition-colors);
|
|
}
|
|
.cc-textarea::placeholder { color: var(--text-muted); }
|
|
.cc-textarea:focus { box-shadow: inset 0 0 0 1px var(--accent), var(--glow-accent-sm); }
|
|
</style>
|