<?php
|
namespace JVBase\admin;
|
|
use JVBase\integrations\ServiceMap;
|
use JVBase\managers\IconsManager;
|
use JVBase\registrar\Registrar;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Integrations
|
{
|
use ServiceMap;
|
|
public function __construct()
|
{
|
// Hook into WordPress admin
|
add_action('admin_menu', [$this, 'registerAdminPages']);
|
add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);
|
add_action(BASE.'dashboard_page_integrations', [$this, 'renderDashPage'], 10);
|
}
|
|
/**
|
* Register the integrations page and subpages
|
*/
|
public function registerAdminPages(): void
|
{
|
add_menu_page(
|
'Integrations',
|
'Integrations',
|
'manage_options',
|
'jvb-integrations',
|
[$this, 'renderPage'],
|
jvbCSSIcon('plugs-connected'),
|
1
|
);
|
|
// Add submenu pages for configured integrations
|
foreach ($this->getAvailableServices() as $service) {
|
$integration = $this->getService($service);
|
if ($integration->isSetUp() && method_exists($integration, 'getAllowedContent') && !empty($integration->getAllowedContent())) {
|
add_submenu_page(
|
'jvb-integrations',
|
$integration->getTitle() . ' Defaults',
|
$integration->getTitle(),
|
'manage_options',
|
'jvb-integration-'.$integration->getServiceName(),
|
[$integration, 'renderDefaults']
|
);
|
}
|
}
|
}
|
|
/**
|
* Enqueue admin assets
|
*/
|
public function enqueueAdminAssets(string $hook): void
|
{
|
// Check if we're on a JVB admin page
|
if (!str_contains($hook, 'jvb')) {
|
return;
|
}
|
|
wp_enqueue_style(
|
'jvb-dash',
|
);
|
|
wp_enqueue_script(
|
'jvb-admin-scripts',
|
JVB_URL . 'assets/js/min/integrations.min.js',
|
['jvb-auth'],
|
'1.0.0',
|
true
|
);
|
|
IconsManager::for()->enqueueIconStyles();
|
IconsManager::for('dash')->enqueueIconStyles();
|
IconsManager::for('forms')->enqueueIconStyles();
|
|
$queue = [
|
'api' => rest_url('jvb/v1/'),
|
'redirect' => wp_login_url(home_url(add_query_arg(null, null))),
|
'labels' => Registrar::getLabels(),
|
];
|
|
wp_localize_script('jvb-auth', 'jvbSettings', $queue);
|
//
|
// wp_localize_script(
|
// 'jvb-admin-scripts',
|
// 'jvbSettings',
|
// [
|
// 'api' => rest_url('jvb/v1/'),
|
// 'currentUser' => get_current_user_id(),
|
// 'nonce' => wp_create_nonce('wp_rest'),
|
// ]
|
// );
|
}
|
|
public function renderDashPage():void
|
{
|
$this->renderPage();
|
}
|
/**
|
* Render the main integrations page
|
*/
|
public function renderPage(): void
|
{
|
?>
|
<div class="wrap jvb-admin-wrap">
|
<h1>Integrations</h1>
|
|
<div class="jvb-admin-content">
|
<?php
|
// Display any admin notices from form submissions
|
if (is_admin()) {
|
settings_errors('jvb_integrations');
|
}
|
|
$this->displayNotices();
|
?>
|
|
<div class="item-grid integrations">
|
<?php
|
foreach ($this->getAvailableServices() as $service) {
|
$integration = $this->getService($service);
|
if ($integration->isSetUp() && method_exists($integration, 'renderConnection')) {
|
$integration->renderConnection();
|
}
|
}
|
|
?>
|
</div>
|
</div>
|
</div>
|
|
<?php if (is_admin()) {
|
$this->renderStyles();
|
} ?>
|
<?php
|
}
|
|
/**
|
* Display admin notices from URL parameters
|
*/
|
private function displayNotices(): void
|
{
|
if (isset($_GET['success'])) {
|
$message = sanitize_text_field($_GET['success']);
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($message) . '</p></div>';
|
}
|
|
if (isset($_GET['error'])) {
|
$message = sanitize_text_field($_GET['error']);
|
echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($message) . '</p></div>';
|
}
|
}
|
|
/**
|
* Render an individual integration card
|
*/
|
private function renderIntegrationCard(string $service, $integration): void
|
{
|
$is_connected = $integration->isSetUp();
|
$credentials = $integration->getCredentials();
|
$last_updated = $credentials['last_updated'] ?? 0;
|
?>
|
<div class="integration <?php echo $is_connected ? 'connected' : 'disconnected'; ?>"
|
data-service="<?php echo esc_attr($service); ?>">
|
<div class="header">
|
<h3><?php echo esc_html($integration->getTitle()); ?></h3>
|
<div class="connection-status">
|
<?php if ($is_connected): ?>
|
<span class="status-indicator connected">●</span>
|
<span>Connected</span>
|
<?php else: ?>
|
<span class="status-indicator disconnected">●</span>
|
<span>Disconnected</span>
|
<?php endif; ?>
|
</div>
|
</div>
|
|
<?php if ($is_connected && $last_updated > 0): ?>
|
<div class="integration-meta">
|
<small>Last updated: <?php echo human_time_diff($last_updated) . ' ago'; ?></small>
|
<?php if (method_exists($integration, 'getLastTestedTime')):
|
$last_tested = $integration->getLastTestedTime();
|
if ($last_tested): ?>
|
<br><small>Last tested: <?php echo human_time_diff($last_tested) . ' ago'; ?></small>
|
<?php endif;
|
endif; ?>
|
</div>
|
<?php endif; ?>
|
|
<div class="integration-content">
|
<?php $this->renderCredentialsForm($service, $integration, $is_connected) ?>
|
</div>
|
</div>
|
<?php
|
}
|
|
/**
|
* Render standard credentials form
|
*/
|
private function renderCredentialsForm(string $service, $integration, bool $is_connected): void
|
{
|
?>
|
<form class="jvb-integration-form"
|
data-service="<?php echo esc_attr($service); ?>"
|
data-nonce="<?php echo wp_create_nonce('jvb_integration_' . $service); ?>">
|
|
<input type="hidden" name="service" value="<?php echo esc_attr($service); ?>">
|
|
<?php
|
// Let the integration render its own connection settings
|
if (method_exists($integration, 'renderConnectionSettings')) {
|
$integration->renderConnectionSettings();
|
} else {
|
echo '<p>This integration does not have configurable settings.</p>';
|
}
|
?>
|
|
<div class="integration-actions">
|
<?php if (method_exists($integration, 'renderConnectionSettings')): ?>
|
<button type="button"
|
data-action="save_credentials"
|
class="button button-primary jvb-action-button">
|
<?php echo $is_connected ? 'Update Credentials' : 'Save Credentials'; ?>
|
</button>
|
<?php endif; ?>
|
|
<?php if ($is_connected): ?>
|
<button type="button"
|
data-action="test_connection"
|
class="button jvb-action-button">
|
Test Connection
|
</button>
|
<button type="button"
|
data-action="clear_credentials"
|
class="button button-link-delete jvb-action-button"
|
data-confirm="Are you sure you want to delete these credentials?">
|
Delete
|
</button>
|
|
<?php if (method_exists($integration, 'renderAdditionalOptions')): ?>
|
<a href="<?php echo admin_url('admin.php?page=jvb-integration-' . $service); ?>"
|
class="button">
|
Advanced Settings
|
</a>
|
<?php endif; ?>
|
<?php endif; ?>
|
</div>
|
</form>
|
<?php
|
}
|
|
/**
|
* Render styles for the integrations page
|
*/
|
private function renderStyles(): void
|
{
|
// jvbInlineStyles('style');
|
?>
|
<style>
|
:root {
|
/*****************************
|
Layout
|
*****************************/
|
--half: min(384px, 17.5vw);
|
--narrow: min(500px, 62.5vw);
|
--content: min(768px, 65vw);
|
--wider: min(900px, 75vw);
|
--wide: min(1024px, 90vw);
|
--full: 100vw;
|
--offScreen: -200vw;
|
|
--aside: min(500px, calc(100vw - 2rem));
|
|
/*****************************
|
Sizing
|
*****************************/
|
--chip: 1.5rem;
|
--chipchip: 3rem;
|
--chip_: 2rem;
|
--btn: 4rem;
|
--btn_: 5rem;
|
--btnbtn: 8rem;
|
--maxHeight: calc(100vh - var(--btnbtn));
|
|
/*****************************
|
Flex
|
*****************************/
|
--dir: row;
|
--justify: center;
|
--align: center;
|
--wrap: wrap;
|
--gap: .5rem;
|
|
/*****************************
|
Typography
|
*****************************/
|
--w: 1.2em; /** For icons **/
|
|
--font-base: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
--heading: 'Aleo', var(--font-base);
|
--body: 'Josefin Slab', var(--font-base);
|
--fw-h-light: 400;
|
--fw-h: 900;
|
--fw-h-bold: 900;
|
--fw-b-light: 200;
|
--fw-b: 400;
|
--fw-b-bold: 700;
|
|
--txt-small: clamp(.75rem, 1.1337rem + -1.2278vw, .059375rem);
|
--txt-x-small: clamp(.95rem, .879rem + .19vw, 1.05rem);
|
--txt-medium: clamp(1.1rem, .993rem + .286vw, 1.25rem);
|
--txt-x-medium: clamp(1.4rem, .971rem + 1.143vw, 2rem);
|
--txt-large: clamp(1.3rem, .6rem + 1.867vw, 2rem);
|
--txt-x-large:clamp(1.6rem, .957rem + 1.714vw, 2.5rem);
|
--txt-xx-large: clamp(2rem, 1.286rem + 1.905vw, 3rem);
|
--txt-xxx-large: clamp(2.5rem, 1.429rem + 2.857vw, 4rem);
|
--txt-enormous: calc(26vh - 4rem);
|
|
/*****************************
|
Colours
|
*****************************/
|
--filter: grayscale(.3) sepia(.4);
|
--filterNone: grayscale(0) sepia(0);
|
|
--mix-blend-mode: darken;
|
--coverBlend: overlay;
|
--coverIndex: 2;
|
--light-0: 239,239,239; /* #efefef; */
|
--light-50: 226,226,226; /* #e2e2e2; */
|
--light-100: 213,213,213; /* #d5d5d5; */
|
--light-200: 201,201,201; /* #c9c9c9; */
|
|
--dark-0: 21,21,21; /* #151515; */
|
--dark-50: 34,34,34; /* #222222; */
|
--dark-100: 46,46,46; /* #2e2e2e; */
|
--dark-200: 59,59,59; /* #3b3b3b; */
|
|
--action-0: 255,0,128; /* #ff0080; */
|
--action-50: 255,38,146; /* #ff2492; */
|
--action-100: 255,71,164; /* #ff47a4; */
|
--action-200: 255,107,181; /* #ff6bb5; */
|
--action-contrast: var(--light-0);
|
|
--action-comp: oklch(from rgb(var(--action-0)) .8 .26 h);
|
--action-tri: oklch(from rgb(var(--action-0)) .82 .2 calc(h - 120));
|
--action-mono: oklch(from rgb(var(--action-0)) .3 .04 h);
|
--secondary-0: 214,145,33; /* #D69121; */
|
--secondary-50: 255,196,33; /* #ffc421; */
|
--secondary-100: 255,205,68;/* #ffcd44; */
|
--secondary-200: 255,215,104;/* #ffd768; */
|
--secondary-contrast: var(--light-0);
|
--secondary-comp: oklch(from rgb(var(--secondary-0)) .8 .26 h);
|
--secondary-tri: oklch(from rgb(var(--secondary-0)) .82 .2 calc(h - 120));
|
--secondary-mono: oklch(from rgb(var(--secondary-0)) .3 .04 h);
|
|
--success: 76,175,80; /* #4CAF50; */
|
--successLight: 234,246,235; /* #eaf6eb; */
|
--successDark: 14,33,15; /* #0e210f; */
|
--successBack: var(--successLight);
|
--successText: var(--successDark);
|
--warning: 232,167,55; /* #E8A737; */
|
--warningLight: 253,247,238;/* #fdf7ee; */
|
--warningDark: 52,35,6; /* #342306; */
|
--warningBack: var(--warningLight);
|
--warningText: var(--warningDark);
|
--error: 183,51,46; /* #B7332E; */
|
--errorLight: 250,235,234; /* #faebea; */
|
--errorDark: 40,11,10; /* #280b0a; */
|
--errorBack: var(--errorLight);
|
--errorText: var(--errorDark);
|
|
--base: var(--light-0);
|
--base-50: var(--light-50);
|
--base-100: var(--light-100);
|
--base-200: var(--light-200);
|
|
--contrast: var(--dark-0);
|
--contrast-50: var(--dark-50);
|
--contrast-100: var(--dark-100);
|
--contrast-200: var(--dark-200);
|
|
|
|
--contrast-comp: oklch(from rgb(var(--contrast)) .8 .26 h);
|
--contrast-tri: oklch(from rgb(var(--contrast)) .82 .2 calc(h - 120));
|
--contrast-mono: oklch(from rgb(var(--contrast)) .3 .04 h);
|
--base-comp: oklch(from rgb(var(--base)) .8 .26 h);
|
--base-tri: oklch(from rgb(var(--base)) .82 .2 calc(h - 120));
|
--base-mono: oklch(from rgb(var(--base)) .3 .04 h);
|
|
|
--tone-a: rgb(var(--contrast));
|
--tone-b: rgb(var(--base));
|
|
--duo-dark: oklch(from var(--tone-a) .22 c h);
|
--duo-light: oklch(from var(--tone-b) .78 c h);
|
|
--shimmer: rgba(var(--contrast),0) 0%,
|
rgba(var(--contrast),.05) 50%,
|
rgba(var(--contrast),0) 100%;
|
|
--op-1: .05;
|
--op-2: .15;
|
--op-3: .25;
|
--op-45: .45;
|
--op-4: .66;
|
--op-5: .75;
|
--op-6: .85;
|
|
/*****************************
|
Shadows
|
*****************************/
|
--shdw-inset: inset 0 0 4px 1px;
|
--shdw: 0 0 4px;
|
--shdw-down: 0 6px 5px -5px;
|
--shdw-right: 6px 0 5px -5px;
|
--shdw-left: -6px 0 5px -5px;
|
--shdw-up: 0 -6px 5px -5px;
|
--shdw-subtle: 0 25px 20px -20px;
|
--shdw-subtle-right: 10px 0 20px -20px;
|
--shdw-none: transparent 0 0 0;
|
|
/*****************************
|
Stacking
|
*****************************/
|
--z-1: 5;
|
--z-2: 10;
|
--z-3: 15;
|
--z-4: 20;
|
--z-5: 50;
|
--z-6: 100;
|
--z-7: 999;
|
--z-8: 1000;
|
--z-9: 999999;
|
--z-10: 9999999;
|
|
/*****************************
|
Rounding
|
*****************************/
|
--radius: 4px;
|
--p-outer: 1rem;
|
--radius-outer: calc(var(--radius) + var(--p-outer));
|
--p-y: var(--sp1);
|
--p-x: var(--sp1);
|
/*****************************
|
Transitions
|
*****************************/
|
--trans-fn: cubic-bezier(.47,.24,.07,.47);
|
--trans-t: .25s;
|
--trans-base: var(--trans-t) var(--trans-fn);
|
--trans-color: background-color var(--trans-base),
|
color var(--trans-base), border var(--trans-base);
|
--trans-transform: transform var(--trans-base);
|
--trans-size: width var(--trans-base),
|
height var(--trans-base),max-width var(--trans-base), max-height var(--trans-base);
|
--trans-vis: opacity var(--trans-base);
|
|
/*****************************
|
Spacing
|
*****************************/
|
--sp1: clamp(0.5rem, 0.714vw + 0.321rem, 1rem); /* 8px → 16px */
|
--sp2: clamp(0.75rem, 1.071vw + 0.482rem, 1.5rem); /* 12px → 24px */
|
--sp3: clamp(1rem, 1.429vw + 0.643rem, 2rem); /* 16px → 32px */
|
--sp4: clamp(1.5rem, 2.143vw + 0.964rem, 3rem); /* 24px → 48px */
|
--sp5: clamp(2rem, 2.857vw + 1.286rem, 4rem); /* 32px → 64px */
|
--sp6: clamp(3rem, 4.286vw + 1.929rem, 6rem); /* 48px → 96px */
|
--sp7: clamp(4rem, 5.714vw + 2.571rem, 8rem); /* 64px → 128px */
|
|
/*****************************
|
Scrollbar
|
*****************************/
|
--sb-width: 8px;
|
--sb-track: rgb(var(--base-100));
|
--sb-thumb: rgb(var(--action-0));
|
--sb-thumb-hover: rgb(var(--action-50));
|
--sb-thumb-border: 2px solid rgb(var(--base-50));
|
--sb-radius: 4px;
|
|
--details: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" fill="%232b1a0d" viewBox="0 0 256 256"><path d="M212.24,100.24l-80,80a6,6,0,0,1-8.48,0l-80-80a6,6,0,0,1,8.48-8.48L128,167.51l75.76-75.75a6,6,0,0,1,8.48,8.48Z"></path></svg>');
|
--link: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" fill="%232b1a0d" viewBox="0 0 256 256"><path d="M238,88.18a52.42,52.42,0,0,1-15.4,35.66l-34.75,34.75A52.28,52.28,0,0,1,150.62,174h-.05A52.63,52.63,0,0,1,98,119.9a6,6,0,0,1,6-5.84h.17a6,6,0,0,1,5.83,6.16A40.62,40.62,0,0,0,150.58,162h0a40.4,40.4,0,0,0,28.73-11.9l34.75-34.74A40.63,40.63,0,0,0,156.63,57.9l-11,11a6,6,0,0,1-8.49-8.49l11-11a52.62,52.62,0,0,1,74.43,0A52.83,52.83,0,0,1,238,88.18Zm-127.62,98.9-11,11A40.36,40.36,0,0,1,70.6,210h0a40.63,40.63,0,0,1-28.7-69.36L76.62,105.9A40.63,40.63,0,0,1,146,135.77a6,6,0,0,0,5.83,6.16H152a6,6,0,0,0,6-5.84A52.63,52.63,0,0,0,68.14,97.42L33.38,132.16A52.63,52.63,0,0,0,70.56,222h0a52.26,52.26,0,0,0,37.22-15.42l11-11a6,6,0,1,0-8.49-8.48Z"></path></svg>');
|
}
|
|
|
body:has(#theme-switcher:checked){
|
/*****************************
|
Colours
|
*****************************/
|
--mixBlendMode: lighten;
|
--coverBlend: multiply;
|
--coverIndex: 0;
|
|
--action-50: 219,0,110; /* #db006e; */
|
--action-100: 184,0.92; /* #b8005c; */
|
--action-200: 148,0,74; /* #94004a; */
|
|
--secondary-50: 216,159,0; /* #d89f00; */
|
--secondary-100: 181,133,0; /* #b58500; */
|
--secondary-200: 145,106,0; /* #916a00; */
|
|
--contrast: var(--light-0);
|
--contrast-50: var(--light-50);
|
--contrast-100: var(--light-100);
|
--contrast-200: var(--light-200);
|
|
--base: var(--dark-0);
|
--base-50: var(--dark-50);
|
--base-100: var(--dark-100);
|
--base-200: var(--dark-200);
|
|
|
--successBack: var(--successDark);
|
--successText: var(--successLight);
|
--warningBack: var(--warningDark);
|
--warningText: var(--warningLight);
|
--errorBack: var(--errorDark);
|
--errorText: var(--errorLight);
|
|
--shimmer: rgba(var(--light-0),0) 0%,
|
rgba(var(--light-0),.05) 50%,
|
rgba(var(--light-0),0) 100%;
|
}
|
|
.item-grid.integrations {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
gap: 2rem;
|
margin-top: 2rem;
|
}
|
|
.icon {
|
width: 1.5em;
|
height: 1.5em;
|
display: inline-block;
|
vertical-align: middle;
|
}
|
.icon svg {
|
width: 100%;
|
height: 100%;
|
}
|
|
.integration {
|
background: rgb(var(--base));
|
border: 2px solid rgb(Var(--contrast-200));
|
border-radius: 8px;
|
padding: 1.5rem;
|
position: relative;
|
transition: all 0.3s ease;
|
}
|
|
.integration.connected {
|
border-color: rgb(var(--success));
|
box-shadow: 0 2px 8px rgba(var(--successBack), .1);
|
}
|
|
.integration.disconnected {
|
border-color: rgb(var(--error));
|
box-shadow: 0 2px 8px rgba(var(--errorBack), 0.1);
|
}
|
|
.integration[data-oauth="true"].disconnected {
|
border-color: rgb(var(--warning));
|
box-shadow: 0 2px 8px rgba(var(--warning), 0.1);
|
}
|
|
.header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 1rem;
|
padding-bottom: 1rem;
|
border-bottom: 2px solid var(--contrast-200);
|
}
|
|
.header h3 {
|
margin: 0;
|
font-size: 1.4rem;
|
font-weight: bold;
|
text-transform: uppercase;
|
letter-spacing: 1px;
|
color: rgb(var(--contrast));
|
}
|
|
.connection-status {
|
display: flex;
|
align-items: center;
|
gap: 0.5rem;
|
font-weight: lighter;
|
text-transform: uppercase;
|
font-size: 0.9rem;
|
}
|
|
.status-indicator {
|
font-size: 1.2rem;
|
}
|
|
.status-indicator.connected {
|
color: rgb(var(--success));
|
}
|
|
.status-indicator.disconnected {
|
color: rgb(var(--error));
|
}
|
|
.integration-meta {
|
margin-bottom: 1rem;
|
color: rgb(var(--contrast-100));
|
font-size: 0.85rem;
|
}
|
|
.integration-content {
|
margin-bottom: 1.5rem;
|
}
|
|
|
.integration label {
|
display: block;
|
font-weight: bold;
|
margin-bottom: 0.5rem;
|
color: rgb(var(--contrast));
|
text-transform: uppercase;
|
font-size: 0.85rem;
|
letter-spacing: .5px;
|
}
|
|
.integration input,
|
.integration code,
|
.integration select,
|
.integration textarea {
|
width: 100%;
|
padding: 0.75rem;
|
border: 2px solid rgb(var(--contrast-200));
|
border-radius: 4px;
|
font-size: 0.9rem;
|
background: rgb(var(--base-50));
|
transition: border-color 0.3s ease;
|
}
|
|
.integration details {
|
margin: 1rem auto;
|
max-width: 90%;
|
}
|
.integration details summary {
|
margin: .5rem 0;
|
}
|
.integration code {
|
display: inline-block;
|
width: 90%;
|
margin: 0 .5rem;
|
background-color: rgb(var(--base-50));
|
border-color: rgb(var(--contrast-200));
|
user-select: all;
|
}
|
|
.integration input[type=checkbox],
|
.integration input[type=radio] {
|
width: unset;
|
}
|
|
.integration .actions {
|
display: flex;
|
flex-wrap: wrap;
|
justify-content: space-between;
|
gap: .5rem;
|
}
|
|
.integration input:focus,
|
.integration select:focus,
|
.integration textarea:focus {
|
outline: none;
|
border-color: rgb(var(--action-0));
|
box-shadow: var(--shdw) rgba(var(--action-0),.1);
|
}
|
|
.integration .hint {
|
font-size: 0.8rem;
|
color: ;
|
margin-top: 0.5rem;
|
line-height: 1.4;
|
}
|
|
button {
|
font-weight: bold;
|
text-transform: uppercase;
|
letter-spacing: 0.5px;
|
border-radius: 4px;
|
padding: 0.6rem 1.2rem;
|
font-size: 0.8rem;
|
cursor: pointer;
|
}
|
button:hover {
|
background: rgb(var(--action-0));
|
color: rgb(var(--action-contrast));
|
}
|
|
.integration-actions .button-primary {
|
background: rgb(var(--action-0));
|
border-color: rgb(var(--action-0));
|
color: rgb(var(--action-contrast));
|
}
|
|
.integration-actions .button-primary:hover {
|
background: rgb(var(--action-0));
|
border-color: rgb(var(--action-200));
|
}
|
|
.integration-actions .button-link-delete {
|
color: rgb(var(--action-0));
|
border-color: transparent;
|
background: transparent;
|
}
|
|
.integration-actions .button-link-delete:hover {
|
color: rgb(var(--action-0));
|
text-decoration: underline;
|
}
|
|
.unsaved-indicator {
|
position: absolute;
|
top: 10px;
|
right: 10px;
|
background: rgb(var(--action-0));
|
color: rgb(var(--action-contrast));
|
padding: 0.25rem 0.5rem;
|
border-radius: 4px;
|
font-size: 0.7rem;
|
font-weight: bold;
|
text-transform: uppercase;
|
letter-spacing: 0.5px;
|
z-index: 10;
|
}
|
|
/* Loading states */
|
.integration-actions button:disabled {
|
opacity: 0.6;
|
cursor: not-allowed;
|
}
|
|
.integration-actions button.loading::after {
|
content: '...';
|
animation: dots 1.5s infinite;
|
}
|
|
@keyframes dots {
|
0%, 20% { content: '.'; }
|
40% { content: '..'; }
|
60%, 100% { content: '...'; }
|
}
|
|
/* Success/Error states */
|
.integration.success {
|
animation: flash-success 0.5s;
|
}
|
|
.integration.error {
|
animation: flash-error 0.5s;
|
}
|
|
@keyframes flash-success {
|
0%, 100% { border-color: inherit; }
|
50% { border-color: rgb(var(--success)); }
|
}
|
|
@keyframes flash-error {
|
0%, 100% { border-color: inherit; }
|
50% { border-color: rgb(var(--error)); }
|
}
|
|
@media (max-width: 768px) {
|
.jvb-integrations-grid {
|
grid-template-columns: 1fr;
|
}
|
|
.integration-actions {
|
flex-direction: column;
|
}
|
|
.integration-actions .button {
|
width: 100%;
|
text-align: center;
|
}
|
}
|
/* Loading states */
|
.button.updating-message {
|
opacity: 0.7;
|
cursor: not-allowed;
|
}
|
|
/* Flash animations */
|
.integration.success-flash {
|
animation: flash-success 0.6s ease;
|
}
|
|
.integration.error-flash {
|
animation: flash-error 0.6s ease;
|
}
|
|
@keyframes flash-success {
|
0%, 100% {
|
background-color: rgb(var(--base));
|
border-color: rgb(var(--success));
|
}
|
50% {
|
background-color: rgb(var(--successBack));
|
border-color: rgb(var(--success));
|
box-shadow: var(--shdw) rgba(var(--success), 0.3);
|
}
|
}
|
|
@keyframes flash-error {
|
0%, 100% {
|
background-color: rgb(var(--base));
|
border-color: rgb(var(--error));
|
}
|
50% {
|
background-color: rgb(var(--errorBack));
|
border-color: rgb(var(--error));
|
box-shadow: var(--shdw) rgba(var(--error), .3);
|
}
|
}
|
|
/* Unsaved indicator */
|
.unsaved-indicator {
|
display: inline-block;
|
padding: 2px 8px;
|
background: rgb(var(--base-200));
|
border-radius: 4px;
|
font-size: 0.75rem;
|
font-weight: 500;
|
}
|
|
/* Admin notices */
|
.jvb-admin-notice {
|
margin: 15px 0;
|
transition: opacity 0.3s ease;
|
}
|
|
.integration-notification {
|
padding: 12px 20px;
|
margin: 10px 0;
|
border-radius: 4px;
|
font-size: 14px;
|
font-weight: 500;
|
animation: slideDown 0.3s ease-out;
|
}
|
|
@keyframes slideDown {
|
from {
|
opacity: 0;
|
transform: translateY(-20px);
|
}
|
to {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
}
|
|
.notification-success {
|
background-color: rgb(var(--successBack));
|
border: 1px solid rgb(var(--success));
|
color: rgb(var(--successText));
|
}
|
|
.notification-error {
|
background-color: rgb(var(--errorBack));
|
border: 1px solid rgb(var(--error));
|
color: rgb(var(--errorText));
|
}
|
|
.notification-info {
|
background-color: rgb(var(--warningBack));
|
border: 1px solid rgb(var(--warning));
|
color: rgb(var(--warningText));
|
}
|
|
/* OAuth Status Indicators */
|
.oauth-status {
|
margin: 10px 0;
|
padding: 8px;
|
border-radius: 4px;
|
display: inline-block;
|
}
|
|
.status-invalid {
|
color: rgb(var(--warning));
|
background-color: rgb(var(--warningBack));
|
padding: 5px 10px;
|
border-radius: 3px;
|
border: 1px solid rgb(var(--warningText));
|
}
|
|
/* Form Field Updates */
|
.form-field {
|
margin-bottom: 20px;
|
transition: opacity 0.3s ease;
|
}
|
|
.form-field.loading {
|
opacity: 0.6;
|
pointer-events: none;
|
}
|
|
.form-field label {
|
display: block;
|
margin-bottom: 5px;
|
font-weight: 600;
|
}
|
|
.form-field select,
|
.form-field input {
|
width: 100%;
|
max-width: 400px;
|
}
|
|
/* AJAX Update Indicator */
|
.jvb-ajax-update {
|
position: relative;
|
}
|
|
.jvb-ajax-update.updating::after {
|
content: '';
|
position: absolute;
|
right: 10px;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 16px;
|
height: 16px;
|
border: 2px solid rgb(var(--action-200));
|
border-top: 2px solid rgb(var(--action-0));
|
border-radius: 50%;
|
animation: spin 1s linear infinite;
|
}
|
|
@keyframes spin {
|
0% { transform: translateY(-50%) rotate(0deg); }
|
100% { transform: translateY(-50%) rotate(360deg); }
|
}
|
|
/* Webhook Signature Generator */
|
.webhook-signature-field {
|
display: flex;
|
gap: 10px;
|
align-items: center;
|
}
|
|
.generate-webhook-signature {
|
padding: 6px 12px;
|
background-color: rgb(var(--secondary-0));
|
color: rgb(var(--secondary-contrast));
|
border: none;
|
border-radius: 3px;
|
cursor: pointer;
|
font-size: 13px;
|
white-space: nowrap;
|
}
|
|
.generate-webhook-signature:hover {
|
background-color: rgb(var(--action-0));
|
}
|
|
/* Save Button States */
|
.save-credentials {
|
padding: 8px 16px;
|
background-color: rgb(var(--action-0));
|
color: rgb(var(--action-contrast));
|
border: none;
|
border-radius: 4px;
|
cursor: pointer;
|
font-size: 14px;
|
font-weight: 500;
|
transition: background-color 0.2s ease;
|
}
|
|
.save-credentials:hover:not(:disabled) {
|
background-color: rgb(var(--action-0));
|
}
|
|
.save-credentials:disabled {
|
opacity: 0.6;
|
cursor: not-allowed;
|
}
|
|
/* Connection Status Badge */
|
.connection-status {
|
display: inline-flex;
|
align-items: center;
|
gap: 5px;
|
padding: 4px 8px;
|
border-radius: 3px;
|
font-size: 12px;
|
font-weight: 500;
|
}
|
|
.connection-status.connected {
|
background-color: rgb(var(--successBack));
|
color: rgb(var(--successText));
|
}
|
|
.connection-status.disconnected {
|
background-color: rgb(var(--errorBack));
|
color: rgb(var(--errorText));
|
}
|
|
.connection-status.expired {
|
background-color: rgb(var(--warningBack));
|
color: rgb(var(--warningText));
|
}
|
|
/* Hidden location field that appears after account selection */
|
.location-field {
|
display: none;
|
animation: fadeIn 0.3s ease-out;
|
}
|
|
.location-field.visible {
|
display: block;
|
}
|
|
@keyframes fadeIn {
|
from {
|
opacity: 0;
|
}
|
to {
|
opacity: 1;
|
}
|
}
|
|
.connection-status.connection-status,
|
.oauth-connect.oauth-connect {
|
display: inline-block;
|
max-width: 50%;
|
vertical-align: middle;
|
}
|
.oauth-connect.oauth-connect a {
|
display: flex;
|
gap: .25rem;
|
|
}
|
.oauth-connect svg {
|
width: 2em;
|
height: auto;
|
}
|
|
</style>
|
<?php
|
}
|
|
}
|
|
new Integrations();
|