Skip to content

Commit 7c7ab56

Browse files
committed
Better order create page
1 parent e369369 commit 7c7ab56

File tree

3 files changed

+165
-168
lines changed

3 files changed

+165
-168
lines changed

app/Controllers/OrdersController.php

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,41 @@ public function createOrder(Request $request, Response $response): Response
7676
$db = $this->container->get('db');
7777
$providers = $db->select("SELECT id, name, type, api_endpoint, credentials, pricing FROM providers WHERE status = 'active'");
7878

79+
$domainProducts = [];
80+
$otherProducts = [];
81+
7982
foreach ($providers as &$provider) {
8083
$rawProducts = json_decode($provider['pricing'], true) ?? [];
84+
$credentials = json_decode($provider['credentials'], true) ?? [];
8185
$enrichedProducts = [];
8286

83-
foreach ($rawProducts as $tld => $actions) {
84-
$enrichedProducts[$tld] = $actions;
85-
$enrichedProducts[$tld]['type'] = 'domain';
86-
$enrichedProducts[$tld]['label'] = $tld;
87-
$enrichedProducts[$tld]['description'] = 'Domain services for ' . $tld;
88-
$enrichedProducts[$tld]['price'] = $actions['register']['1'] ?? 0;
89-
$enrichedProducts[$tld]['billing'] = 'year';
90-
$enrichedProducts[$tld]['fields'] = [];
87+
foreach ($rawProducts as $label => $actions) {
88+
$product = [
89+
'type' => $provider['type'], // domain, server, etc.
90+
'label' => $label,
91+
'description' => ucfirst($provider['type']) . ' service: ' . $label,
92+
'price' => $actions['register']['1'] ?? $actions['price'] ?? 0,
93+
'billing' => $actions['billing'] ?? 'year',
94+
'fields' => $credentials['required_fields'] ?? [],
95+
'actions' => $actions,
96+
];
97+
98+
$enrichedProducts[$label] = $product;
99+
100+
if ($product['type'] === 'domain') {
101+
$domainProducts[] = ['provider' => $provider, 'product' => $product];
102+
} else {
103+
$otherProducts[] = ['provider' => $provider, 'product' => $product];
104+
}
91105
}
92106

93107
$provider['products'] = $enrichedProducts;
94108
$provider['credentials'] = json_decode($provider['credentials'], true) ?? [];
95109
}
96110

97111
return $this->container->get('view')->render($response, 'admin/orders/create.twig', [
98-
'providers' => $providers,
99-
'user' => $_SESSION['auth_user_id'] ?? null,
112+
'domainProducts' => $domainProducts,
113+
'otherProducts' => $otherProducts,
100114
'currency' => $_SESSION['_currency'] ?? 'EUR'
101115
]);
102116
}
Lines changed: 139 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
{% extends "layouts/app.twig" %}
22

3-
{% block title %}{{ __('Create New Order') }}{% endblock %}
3+
{% block title %}{{ __('Create Order') }}{% endblock %}
44

55
{% block content %}
6-
<style>
7-
.card.cursor-pointer:hover {
8-
border-color: #206bc4;
9-
box-shadow: 0 0 0 2px rgba(32, 107, 196, 0.25);
10-
}
11-
</style>
126
<div class="page-wrapper">
137
<!-- Page header -->
148
<div class="page-header d-print-none">
@@ -24,12 +18,12 @@
2418
<a href="{{route('listOrders')}}">{{ __('Orders') }}</a>
2519
</li>
2620
<li class="breadcrumb-item active">
27-
{{ __('Create New Order') }}
21+
{{ __('Create Order') }}
2822
</li>
2923
</ol>
3024
</div>
3125
<h2 class="page-title">
32-
{{ __('Create New Order') }}
26+
{{ __('Create Order') }}
3327
</h2>
3428
</div>
3529
</div>
@@ -38,173 +32,162 @@
3832
<!-- Page body -->
3933
<div class="page-body">
4034
<div class="container-xl">
41-
<div class="col-12">
4235
{% include 'partials/flash.twig' %}
43-
<div class="card">
44-
<div class="card-body">
45-
<form method="post" action="{{route('createOrder')}}" id="createOrderForm">
46-
{{ csrf.field | raw }}
47-
48-
<div class="row">
49-
{% for provider in providers %}
50-
{% for code, product in provider.products %}
51-
{% if product.type == 'domain' %}
52-
<div class="col-md-4 mb-4">
53-
{% set safe_id = 'card-' ~ provider.id ~ '-' ~ code|replace({'.': '-', ':': '-'}) %}
54-
<label class="card h-100 cursor-pointer position-relative" id="{{ safe_id }}" onclick="selectProduct('{{ provider.id }}', '{{ code }}', '{{ safe_id }}')">
55-
<input type="radio" name="product_choice" class="form-check-input position-absolute top-0 end-0 m-3 z-index-3" value="{{ provider.id }}::{{ code }}" required>
56-
<div class="card-status-start bg-primary d-none"></div>
57-
<div class="card-body">
58-
<div class="d-flex align-items-center justify-content-between mb-2">
59-
<span class="badge bg-blue text-blue-fg">from {{ currency }} {{ product.register['1']|default(0) }}/{{ product.billing }}</span>
60-
</div>
61-
<h4 class="card-title mb-0">{{ product.label }}</h4>
62-
<p class="text-muted small mb-2">{{ provider.name }} — {{ product.description }}</p>
63-
<div class="text-end">
64-
<span class="badge bg-light text-light-fg">Click to configure</span>
65-
</div>
66-
</div>
67-
</label>
68-
</div>
69-
{% endif %}
70-
{% endfor %}
71-
{% endfor %}
72-
</div>
7336

