Deploy: 2026-05-22 02:09:48
This commit is contained in:
71
backend/public/favicon.svg
Normal file
71
backend/public/favicon.svg
Normal file
@@ -0,0 +1,71 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<defs>
|
||||
<!-- Background Gradient -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0b0d19" />
|
||||
<stop offset="50%" stop-color="#111428" />
|
||||
<stop offset="100%" stop-color="#070810" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Icon Gradient -->
|
||||
<linearGradient id="accentGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#06b6d4" />
|
||||
<stop offset="50%" stop-color="#3b82f6" />
|
||||
<stop offset="100%" stop-color="#6366f1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Glow Filter -->
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="12" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- App Icon Background Rounded Rect -->
|
||||
<rect width="512" height="512" rx="128" fill="url(#bgGrad)" stroke="rgba(255, 255, 255, 0.05)" stroke-width="4" />
|
||||
|
||||
<!-- Inner decorative tech circle -->
|
||||
<circle cx="256" cy="256" r="220" fill="none" stroke="rgba(6, 182, 212, 0.1)" stroke-width="2" stroke-dasharray="10 15" />
|
||||
|
||||
<g filter="url(#glow)">
|
||||
<!-- Speech Bubble Base -->
|
||||
<path d="M 160 350
|
||||
L 120 390
|
||||
L 120 330
|
||||
A 140 140 0 1 1 370 290
|
||||
A 140 140 0 0 1 160 350 Z"
|
||||
fill="none"
|
||||
stroke="url(#accentGrad)"
|
||||
stroke-width="16"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round" />
|
||||
|
||||
<!-- Intelligent Neural Nodes (Brain Paths inside Speech Bubble) -->
|
||||
<!-- Center core node -->
|
||||
<circle cx="256" cy="210" r="14" fill="#ffffff" />
|
||||
|
||||
<!-- Connections and branching nodes -->
|
||||
<!-- Left top branch -->
|
||||
<path d="M 256 210 L 200 170" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="200" cy="170" r="10" fill="#06b6d4" />
|
||||
|
||||
<!-- Right top branch -->
|
||||
<path d="M 256 210 L 312 170" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="312" cy="170" r="10" fill="#6366f1" />
|
||||
|
||||
<!-- Left bottom branch -->
|
||||
<path d="M 256 210 L 190 240" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="190" cy="240" r="10" fill="#06b6d4" />
|
||||
|
||||
<!-- Right bottom branch -->
|
||||
<path d="M 256 210 L 322 240" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="322" cy="240" r="10" fill="#6366f1" />
|
||||
|
||||
<!-- Straight top vertical sensor -->
|
||||
<path d="M 256 210 L 256 140" fill="none" stroke="#3b82f6" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="256" cy="140" r="10" fill="#3b82f6" />
|
||||
|
||||
<!-- Subtle horizontal link -->
|
||||
<path d="M 200 170 A 70 70 0 0 1 312 170" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="4" stroke-dasharray="5 5" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nabeh Hub — WhatsApp Gateway & CRM</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="description" content="Connect, manage, and scale your WhatsApp communication.">
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
@@ -636,7 +637,10 @@
|
||||
<div class="auth-wrapper">
|
||||
<div class="auth-card">
|
||||
<div class="brand-header">
|
||||
<div class="brand-logo" id="main-brand-logo">Nabeh</div>
|
||||
<div class="brand-logo" id="main-brand-logo" style="display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 0.75rem;">
|
||||
<img src="/logo.svg" alt="Nabeh Logo" style="width: 48px; height: 48px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);">
|
||||
<span style="font-family: 'Outfit', sans-serif; font-size: 2.2rem; font-weight: 800; background: linear-gradient(135deg, #fff 30%, var(--primary-accent) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.05em;">Nabeh</span>
|
||||
</div>
|
||||
<div class="brand-subtitle">WhatsApp Hub & Marketing Automation</div>
|
||||
</div>
|
||||
|
||||
@@ -704,10 +708,13 @@
|
||||
<!-- Main Dashboard View -->
|
||||
<template x-if="isLoggedIn">
|
||||
<div>
|
||||
<div class="dashboard-header">
|
||||
<div>
|
||||
<h1 style="font-size: 1.8rem; margin-bottom: 0.25rem;">Nabeh Dashboard</h1>
|
||||
<p class="text-muted" style="font-size: 0.9rem;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
|
||||
<div class="dashboard-header" style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<img src="/logo.svg" alt="Nabeh Logo" style="width: 52px; height: 52px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.35); border: 1px solid rgba(255, 255, 255, 0.08);">
|
||||
<div>
|
||||
<h1 style="font-size: 1.8rem; margin: 0 0 0.15rem 0;">Nabeh Dashboard</h1>
|
||||
<p class="text-muted" style="font-size: 0.9rem; margin: 0;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="avatar" x-text="user.name.charAt(0).toUpperCase()"></div>
|
||||
@@ -733,6 +740,9 @@
|
||||
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'chatbot' }" @click="activeDashboardTab = 'chatbot'; fetchChatbotSettings()" id="nav-chatbot-btn">
|
||||
<span>🤖</span> AI Chatbot Settings
|
||||
</button>
|
||||
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'integrations' }" @click="activeDashboardTab = 'integrations'; fetchEndpoints()" id="nav-integrations-btn">
|
||||
<span>🔌</span> API Integrations
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right Dashboard Panels -->
|
||||
@@ -1057,6 +1067,53 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Panel: API Integrations -->
|
||||
<div class="panel" x-show="activeDashboardTab === 'integrations'" id="panel-integrations">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h2 style="font-size: 1.4rem; margin: 0;">API Endpoints Integration</h2>
|
||||
<button class="btn btn-primary" style="width: auto;" @click="endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' }; showAddEndpointModal = true" id="add-endpoint-btn">
|
||||
+ Add API Integration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;">
|
||||
Configure external web APIs for multi-tenant integrations. The chatbot can fetch user profiles or verify payment details dynamically.
|
||||
</p>
|
||||
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Integration Name</th>
|
||||
<th>Action Type</th>
|
||||
<th>Endpoint URL</th>
|
||||
<th>Description</th>
|
||||
<th style="width: 150px; text-align: center;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="endpoint in endpoints" :key="endpoint.id">
|
||||
<tr>
|
||||
<td class="font-semibold" x-text="endpoint.name"></td>
|
||||
<td>
|
||||
<span class="status-badge" :class="endpoint.action_type === 'verify_payment' ? 'badge-waiting_qr' : 'badge-connecting'" style="margin: 0; padding: 0.2rem 0.5rem; font-size: 0.75rem;" x-text="endpoint.action_type === 'verify_payment' ? 'Verify Payment' : 'Fetch User Info'"></span>
|
||||
</td>
|
||||
<td style="font-family: monospace; font-size: 0.85rem;" x-text="endpoint.endpoint_url"></td>
|
||||
<td x-text="endpoint.description || 'No description provided.'"></td>
|
||||
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;">
|
||||
<button class="btn btn-secondary" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="editEndpoint(endpoint)" :id="'edit-endpoint-btn-' + endpoint.id">Edit</button>
|
||||
<button class="btn btn-danger" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="deleteEndpoint(endpoint.id)" :id="'delete-endpoint-btn-' + endpoint.id">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<template x-if="endpoints.length === 0">
|
||||
<div class="empty-state" id="empty-endpoints-state">No API endpoints configured yet. Connect Intaleq or Salla integrations.</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1097,6 +1154,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Add/Edit Endpoint Integration -->
|
||||
<div class="modal-overlay" x-show="showAddEndpointModal" id="modal-add-endpoint" style="display: none;">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" x-text="endpointForm.id ? 'Edit API Endpoint Integration' : 'Add API Endpoint Integration'"></h3>
|
||||
<button class="modal-close" @click="showAddEndpointModal = false">×</button>
|
||||
</div>
|
||||
<form @submit.prevent="submitAddEndpoint()" id="form-add-endpoint">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Integration Name</label>
|
||||
<input type="text" class="form-input" x-model="endpointForm.name" required placeholder="e.g. Intaleq Driver Lookup" id="add-endpoint-name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Endpoint URL</label>
|
||||
<input type="url" class="form-input" x-model="endpointForm.endpoint_url" required placeholder="https://yourdomain.com/api/nabeh/action" id="add-endpoint-url">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Action Type</label>
|
||||
<select class="form-input" x-model="endpointForm.action_type" required id="add-endpoint-action-type">
|
||||
<option value="verify_payment">Verify Payment (تحليل وتأكيد الدفع)</option>
|
||||
<option value="fetch_user_info">Fetch User Info (جلب معلومات السائق أو العميل)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-input" x-model="endpointForm.description" placeholder="A brief explanation of what this endpoint does..." id="add-endpoint-description"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Custom HTTP Headers (JSON Object)</label>
|
||||
<textarea class="form-input" style="font-family: monospace; font-size: 0.85rem; height: 120px;" x-model="endpointForm.headers" placeholder='{ "Authorization": "Bearer YOUR_SECRET_TOKEN", "X-Custom-Source": "Nabeh App" }' id="add-endpoint-headers"></textarea>
|
||||
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;">
|
||||
Optionally define any authorization tokens or custom headers in JSON format.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddEndpointModal = false">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
|
||||
<span x-show="!actionLoading" x-text="endpointForm.id ? 'Save Changes' : 'Create Integration'"></span>
|
||||
<span x-show="actionLoading" class="spinner"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: New Template -->
|
||||
<div class="modal-overlay" x-show="showNewTemplateModal" id="modal-new-template" style="display: none;">
|
||||
<div class="modal-card">
|
||||
@@ -1231,6 +1335,18 @@
|
||||
showAddContactModal: false,
|
||||
showNewTemplateModal: false,
|
||||
showLaunchCampaignModal: false,
|
||||
showAddEndpointModal: false,
|
||||
|
||||
// Endpoint Form
|
||||
endpointForm: {
|
||||
id: null,
|
||||
name: '',
|
||||
endpoint_url: '',
|
||||
action_type: 'verify_payment',
|
||||
description: '',
|
||||
headers: ''
|
||||
},
|
||||
endpoints: [],
|
||||
|
||||
// Forms
|
||||
contactName: '',
|
||||
@@ -1358,6 +1474,7 @@
|
||||
this.contacts = [];
|
||||
this.templates = [];
|
||||
this.campaigns = [];
|
||||
this.endpoints = [];
|
||||
},
|
||||
|
||||
initializeDashboard() {
|
||||
@@ -1471,6 +1588,94 @@
|
||||
}
|
||||
},
|
||||
|
||||
// Custom API Integrations CRUD
|
||||
async fetchEndpoints() {
|
||||
try {
|
||||
const response = await fetch('/api/endpoints', {
|
||||
headers: { 'Authorization': `Bearer ${this.token}` }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.endpoints = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching endpoints:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async submitAddEndpoint() {
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
if (this.endpointForm.headers && this.endpointForm.headers.trim()) {
|
||||
try {
|
||||
JSON.parse(this.endpointForm.headers);
|
||||
} catch (jsonErr) {
|
||||
alert('Invalid JSON in Custom Headers. Please verify format.');
|
||||
this.actionLoading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('/api/endpoints', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify(this.endpointForm)
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.showAddEndpointModal = false;
|
||||
this.endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' };
|
||||
await this.fetchEndpoints();
|
||||
} else {
|
||||
alert(data.message || 'Failed to save integration endpoint.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving endpoint:', err);
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
editEndpoint(endpoint) {
|
||||
this.endpointForm = {
|
||||
id: endpoint.id,
|
||||
name: endpoint.name,
|
||||
endpoint_url: endpoint.endpoint_url,
|
||||
action_type: endpoint.action_type,
|
||||
description: endpoint.description || '',
|
||||
headers: endpoint.headers || ''
|
||||
};
|
||||
this.showAddEndpointModal = true;
|
||||
},
|
||||
|
||||
async deleteEndpoint(id) {
|
||||
if (!confirm('Are you sure you want to delete this integration endpoint?')) return;
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
const response = await fetch('/api/endpoints', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
await this.fetchEndpoints();
|
||||
} else {
|
||||
alert(data.message || 'Failed to delete endpoint.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting endpoint:', err);
|
||||
} finally {
|
||||
this.actionLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// CRM List Fetchers
|
||||
async fetchContacts() {
|
||||
this.selectedContactIds = [];
|
||||
|
||||
@@ -68,6 +68,38 @@ $router->get('/api/chatbot/rules', [\App\Controllers\ChatbotController::class, '
|
||||
$router->post('/api/chatbot/rules',[\App\Controllers\ChatbotController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/chatbot/generate-prompt-from-audio', [\App\Controllers\ChatbotController::class, 'generatePromptFromAudio'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
|
||||
// Custom Integration Endpoints Routes (Phase 5)
|
||||
$router->get('/api/endpoints', [\App\Controllers\EndpointController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/endpoints', [\App\Controllers\EndpointController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->delete('/api/endpoints', [\App\Controllers\EndpointController::class, 'delete'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
|
||||
// Mock External API for Entaleq Driver Info (Used to fetch real-time driver data)
|
||||
$router->post('/api/external/driver-info', function ($request, $response) {
|
||||
$body = $request->getBody();
|
||||
$phone = $body['phone'] ?? '';
|
||||
|
||||
if (empty($phone)) {
|
||||
$response->status(400)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Missing phone number'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'name' => 'أحمد الشريف',
|
||||
'phone' => $phone,
|
||||
'role' => 'سائق',
|
||||
'status' => 'نشط',
|
||||
'balance' => 75.25,
|
||||
'vehicle' => 'تويوتا كامري 2023',
|
||||
'trips_count' => 1420
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
// Mock External API for Entaleq Payment Verification (Used to demo automated slip validation)
|
||||
$router->post('/api/external/verify-payment', function ($request, $response) {
|
||||
$body = $request->getBody();
|
||||
@@ -107,5 +139,6 @@ $router->post('/api/external/verify-payment', function ($request, $response) {
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// 4. Dispatch the request
|
||||
$router->dispatch($request, $response);
|
||||
|
||||
71
backend/public/logo.svg
Normal file
71
backend/public/logo.svg
Normal file
@@ -0,0 +1,71 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<defs>
|
||||
<!-- Background Gradient -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0b0d19" />
|
||||
<stop offset="50%" stop-color="#111428" />
|
||||
<stop offset="100%" stop-color="#070810" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Icon Gradient -->
|
||||
<linearGradient id="accentGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#06b6d4" />
|
||||
<stop offset="50%" stop-color="#3b82f6" />
|
||||
<stop offset="100%" stop-color="#6366f1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Glow Filter -->
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="12" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- App Icon Background Rounded Rect -->
|
||||
<rect width="512" height="512" rx="128" fill="url(#bgGrad)" stroke="rgba(255, 255, 255, 0.05)" stroke-width="4" />
|
||||
|
||||
<!-- Inner decorative tech circle -->
|
||||
<circle cx="256" cy="256" r="220" fill="none" stroke="rgba(6, 182, 212, 0.1)" stroke-width="2" stroke-dasharray="10 15" />
|
||||
|
||||
<g filter="url(#glow)">
|
||||
<!-- Speech Bubble Base -->
|
||||
<path d="M 160 350
|
||||
L 120 390
|
||||
L 120 330
|
||||
A 140 140 0 1 1 370 290
|
||||
A 140 140 0 0 1 160 350 Z"
|
||||
fill="none"
|
||||
stroke="url(#accentGrad)"
|
||||
stroke-width="16"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round" />
|
||||
|
||||
<!-- Intelligent Neural Nodes (Brain Paths inside Speech Bubble) -->
|
||||
<!-- Center core node -->
|
||||
<circle cx="256" cy="210" r="14" fill="#ffffff" />
|
||||
|
||||
<!-- Connections and branching nodes -->
|
||||
<!-- Left top branch -->
|
||||
<path d="M 256 210 L 200 170" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="200" cy="170" r="10" fill="#06b6d4" />
|
||||
|
||||
<!-- Right top branch -->
|
||||
<path d="M 256 210 L 312 170" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="312" cy="170" r="10" fill="#6366f1" />
|
||||
|
||||
<!-- Left bottom branch -->
|
||||
<path d="M 256 210 L 190 240" fill="none" stroke="#06b6d4" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="190" cy="240" r="10" fill="#06b6d4" />
|
||||
|
||||
<!-- Right bottom branch -->
|
||||
<path d="M 256 210 L 322 240" fill="none" stroke="#6366f1" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="322" cy="240" r="10" fill="#6366f1" />
|
||||
|
||||
<!-- Straight top vertical sensor -->
|
||||
<path d="M 256 210 L 256 140" fill="none" stroke="#3b82f6" stroke-width="8" stroke-linecap="round" />
|
||||
<circle cx="256" cy="140" r="10" fill="#3b82f6" />
|
||||
|
||||
<!-- Subtle horizontal link -->
|
||||
<path d="M 200 170 A 70 70 0 0 1 312 170" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="4" stroke-dasharray="5 5" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
Reference in New Issue
Block a user