|
|
@@ -0,0 +1,518 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8" />
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
+ <title>Room Price Calculator</title>
|
|
|
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
|
+ <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;600;800&display=swap" rel="stylesheet" />
|
|
|
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
|
+ <style>
|
|
|
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+
|
|
|
+ :root {
|
|
|
+ --bg: #0e0f11;
|
|
|
+ --surface: #16181c;
|
|
|
+ --border: #2a2d35;
|
|
|
+ --accent: #c8f060;
|
|
|
+ --accent2: #60c8f0;
|
|
|
+ --accent3: #c060f0;
|
|
|
+ --text: #e8eaf0;
|
|
|
+ --muted: #6b7080;
|
|
|
+ --danger: #f06080;
|
|
|
+ --radius: 12px;
|
|
|
+ --mono: 'DM Mono', monospace;
|
|
|
+ --display: 'Syne', sans-serif;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ background: var(--bg);
|
|
|
+ color: var(--text);
|
|
|
+ font-family: var(--mono);
|
|
|
+ min-height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 48px 16px 80px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Noise overlay */
|
|
|
+ body::before {
|
|
|
+ content: '';
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 0;
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+
|
|
|
+ #app { position: relative; z-index: 1; width: 100%; max-width: 1080px; }
|
|
|
+
|
|
|
+ header {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ }
|
|
|
+ .service-bar {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 1080px;
|
|
|
+ margin: 0 auto 28px;
|
|
|
+ background: var(--surface);
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: var(--radius);
|
|
|
+ padding: 16px 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+ .service-bar label { flex-shrink: 0; }
|
|
|
+ .service-bar select { max-width: 360px; }
|
|
|
+ .header-tag {
|
|
|
+ font-family: var(--mono);
|
|
|
+ font-size: 11px;
|
|
|
+ letter-spacing: 0.2em;
|
|
|
+ color: var(--accent);
|
|
|
+ text-transform: uppercase;
|
|
|
+ border: 1px solid var(--accent);
|
|
|
+ padding: 3px 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ opacity: 0.85;
|
|
|
+ }
|
|
|
+ h1 {
|
|
|
+ font-family: var(--display);
|
|
|
+ font-weight: 800;
|
|
|
+ font-size: clamp(2rem, 6vw, 3.2rem);
|
|
|
+ line-height: 1.05;
|
|
|
+ letter-spacing: -0.02em;
|
|
|
+ }
|
|
|
+ h1 span { color: var(--accent); }
|
|
|
+ .subtitle {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+
|
|
|
+ .grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 20px;
|
|
|
+ }
|
|
|
+ @media (max-width: 880px) { .grid { grid-template-columns: 1fr 1fr; } }
|
|
|
+ @media (max-width: 560px) { .grid { grid-template-columns: 1fr; } }
|
|
|
+
|
|
|
+ .card {
|
|
|
+ background: var(--surface);
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: var(--radius);
|
|
|
+ padding: 28px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+ }
|
|
|
+ .card:hover { border-color: #3a3d48; }
|
|
|
+
|
|
|
+ .card-accent-bar {
|
|
|
+ position: absolute;
|
|
|
+ top: 0; left: 0; right: 0;
|
|
|
+ height: 2px;
|
|
|
+ }
|
|
|
+ .card.green .card-accent-bar { background: var(--accent); }
|
|
|
+ .card.blue .card-accent-bar { background: var(--accent2); }
|
|
|
+ .card.purple .card-accent-bar { background: var(--accent3); }
|
|
|
+
|
|
|
+ .card-title {
|
|
|
+ font-family: var(--display);
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 1.05rem;
|
|
|
+ letter-spacing: -0.01em;
|
|
|
+ }
|
|
|
+ .card.green .card-title { color: var(--accent); }
|
|
|
+ .card.blue .card-title { color: var(--accent2); }
|
|
|
+ .card.purple .card-title { color: var(--accent3); }
|
|
|
+
|
|
|
+ .card-direction {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .field { display: flex; flex-direction: column; gap: 6px; }
|
|
|
+ label { font-size: 11px; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; }
|
|
|
+
|
|
|
+ input, select {
|
|
|
+ background: #1e2028;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ color: var(--text);
|
|
|
+ font-family: var(--mono);
|
|
|
+ font-size: 14px;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ outline: none;
|
|
|
+ transition: border-color 0.15s, box-shadow 0.15s;
|
|
|
+ width: 100%;
|
|
|
+ appearance: none;
|
|
|
+ }
|
|
|
+ input:focus, select:focus {
|
|
|
+ border-color: #50546a;
|
|
|
+ box-shadow: 0 0 0 3px rgba(100,110,160,0.12);
|
|
|
+ }
|
|
|
+ input[type="number"]::-webkit-inner-spin-button { opacity: 0.4; }
|
|
|
+
|
|
|
+ select {
|
|
|
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7080' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-position: right 12px center;
|
|
|
+ padding-right: 36px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ button {
|
|
|
+ font-family: var(--display);
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 13px;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ padding: 11px 20px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: opacity 0.15s, transform 0.1s;
|
|
|
+ color: #0e0f11;
|
|
|
+ }
|
|
|
+ button:active { transform: scale(0.97); }
|
|
|
+ button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
|
+
|
|
|
+ .btn-green { background: var(--accent); }
|
|
|
+ .btn-blue { background: var(--accent2); }
|
|
|
+ .btn-purple { background: var(--accent3); }
|
|
|
+
|
|
|
+ .result-box {
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 14px 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ min-height: 62px;
|
|
|
+ transition: all 0.3s;
|
|
|
+ }
|
|
|
+ .result-box.idle {
|
|
|
+ background: #1a1c22;
|
|
|
+ border: 1px dashed #2a2d35;
|
|
|
+ color: var(--muted);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ .result-box.success.green { background: #161d12; border: 1px solid #3a5020; }
|
|
|
+ .result-box.success.blue { background: #121a1d; border: 1px solid #20454a; }
|
|
|
+ .result-box.success.purple { background: #1a121d; border: 1px solid #45204a; }
|
|
|
+ .result-box.success .result-label {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ letter-spacing: 0.08em;
|
|
|
+ text-transform: uppercase;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+ .result-box.success .result-value {
|
|
|
+ font-family: var(--display);
|
|
|
+ font-size: 1.6rem;
|
|
|
+ font-weight: 800;
|
|
|
+ letter-spacing: -0.02em;
|
|
|
+ }
|
|
|
+ .result-box.success.green .result-value { color: var(--accent); }
|
|
|
+ .result-box.success.blue .result-value { color: var(--accent2); }
|
|
|
+ .result-box.success.purple .result-value { color: var(--accent3); }
|
|
|
+ .result-box.success .result-meta {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+ .result-extra {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid #22252d;
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+ .result-extra-label {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ letter-spacing: 0.08em;
|
|
|
+ text-transform: uppercase;
|
|
|
+ }
|
|
|
+ .result-extra-value {
|
|
|
+ font-family: var(--display);
|
|
|
+ font-size: 1.05rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--text);
|
|
|
+ }
|
|
|
+ .result-box.error {
|
|
|
+ background: #1d1216;
|
|
|
+ border: 1px solid #4a2030;
|
|
|
+ color: var(--danger);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .spinner {
|
|
|
+ display: inline-block;
|
|
|
+ width: 14px; height: 14px;
|
|
|
+ border: 2px solid currentColor;
|
|
|
+ border-top-color: transparent;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 0.7s linear infinite;
|
|
|
+ margin-right: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ @keyframes spin { to { transform: rotate(360deg); } }
|
|
|
+
|
|
|
+ .details-toggle {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid #22252d;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--muted);
|
|
|
+ letter-spacing: 0.08em;
|
|
|
+ text-transform: uppercase;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ .details-toggle:hover { color: var(--text); }
|
|
|
+ .details-table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ margin-top: 14px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ .details-table tr { border-top: 1px solid #22252d; }
|
|
|
+ .details-table tr:first-child { border-top: 1px solid #2e3240; }
|
|
|
+ .dt-key {
|
|
|
+ color: var(--muted);
|
|
|
+ padding: 5px 0;
|
|
|
+ width: 50%;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ }
|
|
|
+ .dt-val {
|
|
|
+ color: var(--text);
|
|
|
+ padding: 5px 0;
|
|
|
+ text-align: right;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ .result-box.success.blue .dt-val { color: #b8e8f8; }
|
|
|
+ .result-box.success.purple .dt-val { color: #e8b8f8; }
|
|
|
+
|
|
|
+ .result-box.placeholder { opacity: 0.45; }
|
|
|
+ .result-box.placeholder .result-value { font-style: italic; }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+<div id="app">
|
|
|
+ <header>
|
|
|
+ <h1>Room Price<br/><span>Calculator</span></h1>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="service-bar">
|
|
|
+ <label>Service</label>
|
|
|
+ <select v-model="selectedService">
|
|
|
+ <option v-if="services.length === 0" disabled value="">Loading…</option>
|
|
|
+ <option v-for="svc in services" :key="svc.key" :value="svc.key">{{ svc.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="grid">
|
|
|
+ <div v-for="conv in conversions" :key="conv.endpoint" class="card" :class="conv.color">
|
|
|
+ <div class="card-accent-bar"></div>
|
|
|
+ <div>
|
|
|
+ <div class="card-title">{{ conv.endpoint }}</div>
|
|
|
+ <div class="card-direction">{{ labelFor(conv.from) }} → {{ labelFor(conv.to) }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field">
|
|
|
+ <label>Amount</label>
|
|
|
+ <input type="number" v-model.number="states[conv.endpoint].amount" placeholder="e.g. 68.00" step="1.00"
|
|
|
+ @blur="onAmountBlur(conv.endpoint)" @keyup.enter="onAmountBlur(conv.endpoint)" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button :class="`btn-${conv.color}`" @click="call(conv.endpoint)" :disabled="states[conv.endpoint].loading">
|
|
|
+ <span v-if="states[conv.endpoint].loading"><span class="spinner"></span>Calling…</span>
|
|
|
+ <span v-else>→ Convert</span>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="result-box"
|
|
|
+ :class="[resultClass(states[conv.endpoint], conv.color), { placeholder: states[conv.endpoint].isPlaceholder }]">
|
|
|
+ <template v-if="states[conv.endpoint].loading">
|
|
|
+ <span class="spinner"></span> Fetching result…
|
|
|
+ </template>
|
|
|
+ <template v-else-if="states[conv.endpoint].error">{{ states[conv.endpoint].error }}</template>
|
|
|
+ <template v-else-if="states[conv.endpoint].result !== null">
|
|
|
+ <div class="result-label">{{ labelFor(conv.to) }}</div>
|
|
|
+ <div class="result-value">${{ states[conv.endpoint].result }}</div>
|
|
|
+ <div class="result-meta">Service: {{ states[conv.endpoint].displayServiceName }}</div>
|
|
|
+ <div v-for="extra in extrasFor(conv, states[conv.endpoint])" :key="extra.key" class="result-extra">
|
|
|
+ <span class="result-extra-label">{{ extra.label }}</span>
|
|
|
+ <span class="result-extra-value">${{ extra.value }}</span>
|
|
|
+ </div>
|
|
|
+ <template v-if="states[conv.endpoint].details">
|
|
|
+ <div
|
|
|
+ class="details-toggle"
|
|
|
+ @click="states[conv.endpoint].showDetails = !states[conv.endpoint].showDetails"
|
|
|
+ >
|
|
|
+ {{ states[conv.endpoint].showDetails ? '▾ Hide details' : '▸ Show details' }}
|
|
|
+ </div>
|
|
|
+ <table v-if="states[conv.endpoint].showDetails" class="details-table">
|
|
|
+ <tr v-for="(val, key) in states[conv.endpoint].details" :key="key">
|
|
|
+ <td class="dt-key">{{ key }}</td>
|
|
|
+ <td class="dt-val">${{ val }}</td>
|
|
|
+ </tr>
|
|
|
+ </table>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ <template v-else>Result will appear here</template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+const { createApp, reactive, ref, watch, onMounted } = Vue;
|
|
|
+
|
|
|
+const CONVERSIONS = [
|
|
|
+ { endpoint: 'net_to_list', from: 'net', to: 'listed', color: 'green' },
|
|
|
+ { endpoint: 'list_to_net', from: 'listed', to: 'net', color: 'purple' },
|
|
|
+ { endpoint: 'list_to_retail', from: 'listed', to: 'retail', color: 'blue' },
|
|
|
+ { endpoint: 'retail_to_list', from: 'retail', to: 'listed', color: 'green' },
|
|
|
+ { endpoint: 'net_to_retail', from: 'net', to: 'retail', color: 'blue' },
|
|
|
+ { endpoint: 'retail_to_net', from: 'retail', to: 'net', color: 'purple' },
|
|
|
+];
|
|
|
+
|
|
|
+const FIELD_LABELS = { listed: 'List', retail: 'Retail', net: 'Net' };
|
|
|
+
|
|
|
+function labelFor(key) { return FIELD_LABELS[key] || key; }
|
|
|
+
|
|
|
+createApp({
|
|
|
+ setup() {
|
|
|
+ const API_BASE = 'https://zi3.xyz/room'; // mirrors URL_PREFIX on the backend
|
|
|
+ const apiBase = () => API_BASE;
|
|
|
+
|
|
|
+ const services = ref([]);
|
|
|
+ const selectedService = ref('');
|
|
|
+
|
|
|
+ const makeState = () => reactive({
|
|
|
+ amount: null,
|
|
|
+ displayServiceName: null,
|
|
|
+ result: '0.00',
|
|
|
+ values: {},
|
|
|
+ details: null,
|
|
|
+ error: null,
|
|
|
+ loading: false,
|
|
|
+ isPlaceholder: false,
|
|
|
+ showDetails: false,
|
|
|
+ });
|
|
|
+
|
|
|
+ const states = reactive({});
|
|
|
+ for (const conv of CONVERSIONS) states[conv.endpoint] = makeState();
|
|
|
+
|
|
|
+ async function fetchServices() {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${apiBase()}/services`);
|
|
|
+ const list = await res.json();
|
|
|
+ if (Array.isArray(list) && list.length > 0) {
|
|
|
+ services.value = list;
|
|
|
+ selectedService.value = list[0].key;
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load services:', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function call(endpoint) {
|
|
|
+ const s = states[endpoint];
|
|
|
+ const conv = CONVERSIONS.find(c => c.endpoint === endpoint);
|
|
|
+ if (s.amount === null || s.amount === '' || s.amount === 0) {
|
|
|
+ s.error = 'Please enter a non-zero amount.';
|
|
|
+ s.result = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const serviceKey = selectedService.value;
|
|
|
+ const pendingServiceName = (services.value.find(sv => sv.key === serviceKey) || {}).name || serviceKey;
|
|
|
+ s.loading = true;
|
|
|
+ s.error = null;
|
|
|
+ s.result = null;
|
|
|
+ s.values = { listed: null, retail: null, net: null };
|
|
|
+ s.details = null;
|
|
|
+ try {
|
|
|
+ const url = new URL(`${apiBase()}/${endpoint}`);
|
|
|
+ url.searchParams.set('amount', s.amount);
|
|
|
+ url.searchParams.set('service', serviceKey);
|
|
|
+ const res = await fetch(url.toString());
|
|
|
+ const json = await res.json();
|
|
|
+ if (!res.ok || json.error) throw new Error(json.error || `HTTP ${res.status}`);
|
|
|
+ s.result = json[conv.to] ?? null;
|
|
|
+ s.values = {
|
|
|
+ listed: json.listed ?? null,
|
|
|
+ retail: json.retail ?? null,
|
|
|
+ net: json.net ?? null,
|
|
|
+ };
|
|
|
+ s.details = json.details ?? null;
|
|
|
+ s.displayServiceName = pendingServiceName;
|
|
|
+ s.isPlaceholder = false;
|
|
|
+ } catch (err) {
|
|
|
+ s.error = `Error: ${err.message}`;
|
|
|
+ } finally {
|
|
|
+ s.loading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onAmountBlur(endpoint) {
|
|
|
+ const s = states[endpoint];
|
|
|
+ if (s.amount !== null && s.amount !== '' && s.amount !== 0) call(endpoint);
|
|
|
+ }
|
|
|
+
|
|
|
+ function resultClass(s, color) {
|
|
|
+ if (s.loading) return 'idle';
|
|
|
+ if (s.error) return 'error';
|
|
|
+ if (s.result !== null) return `success ${color}`;
|
|
|
+ return 'idle';
|
|
|
+ }
|
|
|
+
|
|
|
+ function extrasFor(conv, s) {
|
|
|
+ if (!s.values) return [];
|
|
|
+ const extras = [];
|
|
|
+ for (const k of ['listed', 'retail', 'net']) {
|
|
|
+ if (k === conv.to) continue;
|
|
|
+ if (s.values[k] != null) extras.push({ key: k, label: labelFor(k), value: s.values[k] });
|
|
|
+ }
|
|
|
+ return extras;
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(fetchServices);
|
|
|
+
|
|
|
+ // When the shared service changes, re-run any card that already has an amount.
|
|
|
+ watch(selectedService, () => {
|
|
|
+ for (const conv of CONVERSIONS) {
|
|
|
+ const s = states[conv.endpoint];
|
|
|
+ if (s.amount !== null && s.amount !== '' && s.amount !== 0) call(conv.endpoint);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ services,
|
|
|
+ selectedService,
|
|
|
+ states,
|
|
|
+ conversions: CONVERSIONS,
|
|
|
+ call,
|
|
|
+ resultClass,
|
|
|
+ onAmountBlur,
|
|
|
+ labelFor,
|
|
|
+ extrasFor,
|
|
|
+ };
|
|
|
+ }
|
|
|
+}).mount('#app');
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|