{% extends "console/layout.html" %}
{% block title %}{{ addon.name }} ยท Addons{% endblock %}
{% block content %}
<div class="p-4 sm:p-6"
x-data="addonDetail('{{ addon.id }}', {{ 'true' if addon.license_status == 'Expired' else 'false' }}, {{ 'true' if (addon.enabled and addon.license_status and addon.license_status != 'Valid') else 'false' }})">
{% if commerce_enabled %}
<div x-show="showExpiredModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
@keydown.escape.window="showExpiredModal = false">
<div class="relative w-full max-w-md rounded-2xl bg-white p-6 shadow-xl ring-1 ring-black/5"
@click.outside="showExpiredModal = false">
<button type="button" @click="showExpiredModal = false"
class="absolute right-4 top-4 rounded-md p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-colors">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
<span class="sr-only">{{ "addon_detail.close" | t }}</span>
</button>
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-amber-100">
<svg class="h-6 w-6 text-amber-600" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
</div>
<template x-if="licenseExpired">
<div>
<h3 class="mt-4 text-base font-semibold text-slate-900">{{ "addon_detail.license_expired" | t }}
</h3>
<p class="mt-2 text-sm text-slate-600">
{{ "addon_detail.license_expired_desc" | t }}
</p>
</div>
</template>
<template x-if="!licenseExpired">
<div>
<h3 class="mt-4 text-base font-semibold text-slate-900">{{ "addon_detail.no_active_license" | t }}
</h3>
<p class="mt-2 text-sm text-slate-600">
{{ "addon_detail.no_active_license_desc" | t }}
</p>
</div>
</template>
<div class="mt-5 flex flex-col gap-2 sm:flex-row-reverse">
<a href="https://miuda.ai/try" target="_blank"
class="inline-flex w-full items-center justify-center rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-500 transition-colors sm:w-auto"
x-text="licenseExpired ? '{{ "addon_detail.renew_license" | t }}'
: '{{ "addon_detail.get_license" | t }}'">
</a>
<button type=" button" @click="showExpiredModal = false"
class="inline-flex w-full items-center justify-center rounded-md bg-white px-4 py-2 text-sm font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 transition-colors sm:w-auto">
{{ "addon_detail.dismiss" | t }}
</button>
</div>
</div>
</div>
{% endif %}{# commerce_enabled #}
<div class="mx-auto max-w-7xl space-y-6">
<nav class="flex" aria-label="Breadcrumb">
<ol role="list" class="flex items-center space-x-4">
<li>
<div>
<a href="{{ base_path | safe}}/addons" class="text-slate-400 hover:text-slate-500">
<svg class="h-5 w-5 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M9.293 2.293a1 1 0 011.414 0l7 7A1 1 0 0117 11h-1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3a1 1 0 00-1-1H9a1 1 0 00-1 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-6H3a1 1 0 01-.707-1.707l7-7z"
clip-rule="evenodd" />
</svg>
<span class="sr-only">{{ "addon_detail.home" | t }}</span>
</a>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="h-5 w-5 flex-shrink-0 text-slate-300" fill="currentColor" viewBox="0 0 20 20"
aria-hidden="true">
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<a href="{{ base_path | safe}}/addons"
class="ml-4 text-sm font-medium text-slate-500 hover:text-slate-700">Addons</a>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="h-5 w-5 flex-shrink-0 text-slate-300" fill="currentColor" viewBox="0 0 20 20"
aria-hidden="true">
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<span class="ml-4 text-sm font-medium text-slate-500" aria-current="page">{{ addon.name
}}</span>
</div>
</li>
</ol>
</nav>
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-slate-900">{{ addon.name }}</h1>
<p class="text-sm text-slate-500">{{ "addon_detail.by_developer" | t }} {{ addon.developer }}</p>
</div>
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-slate-900">{{ addon.cost }}</span>
</div>
</div>
{% if addon.restart_required %}
<div class="rounded-md bg-yellow-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">{{ "addon_detail.restart_required" | t }}</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>{{ "addon_detail.restart_required_desc" | t }}</p>
</div>
</div>
</div>
</div>
{% endif %}
{% if commerce_enabled and addon.enabled and addon.category == "Commercial" and addon.license_status and
addon.license_status !=
"Valid" %}
<div class="rounded-lg bg-amber-50 border border-amber-200 p-4 flex items-start gap-3">
<svg class="h-5 w-5 text-amber-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
<div class="flex-1 min-w-0">
{% if addon.license_status == "Expired" %}
<p class="text-sm font-medium text-amber-800">{{ "addon_detail.license_expired_banner" | t }}
</p>
<p class="text-xs text-amber-700 mt-0.5">{{ "addon_detail.renew_at_online" | t }}</p>
{% else %}
<p class="text-sm font-medium text-amber-800">{{ "addon_detail.no_valid_license_banner" | t }}</p>
<p class="text-xs text-amber-700 mt-0.5">{{ "addon_detail.works_normally" | t }}</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="prose prose-slate max-w-none">
<p>{{ addon.description }}</p>
</div>
{% if commerce_enabled and addon.category == "Commercial" %}
<div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5 mt-6">
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div>
<h2 class="text-base font-semibold text-slate-900">{{ "addon_detail.license_section" | t }}</h2>
<p class="text-xs text-slate-500 mt-0.5">
{{ "addon_detail.commercial_addon_desc" | t }}
</p>
</div>
{% if addon.license_status %}
<div class="flex-shrink-0">
{% if addon.license_status == "Valid" %}
<span
class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-3 py-1 text-xs font-semibold text-green-700 ring-1 ring-inset ring-green-600/20">
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
{{ "addon_detail.valid" | t }}
</span>
{% elif addon.license_status == "Expired" %}
<span
class="inline-flex items-center gap-1.5 rounded-full bg-red-50 px-3 py-1 text-xs font-semibold text-red-700 ring-1 ring-inset ring-red-600/20">
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd" />
</svg>
{{ "addons.expired" | t }}
</span>
{% else %}
<span
class="inline-flex items-center gap-1.5 rounded-full bg-yellow-50 px-3 py-1 text-xs font-semibold text-yellow-700 ring-1 ring-inset ring-yellow-600/20">
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
{{ "addon_detail.not_licensed" | t }}
</span>
{% endif %}
</div>
{% endif %}
</div>
{% if addon.license_status == "Valid" %}
<dl class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 border-t border-slate-100 pt-4">
<div>
<dt class="text-xs font-medium text-slate-500">{{ "addon_detail.plan" | t }}</dt>
<dd class=\"mt-1 text-sm font-semibold text-slate-800\">{{ addon.license_plan | default('โ') }}</dd>
</div>
{% if addon.license_expiry %}
<div>
<dt class="text-xs font-medium text-slate-500">{{ "addon_detail.expires" | t }}</dt>
<dd class="mt-1 text-sm text-slate-800">{{ addon.license_expiry }}</dd>
</div>
{% endif %}
</dl>
{% endif %}
{% if addon.license_status == "Expired" %}
<div class="mt-4 rounded-lg bg-red-50 border border-red-200 p-4 flex items-start gap-3">
<svg class="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd" />
</svg>
<div class="flex-1">
<p class="text-sm font-medium text-red-800">{% if addon.license_expiry %}{{
"addon_detail.license_expired_on" | tvars({"expiry": addon.license_expiry}) }}{% else %}{{
"addon_detail.license_expired" | t }}{% endif %}</p>
<p class="text-xs text-red-700 mt-0.5">{{ "addon_detail.enter_renewed_key" | t }}</p>
</div>
</div>
{% elif addon.license_status != "Valid" %}
<div class="mt-4 rounded-lg bg-sky-50 border border-sky-200 p-4 flex items-start gap-3">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
clip-rule="evenodd" />
</svg>
<div class="flex-1">
<p class="text-sm font-medium text-sky-800">{{ "addon_detail.no_active_license_cta" | t }}</p>
<p class="text-xs text-sky-700 mt-0.5">{{ "addon_detail.enter_key_below" | t }}</p>
</div>
</div>
{% endif %}
<div class="mt-5 border-t border-slate-100 pt-5">
{% if addon.license_status == "Valid" %}
<button type="button" @click="showInput = !showInput"
class="text-xs font-medium text-slate-500 hover:text-sky-600 flex items-center gap-1 mb-3">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path
d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
</svg>
<span x-text="showInput ? '{{ "addon_detail.hide" | t }}' : '{{ "addon_detail.update_key" | t }}'"></span>
</button>
<div x-show=" showInput" x-cloak>
{% else %}
<div>
{% endif %}
<label class="block text-xs font-semibold uppercase tracking-wide text-slate-400 mb-1">{{
"addon_detail.license_key_label" | t }}</label>
<div class="flex rounded-md shadow-sm">
<input type="text" x-model="licenseKey"
class="block w-full rounded-none rounded-l-md border border-slate-200 px-3 py-2 text-sm text-slate-700 placeholder-slate-400 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
placeholder="{{ "addon_detail.license_key_placeholder" | t }}"
@keydown.enter="verifyLicense">
<button type="button" @click="verifyLicense" :disabled="loading"
class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
<svg x-show="loading" class="animate-spin h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
<span x-show="!loading">{{ "addon_detail.verify_save" | t }}</span>
<span x-show="loading">{{ "addon_detail.verifying" | t }}</span>
</button>
</div>
</div>
<div x-show="error" x-cloak
class="mt-3 rounded-md bg-red-50 border border-red-200 p-3 flex items-start gap-2">
<svg class="h-4 w-4 text-red-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd" />
</svg>
<p class="text-xs text-red-700" x-text="error"></p>
</div>
<div x-show="verifiedPlan" x-cloak
class="mt-3 rounded-md bg-green-50 border border-green-200 p-3">
<div class="flex items-start gap-2">
<svg class="h-4 w-4 text-green-600 flex-shrink-0 mt-0.5" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
<div>
<p class="text-xs font-semibold text-green-800"
x-text="translations['addon_detail.license_verified']"></p>
<p class="text-xs text-green-700 mt-0.5">
<span x-text="translations['addon_detail.plan_label']"></span> <span
class="font-medium" x-text="verifiedPlan"></span>
<template x-if="verifiedExpiry">
<span> — <span
x-text="translations['addon_detail.expires_on']"></span> <span
x-text="verifiedExpiry"></span></span>
</template>
</p>
<template x-if="verifiedScope && verifiedScope.length > 0">
<p class="text-xs text-green-700 mt-0.5">
<span x-text="translations['addon_detail.covers_label']"></span> <span
class="font-medium" x-text="verifiedScope.join(', ')"></span>
</p>
</template>
<template x-if="!verifiedScope || verifiedScope.length === 0">
<p class="text-xs text-green-700 mt-0.5"><span
x-text="translations['addon_detail.covers_label']"></span> <span
class="font-medium"
x-text="translations['addon_detail.covers_all']"></span></p>
</template>
</div>
</div>
<p class="text-xs text-green-600 mt-2" x-text="translations['addon_detail.reloading']"></p>
</div>
</div>
</div>
{% endif %}
{% if addon.screenshots and addon.screenshots|length > 0 %}
<div class="mt-6">
<h3 class="text-lg font-medium text-slate-900">{{ "addon_detail.screenshots" | t }}</h3>
<div class="mt-4 flex flex-col gap-6">
{% for screenshot in addon.screenshots %}
<div class="overflow-hidden rounded-lg shadow-sm ring-1 ring-gray-900/5">
<img src="{{ screenshot }}" alt="Screenshot" class="w-full max-w-4xl object-contain">
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<script type="application/json" id="__addonDetailT">{{ t | json | safe }}</script>
<script>
document.addEventListener('alpine:init', () => {
const translations = JSON.parse(document.getElementById('__addonDetailT').textContent || '{}');
const i18n = (key) => translations[key] || key;
Alpine.data('addonDetail', (id, licenseExpired, licenseUnlicensed) => ({
licenseKey: '',
error: '',
verifiedPlan: '',
verifiedExpiry: '',
verifiedScope: null,
loading: false,
showInput: false,
licenseExpired: licenseExpired,
showExpiredModal: licenseExpired || licenseUnlicensed,
translations: translations,
async verifyLicense() {
this.error = '';
this.verifiedPlan = '';
this.verifiedExpiry = '';
this.verifiedScope = null;
const key = this.licenseKey.trim();
if (!key) {
this.error = i18n('addon_detail.please_enter_key');
return;
}
this.loading = true;
try {
const response = await fetch('{{ api_prefix | safe }}/addons/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id, license_key: key })
});
const result = await response.json();
if (response.ok && result.success) {
this.verifiedPlan = result.plan || 'unknown';
this.verifiedExpiry = result.expiry || '';
this.verifiedScope = result.scope || null;
setTimeout(() => window.location.reload(), 2200);
} else {
this.error = result.message || i18n('addon_detail.verification_failed');
}
} catch (e) {
this.error = i18n('addon_detail.network_error');
} finally {
this.loading = false;
}
}
}))
})
</script>
{% endblock %}