74-
<div id="productFields" class="d-none">
75-
<h4>Order Details</h4>
76-
<div class="row g-3" id="dynamicFields">
77-
<!-- Dynamic domain/contact fields here -->
78-
</div>
79-
</div>
80-
</div>
81-
<div class="card-footer">
82-
<div class="row align-items-center">
83-
<div class="col-auto">
84-
<button type="submit" class="btn btn-primary">{{ __('Create Order') }}</button></form>
37+
{% if domainProducts|length > 0 %}
38+
<div class="card mb-4">
39+
<div class="card-body">
40+
<h3 class="card-title">{{ __('Find Your Perfect Domain') }}</h3>
41+
<p class="text-muted">{{ __('Start your online journey by searching for an available domain.') }}</p>
42+
<form class="row g-2 align-items-stretch" onsubmit="checkDomain(event)">
43+
<div class="col-md-9">
44+
<input type="text" id="domainInput" class="form-control form-control-lg rounded" placeholder="example.com" required>
45+
</div>
46+
<div class="col-md-3 d-grid">
47+
<button class="btn btn-primary btn-lg w-100 rounded" type="submit">{{ __('Check') }}</button>
48+
</div>
49+
</form>
50+
<div id="resultContainer" class="mt-4"></div>
51+
</div>
52+
</div>
53+
<style>
54+
input[name="domain"],
55+
button[type="submit"] {
56+
height: calc(2.875rem + 2px);
57+
border-radius: 0.5rem;
58+
}
59+
</style>
60+
{% endif %}
61+
62+
{% if otherProducts|length > 0 %}
63+
<h3 class="mb-3">Other Services</h3>
64+
<div class="row">
65+
{% for item in otherProducts %}
66+
{% set provider = item.provider %}
67+
{% set product = item.product %}
68+
<div class="col-md-4 mb-4">
69+
<div class="card h-100 cursor-pointer" onclick="showProductOptions('{{ provider.id }}', '{{ product.label }}')">
70+
<div class="card-body">
71+
<h4 class="card-title">{{ product.label }}</h4>
72+
<p class="text-muted small">{{ provider.name }} — {{ product.description }}</p>
73+
<span class="badge bg-blue text-blue-fg">from {{ currency }} {{ product.price }}/{{ product.billing }}</span>
8574
</div>
8675
</div>
8776
</div>
77+
{% endfor %}
78+
</div>
79+
{% endif %}
80+
81+
<div id="productOptions" class="card mt-4 d-none">
82+
<div class="card-body">
83+
<h4>{{ __('Configure Product') }}</h4>
84+
<form method="post" action="{{ route('createOrder') }}">
85+
{{ csrf.field | raw }}
86+
<input type="hidden" name="product_choice" id="selectedProductChoice">
87+
<div class="row g-3" id="dynamicFields"></div>
88+
<div class="mt-3"><button class="btn btn-primary">{{ __('Submit Order') }}</button></div>
89+
</form>
90+
</div>
8891
</div>
92+
8993
</div>
9094
</div>
9195
{% include 'partials/footer.twig' %}
9296
</div>
9397
<script>
94-
const productData = {{ providers|json_encode|raw }};
98+
const productData = {{ otherProducts|json_encode|raw }};
9599
const currency = {{ currency|json_encode|raw }};
96100
97-
function selectProduct(providerId, productCode, cardId) {
98-
document.querySelectorAll('.card .card-status-start').forEach(el => el.classList.add('d-none'));
99-
100-
const selectedCard = document.getElementById(cardId);
101-
if (selectedCard) {
102-
const statusEl = selectedCard.querySelector('.card-status-start');
103-
if (statusEl) statusEl.classList.remove('d-none');
104-
}
105-
106-
document.querySelectorAll('input[name="product_choice"]').forEach(r => r.checked = false);
107-
const inputEl = document.querySelector(`input[value="${providerId}::${productCode}"]`);
108-
if (inputEl) inputEl.checked = true;
109-
110-
const provider = productData.find(p => p.id == providerId);
111-
const product = provider.products[productCode];
112-
113-
document.getElementById('productFields').classList.remove('d-none');
114-
115-
const extra = document.getElementById('dynamicFields');
116-
117-
if (product.type === 'domain') {
118-
const requiredFields = provider.credentials?.required_fields || {};
119-
120-
extra.innerHTML = `
121-
<div class="col-md-8">
122-
<label class="form-label">Domain Name</label>
123-
<div class="input-group">
124-
<input type="text" name="config[domain]" class="form-control" placeholder="yourname">
125-
<span class="input-group-text">.com</span>
126-
</div>
127-
<small class="form-hint">Enter the domain name without TLD.</small>
128-
</div>
129-
130-
<div class="col-md-4">
131-
<label class="form-label">Registration Period</label>
132-
<select name="config[years]" class="form-select" required>
133-
${Object.entries(product.register || {}).map(([year, price]) =>
134-
`<option value="${year}">${year} year(s) (${currency} ${parseFloat(price).toFixed(2)})</option>`).join('')}
135-
</select>
136-
</div>
137-
138-
<div class="col-12">
139-
<label class="form-label">Nameservers</label>
140-
<div class="row g-2">
141-
<div class="col-md-4"><input type="text" class="form-control" name="config[nameservers][]" placeholder="ns1.example.com" required></div>
142-
<div class="col-md-4"><input type="text" class="form-control" name="config[nameservers][]" placeholder="ns2.example.com" required></div>
143-
<div class="col-md-4"><input type="text" class="form-control" name="config[nameservers][]" placeholder="ns3.example.com (optional)"></div>
144-
</div>
145-
</div>
146-
147-
<div class="col-12 mt-4">
148-
<label class="form-label">Registrant Contact</label>
149-
<div class="form-selectgroup">
150-
<label class="form-selectgroup-item">
151-
<input type="radio" name="contact_mode" value="default" class="form-selectgroup-input" checked onchange="toggleContactFields(false)">
152-
<span class="form-selectgroup-label">Use Default Contact</span>
153-
</label>
154-
<label class="form-selectgroup-item">
155-
<input type="radio" name="contact_mode" value="custom" class="form-selectgroup-input" onchange="toggleContactFields(true)">
156-
<span class="form-selectgroup-label">Enter New Contact</span>
157-
</label>
158-
</div>
159-
</div>
160-
161-
<div id="contactFields" class="row g-3 mt-3 d-none">
162-
<div class="col-md-6">
163-
<label class="form-label">Full Name</label>
164-
<input type="text" name="config[contact][name]" class="form-control">
165-
</div>
166-
<div class="col-md-6">
167-
<label class="form-label">Email</label>
168-
<input type="email" name="config[contact][email]" class="form-control">
169-
</div>
101+
function showProductOptions(providerId, productLabel) {
102+
const target = productData.find(p => p.provider.id == providerId && p.product.label == productLabel);
103+
if (!target) return;
104+
105+
const product = target.product;
106+
const fieldsDiv = document.getElementById('dynamicFields');
107+
const choiceInput = document.getElementById('selectedProductChoice');
108+
109+
fieldsDiv.innerHTML = '';
110+
choiceInput.value = `${providerId}::${productLabel}`;
111+
document.getElementById('productOptions').classList.remove('d-none');
112+
113+
fieldsDiv.innerHTML += `
114+
<div class="col-md-6">
115+
<label class="form-label">Service Name</label>
116+
<input type="text" name="config[name]" class="form-control" required>
117+
</div>
118+
<div class="col-md-6">
119+
<label class="form-label">Billing Cycle</label>
120+
<select name="config[cycle]" class="form-select" required>
121+
<option value="monthly">Monthly</option>
122+
<option value="yearly" selected>Yearly</option>
123+
</select>
124+
</div>
125+
`;
126+
127+
if (product.fields) {
128+
for (const [field, def] of Object.entries(product.fields)) {
129+
fieldsDiv.innerHTML += `
170130
<div class="col-md-6">
171-
<label class="form-label">Organization (optional)</label>
172-
<input type="text" name="config[contact][org]" class="form-control">
173-
</div>
174-
<div class="col-md-6">
175-
<label class="form-label">Phone</label>
176-
<input type="text" name="config[contact][phone]" class="form-control" placeholder="+1.1234567890">
177-
</div>
178-
<div class="col-12">
179-
<label class="form-label">Address</label>
180-
<input type="text" name="config[contact][address]" class="form-control" placeholder="Street, City, Country">
181-
</div>
182-
</div>
183-
`;
184-
185-
Object.entries(requiredFields).forEach(([fieldName, fieldDef]) => {
186-
const fieldType = fieldDef.type || 'text';
187-
extra.innerHTML += `
188-
<div class="col-md-6">
189-
<label class="form-label">${fieldDef.label || fieldName}</label>
190-
<input type="${fieldType}" name="config[${fieldName}]" class="form-control" placeholder="${fieldDef.hint || ''}" ${fieldDef.required ? 'required' : ''}>
131+
<label class="form-label">${def.label || field}</label>
132+
<input type="${def.type || 'text'}" name="config[${field}]" class="form-control" placeholder="${def.hint || ''}" ${def.required ? 'required' : ''}>
191133
</div>
192134
`;
193-
});
194-
} else {
195-
extra.innerHTML = '';
135+
}
196136
}
197137
}
198138
199-
function toggleContactFields(show) {
200-
const el = document.getElementById('contactFields');
201-
if (el) {
202-
if (show) {
203-
el.classList.remove('d-none');
204-
} else {
205-
el.classList.add('d-none');
139+
async function checkDomain(event) {
140+
event.preventDefault();
141+
const domainInput = document.getElementById("domainInput");
142+
const resultContainer = document.getElementById("resultContainer");
143+
144+
const domain = domainInput.value.trim();
145+
if (!domain) {
146+
resultContainer.innerHTML = '<div class="alert alert-warning">Please enter a domain name.</div>';
147+
return;
206148
}
207-
}
149+
150+
// Send POST request
151+
try {
152+
const response = await fetch('/dapi/domain/check', {
153+
method: 'POST',
154+
headers: {
155+
'Content-Type': 'application/json'
156+
},
157+
body: JSON.stringify({ domain: [domain] }) // Sending domain array
158+
});
159+
160+
const data = await response.json();
161+
resultContainer.innerHTML = formatResult(data);
162+
} catch (error) {
163+
resultContainer.innerHTML = '<div class="alert alert-danger">An error occurred. Please try again.</div>';
164+
}
165+
}
166+
167+
function formatResult(data) {
168+
if (!data.domains || data.domains.length === 0) {
169+
return '<div class="alert alert-info">No results found.</div>';
170+
}
171+
172+
return data.domains.map(domain => `
173+
<div class="card border-${domain.available ? 'success' : 'danger'} mt-3">
174+
<div class="card-body">
175+
<h5 class="card-title">${domain.name}</h5>
176+
<p class="card-text">
177+
<strong>Status:</strong>
178+
<span class="badge bg-${domain.available ? 'success' : 'danger'} text-${domain.available ? 'success' : 'danger'}-fg">
179+
${domain.available ? 'Available' : 'Not Available'}
180+
</span>
181+
</p>
182+
${domain.reason ? `<p class="text-muted"><strong>Reason:</strong> ${domain.reason}</p>` : ''}
183+
${domain.available ? `
184+
<a href="/register?domain=${encodeURIComponent(domain.name)}" class="btn btn-success">
185+
Register
186+
</a>
187+
` : ''}
188+
</div>
189+
</div>
190+
`).join('');
208191
}
209192
</script>
210193
{% endblock %}

0 commit comments

Comments
 (0)