2505 lines
81 KiB
HTML
2505 lines
81 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>My Plugins - Plugin Compass</title>
|
|
<link rel="icon" type="image/png" href="/assets/Plugin.png">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&family=Inter:wght@400;600&display=swap"
|
|
rel="stylesheet">
|
|
<link rel="stylesheet" href="/chat/styles.css">
|
|
<style>
|
|
:root {
|
|
--shopify-green: #008060;
|
|
--shopify-green-dark: #004c3f;
|
|
--shopify-green-light: #e3f5ef;
|
|
--accent: #5A31F4;
|
|
--accent-2: #8B5CF6;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.apps-container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 40px 24px;
|
|
}
|
|
|
|
.apps-header {
|
|
margin-bottom: 48px;
|
|
}
|
|
|
|
.apps-header h1 {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
font-size: 36px;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin: 0 0 12px 0;
|
|
}
|
|
|
|
.apps-header p {
|
|
color: #6c757d;
|
|
font-size: 16px;
|
|
margin: 0;
|
|
}
|
|
|
|
.apps-actions {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 32px;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.upload-app-btn {
|
|
background: #0f172a;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
|
|
}
|
|
|
|
.upload-app-btn:hover:not(.locked) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.25);
|
|
}
|
|
|
|
.upload-app-btn.locked {
|
|
background: #f8fafc;
|
|
color: #9aa3ad;
|
|
border: 1px dashed #d0d7de;
|
|
box-shadow: none;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pill-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: rgba(0, 128, 96, 0.08);
|
|
color: var(--shopify-green);
|
|
padding: 6px 10px;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
letter-spacing: 0.01em;
|
|
border: 1px solid rgba(0, 128, 96, 0.14);
|
|
}
|
|
|
|
.plan-hint {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: #4b5563;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
margin-top: -12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
/* Header card to match builder page */
|
|
.apps-header {
|
|
background: #fff;
|
|
border: 1px solid var(--border, #e6e9ee);
|
|
border-radius: 16px;
|
|
padding: 18px 20px;
|
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.apps-header h1 {
|
|
margin: 0 0 6px 0;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.apps-header p {
|
|
margin: 0;
|
|
color: var(--muted, #6b7280);
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* Search box */
|
|
.search-box {
|
|
display: flex;
|
|
align-items: center;
|
|
background: #fff;
|
|
border: 1px solid var(--border, #e6e9ee);
|
|
border-radius: 12px;
|
|
padding: 8px 12px;
|
|
flex: 1;
|
|
max-width: 520px;
|
|
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
|
}
|
|
|
|
.search-box svg {
|
|
color: var(--muted, #9aa3ad);
|
|
flex: 0 0 auto;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.search-box input {
|
|
border: none;
|
|
outline: none;
|
|
background: transparent;
|
|
font-size: 14px;
|
|
width: 100%;
|
|
color: inherit;
|
|
}
|
|
|
|
.search-box input::placeholder {
|
|
color: var(--muted, #9aa3ad);
|
|
}
|
|
|
|
.search-box:focus-within {
|
|
border-color: rgba(0, 128, 96, 0.4);
|
|
box-shadow: 0 6px 20px rgba(0, 128, 96, 0.06);
|
|
}
|
|
|
|
/* Create button (primary) */
|
|
.create-app-btn {
|
|
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 12px 18px;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
box-shadow: 0 8px 24px rgba(0, 128, 96, 0.12);
|
|
}
|
|
|
|
.create-app-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 12px 32px rgba(0, 128, 96, 0.18);
|
|
}
|
|
|
|
/* Upload button - more prominent and conversion-focused */
|
|
.upload-app-btn {
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
/* slightly brighter */
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 12px 18px;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
box-shadow: 0 10px 28px rgba(16, 185, 129, 0.12);
|
|
}
|
|
|
|
.upload-app-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 18px 40px rgba(16, 185, 129, 0.18);
|
|
}
|
|
|
|
/* Subtle upgrade state: still clickable but visually hints that action will prompt upgrade */
|
|
.upload-app-btn.needs-upgrade {
|
|
opacity: 1;
|
|
/* keep prominent to encourage click */
|
|
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.08);
|
|
}
|
|
|
|
.upload-app-btn .upload-label {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Keep the small hint text but ensure it's subtle and readable */
|
|
#upload-hint {
|
|
color: #6b7280;
|
|
font-size: 13px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
|
|
/* App grid and cards */
|
|
.apps-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 24px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-action-btn.delete {
|
|
border-color: #f5c6cb;
|
|
color: #c92a2a;
|
|
background: #fff;
|
|
}
|
|
|
|
.app-badge.status-active {
|
|
background: var(--shopify-green-light);
|
|
color: var(--shopify-green);
|
|
padding: 6px 10px;
|
|
border-radius: 10px;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.app-date {
|
|
color: var(--muted, #6b7280);
|
|
font-size: 13px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.app-card {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
border: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.app-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
|
|
transform: scaleX(0);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.app-card:hover {
|
|
border-color: var(--shopify-green);
|
|
box-shadow: 0 8px 24px rgba(0, 128, 96, 0.12);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.app-card:hover::before {
|
|
transform: scaleX(1);
|
|
}
|
|
|
|
.app-icon {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 12px;
|
|
background: linear-gradient(135deg, var(--shopify-green-light), #d4f1e8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
margin-bottom: 16px;
|
|
color: var(--shopify-green);
|
|
}
|
|
|
|
.app-card h3 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin: 0 0 8px 0;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.app-meta {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.app-badge {
|
|
background: #f8f9fa;
|
|
color: #6c757d;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.app-badge.model {
|
|
background: linear-gradient(135deg, rgba(90, 49, 244, 0.1), rgba(139, 92, 246, 0.1));
|
|
color: var(--accent);
|
|
}
|
|
|
|
.app-badge.upload {
|
|
background: rgba(15, 23, 42, 0.08);
|
|
color: #0f172a;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.app-badge.status-active {
|
|
background: var(--shopify-green-light);
|
|
color: var(--shopify-green);
|
|
}
|
|
|
|
.app-date {
|
|
color: #adb5bd;
|
|
font-size: 13px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.app-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 16px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid #f1f3f5;
|
|
}
|
|
|
|
.app-action-btn {
|
|
padding: 8px 12px;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
color: #6c757d;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.app-action-btn:hover {
|
|
border-color: var(--shopify-green);
|
|
color: var(--shopify-green);
|
|
background: var(--shopify-green-light);
|
|
}
|
|
|
|
.app-action-btn.delete {
|
|
color: #dc3545;
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
.app-action-btn.delete:hover {
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.app-action-btn.primary {
|
|
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
|
|
color: #fff;
|
|
border: none;
|
|
box-shadow: 0 8px 24px rgba(0, 128, 96, 0.08);
|
|
}
|
|
|
|
.app-action-btn.primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 14px 36px rgba(0, 128, 96, 0.12);
|
|
}
|
|
|
|
.app-action-btn.outline {
|
|
background: transparent;
|
|
border: 1px solid #dee2e6;
|
|
color: #374151;
|
|
}
|
|
|
|
.app-action-btn.outline:hover {
|
|
background: var(--shopify-green-light);
|
|
color: var(--shopify-green);
|
|
border-color: rgba(0, 128, 96, 0.16);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 80px 20px;
|
|
background: white;
|
|
border-radius: 16px;
|
|
border: 2px dashed #dee2e6;
|
|
}
|
|
|
|
.empty-state-icon {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
background: var(--shopify-green-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 36px;
|
|
margin: 0 auto 24px;
|
|
color: var(--shopify-green);
|
|
}
|
|
|
|
.empty-state h2 {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin: 0 0 12px 0;
|
|
}
|
|
|
|
.empty-state p {
|
|
color: #6c757d;
|
|
margin: 0 0 24px 0;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.nav-bar {
|
|
background: white;
|
|
border-bottom: 1px solid #e9ecef;
|
|
padding: 16px 24px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.nav-content {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.brand-mark {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.brand-text {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.nav-links {
|
|
display: flex;
|
|
gap: 24px;
|
|
align-items: center;
|
|
}
|
|
|
|
.user-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 10px;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 12px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.user-chip:hover {
|
|
border-color: var(--shopify-green);
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.user-menu-container {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.user-menu-popup {
|
|
position: absolute;
|
|
top: calc(100% + 8px);
|
|
right: 0;
|
|
background: #fff;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
z-index: 10000;
|
|
min-width: 180px;
|
|
display: none;
|
|
padding: 6px;
|
|
animation: menuFadeIn 0.2s ease-out;
|
|
}
|
|
|
|
@keyframes menuFadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.user-menu-popup.active {
|
|
display: block;
|
|
}
|
|
|
|
.user-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
text-decoration: none;
|
|
color: #495057;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
border-radius: 8px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.user-menu-item:hover {
|
|
background: #f8f9fa;
|
|
color: var(--shopify-green);
|
|
}
|
|
|
|
.user-menu-item svg {
|
|
color: #6c757d;
|
|
}
|
|
|
|
.user-menu-item:hover svg {
|
|
color: var(--shopify-green);
|
|
}
|
|
|
|
.user-chip-avatar {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 999px;
|
|
background: var(--shopify-green-light);
|
|
color: var(--shopify-green);
|
|
display: grid;
|
|
place-items: center;
|
|
font-weight: 700;
|
|
letter-spacing: 0.02em;
|
|
text-transform: uppercase;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.user-chip-status {
|
|
font-size: 11px;
|
|
color: #6c757d;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.user-chip-email {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #1a1a1a;
|
|
max-width: 160px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* Mobile portrait: show only avatar to save space */
|
|
@media (max-width: 640px) and (orientation: portrait) {
|
|
|
|
.user-chip .user-chip-status,
|
|
.user-chip .user-chip-email {
|
|
display: none !important;
|
|
}
|
|
|
|
.user-chip {
|
|
padding: 6px 8px;
|
|
}
|
|
|
|
.user-chip-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
}
|
|
|
|
.nav-link {
|
|
color: #6c757d;
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
color: var(--shopify-green);
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 4px solid #f1f3f5;
|
|
border-top-color: var(--shopify-green);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Modal Styles */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(4px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.modal-container {
|
|
background: white;
|
|
border-radius: 16px;
|
|
max-width: 480px;
|
|
width: 90%;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
transform: scale(0.95) translateY(20px);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.modal-overlay.active .modal-container {
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 24px;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.modal-header h2 {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
margin: 0;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
.upload-dropzone {
|
|
border: 2px dashed #d0d7de;
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.upload-dropzone strong {
|
|
color: #0f172a;
|
|
}
|
|
|
|
.upload-dropzone small {
|
|
color: #6c757d;
|
|
display: block;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.modal-label {
|
|
display: block;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #6c757d;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.modal-input {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 12px;
|
|
font-size: 15px;
|
|
color: #1a1a1a;
|
|
transition: all 0.2s;
|
|
outline: none;
|
|
}
|
|
|
|
.modal-input:focus {
|
|
border-color: var(--shopify-green);
|
|
box-shadow: 0 0 0 3px var(--shopify-green-light);
|
|
}
|
|
|
|
.modal-input::placeholder {
|
|
color: #adb5bd;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 24px;
|
|
border-top: 1px solid #e9ecef;
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.modal-btn {
|
|
padding: 12px 24px;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
}
|
|
|
|
.modal-btn-secondary {
|
|
background: #f8f9fa;
|
|
color: #6c757d;
|
|
border: 2px solid #dee2e6;
|
|
}
|
|
|
|
.modal-btn-secondary:hover {
|
|
background: #e9ecef;
|
|
border-color: #ced4da;
|
|
}
|
|
|
|
.modal-btn-primary {
|
|
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
|
|
color: white;
|
|
box-shadow: 0 4px 12px rgba(0, 128, 96, 0.15);
|
|
}
|
|
|
|
.modal-btn-primary:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 16px rgba(0, 128, 96, 0.25);
|
|
}
|
|
|
|
.modal-btn-primary:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
/* Templates Modal Specific */
|
|
.templates-modal-container {
|
|
max-width: 1000px;
|
|
width: 95%;
|
|
}
|
|
|
|
.templates-grid-modal {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
}
|
|
|
|
.template-card-mini {
|
|
background: white;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
border: 1px solid #e9ecef;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.template-card-mini:hover {
|
|
border-color: var(--shopify-green);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.template-card-mini .card-image {
|
|
height: 140px;
|
|
background: #f1f3f5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 32px;
|
|
}
|
|
|
|
.template-card-mini .card-body {
|
|
padding: 16px;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.template-card-mini .badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
background: var(--shopify-green-light);
|
|
color: var(--shopify-green);
|
|
margin-bottom: 8px;
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.template-card-mini .card-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
margin: 0 0 8px 0;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.template-card-mini .card-desc {
|
|
font-size: 13px;
|
|
color: #6c757d;
|
|
margin-bottom: 16px;
|
|
line-height: 1.4;
|
|
flex: 1;
|
|
}
|
|
|
|
.template-card-mini .use-template-btn {
|
|
background: #1a1a1a;
|
|
color: white;
|
|
text-align: center;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
transition: background 0.2s;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.template-card-mini .use-template-btn:hover {
|
|
background: var(--shopify-green);
|
|
}
|
|
</style>
|
|
|
|
<!-- PostHog Analytics -->
|
|
<script src="/posthog.js"></script>
|
|
</head>
|
|
|
|
<body>
|
|
<nav class="nav-bar">
|
|
<div class="nav-content">
|
|
<a href="/" class="brand">
|
|
<img src="/assets/Plugin.png" alt="Plugin Compass" style="width: 32px; height: 32px; border-radius: 8px;">
|
|
<span class="brand-text">Plugin Compass</span>
|
|
</a>
|
|
<div class="nav-links">
|
|
<div class="user-menu-container">
|
|
<div class="user-chip" id="apps-user-chip" title="Account & settings">
|
|
<div class="user-chip-avatar" id="apps-user-avatar">?</div>
|
|
<div style="display:flex; flex-direction:column; gap:2px; min-width:0;">
|
|
<span class="user-chip-status">Signed in</span>
|
|
<span class="user-chip-email" id="apps-user-email">Checking account…</span>
|
|
</div>
|
|
</div>
|
|
<div class="user-menu-popup" id="user-menu-popup">
|
|
<a href="/settings" class="user-menu-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="3"></circle>
|
|
<path
|
|
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
|
|
</path>
|
|
</svg>
|
|
Settings
|
|
</a>
|
|
<a href="/feature-requests" class="user-menu-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
|
</svg>
|
|
Feature Requests
|
|
</a>
|
|
<a href="/topup" class="user-menu-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"></path>
|
|
<path d="M12 18V6"></path>
|
|
</svg>
|
|
Tokens Top-up
|
|
</a>
|
|
<div style="margin: 4px 0; border-top: 1px solid #f1f3f5;"></div>
|
|
<a href="#" class="user-menu-item" id="logout-link">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
<polyline points="16 17 21 12 16 7"></polyline>
|
|
<line x1="21" y1="12" x2="9" y2="12"></line>
|
|
</svg>
|
|
Logout
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="apps-container">
|
|
<div class="apps-header">
|
|
<h1>My Wordpress Plugins</h1>
|
|
<p>Build, manage, and deploy your Wordpress Plugins powered by AI</p>
|
|
</div>
|
|
|
|
<div class="apps-actions">
|
|
<div class="search-box">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round" style="color:#adb5bd; margin-right:8px;">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
</svg>
|
|
<input type="text" id="search-input" placeholder="Search apps..." />
|
|
</div>
|
|
<div class="action-buttons">
|
|
<button class="upload-app-btn" id="upload-app-btn" aria-label="Upload ZIP">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<polyline points="16 16 12 12 8 16"></polyline>
|
|
<line x1="12" y1="12" x2="12" y2="21"></line>
|
|
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 4 16.3"></path>
|
|
</svg>
|
|
<span class="upload-label">Upload ZIP</span>
|
|
</button>
|
|
<button class="create-app-btn" id="create-new-app">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
</svg>
|
|
Create New App
|
|
</button>
|
|
<button id="browse-templates-btn" class="create-app-btn"
|
|
style="background: white; color: var(--shopify-green); border: 1px solid var(--shopify-green); margin-left: 10px;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
</svg>
|
|
Browse Templates
|
|
</button>
|
|
<a href="/upgrade?source=apps_page" class="create-app-btn upgrade-page-btn"
|
|
style="background: linear-gradient(135deg, #008060, #004c3f); color: white; border: none; margin-left: 10px;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
|
|
<path d="M2 17l10 5 10-5"></path>
|
|
<path d="M2 12l10 5 10-5"></path>
|
|
</svg>
|
|
Upgrade Plan
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="apps-content">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<p>Loading your apps...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create App Modal -->
|
|
<div class="modal-overlay" id="create-app-modal">
|
|
<div class="modal-container">
|
|
<div class="modal-header">
|
|
<h2 style="display:flex; align-items:center; gap:10px;">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
</svg>
|
|
Create New App
|
|
</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<label for="app-name-input" class="modal-label">App Name</label>
|
|
<input type="text" id="app-name-input" class="modal-input" placeholder="e.g., Product Discount Manager"
|
|
maxlength="100" />
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="modal-btn modal-btn-secondary" id="cancel-create-app">Cancel</button>
|
|
<button class="modal-btn modal-btn-primary" id="confirm-create-app">Create App</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload App Modal -->
|
|
<div class="modal-overlay" id="upload-app-modal">
|
|
<div class="modal-container">
|
|
<div class="modal-header">
|
|
<h2 style="display:flex; align-items:center; gap:10px;">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="16 16 12 12 8 16"></polyline>
|
|
<line x1="12" y1="12" x2="12" y2="21"></line>
|
|
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 4 16.3"></path>
|
|
</svg>
|
|
Upload Existing App (ZIP)
|
|
</h2>
|
|
</div>
|
|
<div class="modal-body" id="upload-body">
|
|
<label for="upload-app-name-input" class="modal-label">Plugin Name</label>
|
|
<input type="text" id="upload-app-name-input" class="modal-input" placeholder="e.g., My Custom Plugin"
|
|
maxlength="100" />
|
|
<div class="upload-dropzone" style="margin-top:12px;">
|
|
<div>
|
|
<strong>Select a ZIP file</strong>
|
|
<small>We will create a new app using your uploaded files.</small>
|
|
</div>
|
|
<label class="modal-btn modal-btn-primary" style="margin:0; cursor:pointer;">
|
|
Choose File
|
|
<input type="file" id="upload-input" accept=".zip" style="display:none;">
|
|
</label>
|
|
</div>
|
|
<div style="margin-top:12px; font-size:13px; color:#6b7280;" id="upload-hint">
|
|
Uploads available on Business and Enterprise plans. Max size 25MB.
|
|
</div>
|
|
<div style="margin-top:10px; display:none;" id="upload-selected"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="modal-btn modal-btn-secondary" id="cancel-upload-app">Cancel</button>
|
|
<button class="modal-btn modal-btn-primary" id="confirm-upload-app" disabled>Upload & Open</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- App Notice Modal -->
|
|
<div class="modal-overlay" id="app-notice-modal">
|
|
<div class="modal-container">
|
|
<div class="modal-header">
|
|
<h2 id="app-notice-title"></h2>
|
|
</div>
|
|
<div class="modal-body" id="app-notice-body" style="color:#475569; font-size:14px;"></div>
|
|
<div class="modal-footer" id="app-notice-footer">
|
|
<button class="modal-btn modal-btn-secondary" id="app-notice-secondary">Close</button>
|
|
<button class="modal-btn modal-btn-primary" id="app-notice-primary">OK</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Onboarding Modal -->
|
|
<div id="onboarding-modal" class="onboarding-modal"
|
|
style="display:none; position:fixed; inset:0; background:transparent; z-index:10001; transition: all 0.3s ease;">
|
|
<div id="onboarding-container" class="onboarding-container"
|
|
style="background:#fff; border-radius:12px; width:280px; box-shadow:0 20px 50px rgba(0,0,0,0.2); position:fixed; overflow:visible; display:flex; flex-direction:column; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);">
|
|
|
|
<!-- Tooltip Pointer -->
|
|
<div id="onboarding-pointer" style="position:absolute; width:16px; height:16px; background:#fff; transform:rotate(45deg); z-index:-1; display:none;"></div>
|
|
|
|
<button id="onboarding-exit"
|
|
style="position:absolute; top:8px; right:8px; border:none; background:rgba(255,255,255,0.8); color:#9ca3af; cursor:pointer; font-size:14px; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; z-index:100; transition:all 0.2s ease;">
|
|
✕
|
|
</button>
|
|
|
|
<div id="onboarding-content" class="onboarding-content" style="padding:16px 18px 12px;">
|
|
<div id="onboarding-step-1" class="onboarding-step">
|
|
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Welcome!</h2>
|
|
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
|
|
This is your workspace. Here you can manage all your WordPress projects in one place.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="onboarding-step-2" class="onboarding-step" style="display:none;">
|
|
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">New Projects</h2>
|
|
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
|
|
Start from scratch by describing your idea. Our AI will guide you through the planning phase.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="onboarding-step-3" class="onboarding-step" style="display:none;">
|
|
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Upload Plugins</h2>
|
|
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
|
|
Have an existing project or plugin? Upload them here to edit them with plugin compass.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="onboarding-step-4" class="onboarding-step" style="display:none;">
|
|
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Templates</h2>
|
|
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
|
|
Browse our library of pre-built templates for common WordPress needs to jumpstart development.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="onboarding-step-5" class="onboarding-step" style="display:none;">
|
|
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Ready!</h2>
|
|
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
|
|
You're all set to begin. Build your first custom plugin today and see the power of AI.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="onboarding-footer" style="padding:0 18px 14px; display:flex; align-items:center; justify-content:space-between;">
|
|
<button id="onboarding-back"
|
|
style="padding:6px 0; font-size:13px; font-weight:600; border:none; background:transparent; color:#94a3b8; cursor:pointer; display:none;">
|
|
Back
|
|
</button>
|
|
<div id="onboarding-step-counter" style="font-size:12px; font-weight:600; color:#94a3b8; flex:1; text-align:center;">
|
|
1 of 5
|
|
</div>
|
|
<button id="onboarding-next"
|
|
style="padding:6px 12px; font-size:13px; font-weight:700; border-radius:8px; border:none; background:#008060; color:#fff; cursor:pointer; transition:all 0.2s ease;">
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Templates Modal -->
|
|
<div class="modal-overlay" id="templates-modal">
|
|
<div class="modal-container templates-modal-container">
|
|
<div class="modal-header">
|
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
<h2 style="display:flex; align-items:center; gap:10px;">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
</svg>
|
|
Choose a Template
|
|
</h2>
|
|
<button class="modal-btn modal-btn-secondary" id="close-templates-modal" style="padding: 8px;">✕</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="templates-grid-modal" class="templates-grid-modal">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<p>Loading templates...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const state = {
|
|
userId: null,
|
|
apps: [],
|
|
filteredApps: [],
|
|
plan: 'starter',
|
|
canUpload: false,
|
|
uploadFile: null,
|
|
uploadLimit: 25_000_000,
|
|
};
|
|
const MAX_UPLOAD_BYTES = 25_000_000; // Mirror server MAX_UPLOAD_ZIP_SIZE default (25MB)
|
|
|
|
// API function for making requests to the backend
|
|
async function api(endpoint, options = {}) {
|
|
const defaultOptions = {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
|
|
// Add user authentication header if available
|
|
if (state.userId) {
|
|
defaultOptions.headers['X-User-Id'] = state.userId;
|
|
}
|
|
|
|
const mergedOptions = {
|
|
...defaultOptions,
|
|
...options,
|
|
headers: {
|
|
...defaultOptions.headers,
|
|
...options.headers,
|
|
},
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(endpoint, mergedOptions);
|
|
|
|
// Handle non-2xx responses
|
|
if (!response.ok) {
|
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
|
|
try {
|
|
const errorData = await response.json();
|
|
if (errorData.error) {
|
|
errorMessage = errorData.error;
|
|
}
|
|
} catch (e) {
|
|
// If JSON parsing fails, use the status text
|
|
}
|
|
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
// Try to parse JSON response
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType && contentType.includes('application/json')) {
|
|
return await response.json();
|
|
}
|
|
|
|
return await response.text();
|
|
} catch (error) {
|
|
console.error('API request failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const userChip = document.getElementById('apps-user-chip');
|
|
const userChipEmail = document.getElementById('apps-user-email');
|
|
const userChipAvatar = document.getElementById('apps-user-avatar');
|
|
const userMenuPopup = document.getElementById('user-menu-popup');
|
|
const logoutLink = document.getElementById('logout-link');
|
|
|
|
if (userChip && userMenuPopup) {
|
|
userChip.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
userMenuPopup.classList.toggle('active');
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!userMenuPopup.contains(e.target) && !userChip.contains(e.target)) {
|
|
userMenuPopup.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (logoutLink) {
|
|
logoutLink.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const response = await fetch('/api/logout', { method: 'POST' });
|
|
if (response.ok) {
|
|
localStorage.removeItem('shopify_ai_user');
|
|
localStorage.removeItem('wordpress_plugin_ai_user');
|
|
localStorage.removeItem('plugin_compass_onboarding_completed');
|
|
window.location.href = '/login';
|
|
}
|
|
} catch (err) {
|
|
console.error('Logout failed:', err);
|
|
window.location.href = '/login';
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Deterministic hash used to derive a stable account id from email for client-side display (used by computeAccountId later in this script) */
|
|
function cyrb53(str, seed = 0) {
|
|
let h1 = 0xdeadbeef ^ seed;
|
|
let h2 = 0x41c6ce57 ^ seed;
|
|
// Constants derive from the original cyrb53 implementation to balance speed and distribution
|
|
for (let i = 0, ch; i < str.length; i++) {
|
|
ch = str.charCodeAt(i);
|
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
}
|
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
}
|
|
|
|
function computeAccountId(email) {
|
|
const normalized = (email || '').trim().toLowerCase();
|
|
if (!normalized) return '';
|
|
const hash = cyrb53(normalized);
|
|
return `acct-${hash.toString(16)}`;
|
|
}
|
|
|
|
function resolveUserId() {
|
|
try {
|
|
const keys = ['shopify_ai_user', 'wordpress_plugin_ai_user'];
|
|
for (const key of keys) {
|
|
const stored = localStorage.getItem(key);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
if (parsed && parsed.email) {
|
|
const accountId = parsed.accountId || computeAccountId(parsed.email);
|
|
if (accountId && (!parsed.accountId || parsed.accountId !== accountId)) {
|
|
try { localStorage.setItem(key, JSON.stringify({ ...parsed, accountId })); } catch (_) { }
|
|
}
|
|
return accountId;
|
|
}
|
|
}
|
|
}
|
|
} catch (_) { }
|
|
return '';
|
|
}
|
|
|
|
const MOBILE_HEADER_MQ = window.matchMedia('(max-width: 640px) and (orientation: portrait)');
|
|
|
|
function updateUserChipForMobile(isMobile) {
|
|
if (!userChip) return;
|
|
const emailEl = userChipEmail;
|
|
const statusEl = userChip.querySelector('.user-chip-status');
|
|
if (emailEl) emailEl.setAttribute('aria-hidden', isMobile ? 'true' : 'false');
|
|
if (statusEl) statusEl.setAttribute('aria-hidden', isMobile ? 'true' : 'false');
|
|
if (isMobile) {
|
|
userChip.setAttribute('aria-label', 'Account');
|
|
document.body.classList.add('mobile-portrait');
|
|
} else {
|
|
const email = (userChipEmail && userChipEmail.textContent) || readLocalEmail();
|
|
userChip.setAttribute('aria-label', email ? `Signed in as ${email}` : 'Account settings');
|
|
document.body.classList.remove('mobile-portrait');
|
|
}
|
|
|
|
// Lightweight tracking: push to dataLayer if available and make a best-effort fire-and-forget POST
|
|
try {
|
|
if (window.dataLayer && typeof window.dataLayer.push === 'function') {
|
|
window.dataLayer.push({ event: 'mobilePortraitHeader', mobilePortrait: isMobile });
|
|
}
|
|
fetch('/api/track', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ event: 'mobilePortraitHeader', mobilePortrait: isMobile }),
|
|
keepalive: true,
|
|
}).catch(() => { });
|
|
} catch (_) { }
|
|
}
|
|
|
|
function setUserChipEmail(email) {
|
|
const safe = (email || '').trim();
|
|
if (userChipEmail) userChipEmail.textContent = safe || 'Guest';
|
|
if (userChipAvatar) userChipAvatar.textContent = safe ? safe.charAt(0).toUpperCase() : '?';
|
|
// apply appropriate aria/visibility based on current media state
|
|
try {
|
|
const isMobile = MOBILE_HEADER_MQ.matches;
|
|
updateUserChipForMobile(isMobile);
|
|
} catch (_) {
|
|
if (userChip) {
|
|
userChip.setAttribute('aria-label', safe ? `Signed in as ${safe}` : 'Account settings');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wire up media query changes
|
|
if (MOBILE_HEADER_MQ.addEventListener) {
|
|
MOBILE_HEADER_MQ.addEventListener('change', (e) => updateUserChipForMobile(e.matches));
|
|
} else if (MOBILE_HEADER_MQ.addListener) {
|
|
MOBILE_HEADER_MQ.addListener((e) => updateUserChipForMobile(e.matches));
|
|
}
|
|
// initialize
|
|
updateUserChipForMobile(MOBILE_HEADER_MQ.matches);
|
|
|
|
function syncUploadUi() {
|
|
const uploadBtn = document.getElementById('upload-app-btn');
|
|
const uploadHint = document.getElementById('upload-hint');
|
|
if (!uploadBtn) return;
|
|
const limitMb = Math.ceil((state.uploadLimit || MAX_UPLOAD_BYTES) / (1024 * 1024));
|
|
if (state.canUpload) {
|
|
uploadBtn.classList.remove('needs-upgrade');
|
|
uploadBtn.removeAttribute('aria-disabled');
|
|
if (uploadHint) uploadHint.textContent = `Upload a ZIP to create a new app instantly (max ${limitMb}MB).`;
|
|
} else {
|
|
// keep the CTA text the same (no "(paid)") so the button remains inviting,
|
|
// but add a class so we can style it subtly and show the in-app upgrade hint
|
|
uploadBtn.classList.add('needs-upgrade');
|
|
uploadBtn.setAttribute('aria-disabled', 'false'); // still clickable
|
|
if (uploadHint) uploadHint.textContent = `Upgrade to Business or Enterprise to upload existing apps (max ${limitMb}MB).`;
|
|
}
|
|
}
|
|
|
|
function readLocalEmail() {
|
|
const keys = ['shopify_ai_user', 'wordpress_plugin_ai_user'];
|
|
for (const key of keys) {
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) continue;
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed?.email) return parsed.email;
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
return '';
|
|
}
|
|
|
|
async function loadUserChip() {
|
|
let email = '';
|
|
try {
|
|
const resp = await fetch('/api/account', { credentials: 'same-origin' });
|
|
if (resp.ok) {
|
|
const data = await resp.json().catch(() => ({}));
|
|
email = data?.account?.email || '';
|
|
if (data?.account?.plan) {
|
|
state.plan = data.account.plan;
|
|
state.canUpload = ['business', 'enterprise'].includes(state.plan.toLowerCase());
|
|
|
|
// Hide upgrade button for enterprise users
|
|
if (state.plan.toLowerCase() === 'enterprise') {
|
|
const upgradeBtn = document.querySelector('.upgrade-page-btn');
|
|
if (upgradeBtn) {
|
|
upgradeBtn.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
if (data?.account?.limits?.uploadZipBytes) {
|
|
state.uploadLimit = Number(data.account.limits.uploadZipBytes) || MAX_UPLOAD_BYTES;
|
|
} else {
|
|
state.uploadLimit = MAX_UPLOAD_BYTES;
|
|
}
|
|
}
|
|
} catch (_) { /* ignore */ }
|
|
|
|
if (!email) email = readLocalEmail();
|
|
setUserChipEmail(email);
|
|
}
|
|
|
|
state.userId = resolveUserId();
|
|
if (!state.userId) {
|
|
const next = encodeURIComponent(window.location.pathname + window.location.search);
|
|
window.location.href = `/login?next=${next}`;
|
|
}
|
|
try {
|
|
document.cookie = `chat_user=${encodeURIComponent(state.userId)}; path=/; SameSite=Lax`;
|
|
} catch (_) { }
|
|
loadUserChip().finally(syncUploadUi);
|
|
|
|
async function api(path, options = {}) {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-User-Id': state.userId,
|
|
...(options.headers || {}),
|
|
};
|
|
const res = await fetch(path, { headers, ...options });
|
|
const text = await res.text();
|
|
const json = text ? JSON.parse(text) : {};
|
|
if (!res.ok) {
|
|
throw new Error(json.error || res.statusText);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
function formatDate(timestamp) {
|
|
if (!timestamp) return 'Unknown';
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
|
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
}
|
|
|
|
function getAppIcon(title) {
|
|
// Use consistent colors for initials
|
|
const colors = [
|
|
'linear-gradient(135deg, #FF6B6B 0%, #EE5253 100%)',
|
|
'linear-gradient(135deg, #4834d4 0%, #686de0 100%)',
|
|
'linear-gradient(135deg, #6ab04c 0%, #badc58 100%)',
|
|
'linear-gradient(135deg, #f0932b 0%, #ffbe76 100%)',
|
|
'linear-gradient(135deg, #22a6b3 0%, #7ed6df 100%)'
|
|
];
|
|
const char = (title || 'A').charAt(0).toUpperCase();
|
|
const hash = char.charCodeAt(0) % colors.length;
|
|
return `<div style="width:100%; height:100%; display:flex; align-items:center; justify-content:center; color:white; font-weight:700; font-size:24px; background:${colors[hash]}; border-radius:12px;">${char}</div>`;
|
|
}
|
|
|
|
function renderApps() {
|
|
const container = document.getElementById('apps-content');
|
|
|
|
if (state.filteredApps.length === 0 && state.apps.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">
|
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
|
</div>
|
|
<h2>No apps yet</h2>
|
|
<p>Create your first Wordpress Plugin and let AI do the heavy lifting</p>
|
|
<button class="create-app-btn" onclick="createNewApp()">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
|
Create Your First App
|
|
</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (state.filteredApps.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">
|
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
|
</div>
|
|
<h2>No apps found</h2>
|
|
<p>Try adjusting your search criteria</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const grid = document.createElement('div');
|
|
grid.className = 'apps-grid';
|
|
|
|
state.filteredApps.forEach(app => {
|
|
const card = document.createElement('div');
|
|
card.className = 'app-card';
|
|
card.onclick = (e) => {
|
|
if (!e.target.classList.contains('app-action-btn')) {
|
|
openApp(app.id);
|
|
}
|
|
};
|
|
|
|
const status = app.pending && app.pending > 0 ? 'Building' : 'Ready';
|
|
const icon = getAppIcon(app.title);
|
|
const uploadBadge = app.source === 'upload';
|
|
const chatCountBadge = app.chatCount > 1 ? `<span class="app-badge">${app.chatCount} chats</span>` : '';
|
|
|
|
card.innerHTML = `
|
|
<div class="app-icon" style="padding:0; overflow:hidden;">${icon}</div>
|
|
<h3>${app.title || 'Untitled App'}</h3>
|
|
<div class="app-meta">
|
|
<span class="app-badge ${status === 'Ready' ? 'status-active' : ''}">${status}</span>
|
|
${uploadBadge ? '<span class="app-badge upload">Uploaded</span>' : ''}
|
|
${chatCountBadge}
|
|
</div>
|
|
<div class="app-date">Created ${formatDate(app.createdAt)}</div>
|
|
<div class="app-actions">
|
|
<button class="app-action-btn primary" onclick="event.stopPropagation(); openApp('${app.id}')">
|
|
Open
|
|
</button>
|
|
<button class="app-action-btn outline" onclick="event.stopPropagation(); exportApp('${app.id}')">
|
|
Export
|
|
</button>
|
|
<button class="app-action-btn delete" onclick="event.stopPropagation(); deleteApp('${app.id}')">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
grid.appendChild(card);
|
|
});
|
|
|
|
container.innerHTML = '';
|
|
container.appendChild(grid);
|
|
}
|
|
|
|
|
|
function filterApps(searchTerm) {
|
|
const term = searchTerm.toLowerCase().trim();
|
|
if (!term) {
|
|
state.filteredApps = [...state.apps];
|
|
} else {
|
|
state.filteredApps = state.apps.filter(app => {
|
|
const title = (app.title || '').toLowerCase();
|
|
const model = (app.model || '').toLowerCase();
|
|
return title.includes(term) || model.includes(term);
|
|
});
|
|
}
|
|
renderApps();
|
|
}
|
|
|
|
async function loadApps() {
|
|
try {
|
|
const data = await api('/api/sessions');
|
|
const sessions = data.sessions || [];
|
|
|
|
// Group sessions by appId to display apps (not individual chats)
|
|
const timestampCache = new Map();
|
|
let fallbackTimestamp = null;
|
|
sessions.forEach((session) => {
|
|
const value = session.updatedAt || session.createdAt || 0;
|
|
const ts = Date.parse(value);
|
|
if (!Number.isNaN(ts)) {
|
|
timestampCache.set(session.id, ts);
|
|
fallbackTimestamp = fallbackTimestamp === null ? ts : Math.max(fallbackTimestamp, ts);
|
|
}
|
|
});
|
|
if (fallbackTimestamp === null) {
|
|
fallbackTimestamp = Date.now();
|
|
}
|
|
sessions.forEach((session) => {
|
|
if (!timestampCache.has(session.id)) {
|
|
timestampCache.set(session.id, fallbackTimestamp);
|
|
}
|
|
});
|
|
const getSessionTimestamp = (session) => (
|
|
timestampCache.has(session.id) ? timestampCache.get(session.id) : fallbackTimestamp
|
|
);
|
|
const grouped = new Map();
|
|
sessions.forEach((session) => {
|
|
const key = session.appId || `single-${session.id}`;
|
|
const existing = grouped.get(key);
|
|
if (!existing) {
|
|
grouped.set(key, session);
|
|
return;
|
|
}
|
|
if (getSessionTimestamp(session) > getSessionTimestamp(existing)) {
|
|
grouped.set(key, session);
|
|
}
|
|
});
|
|
state.apps = Array.from(grouped.values()).sort((a, b) => {
|
|
return getSessionTimestamp(b) - getSessionTimestamp(a);
|
|
});
|
|
|
|
// Enhance apps with chatCount, source, and pending aggregation
|
|
const appsMap = new Map();
|
|
for (const session of sessions) {
|
|
const appId = session.appId || session.id;
|
|
if (!appsMap.has(appId)) {
|
|
appsMap.set(appId, {
|
|
id: session.id,
|
|
appId: appId,
|
|
title: session.title || 'Untitled App',
|
|
createdAt: session.createdAt,
|
|
updatedAt: session.updatedAt,
|
|
model: session.model,
|
|
pending: session.pending || 0,
|
|
source: session.source,
|
|
chatCount: 1,
|
|
});
|
|
} else {
|
|
const app = appsMap.get(appId);
|
|
app.chatCount++;
|
|
app.pending += (session.pending || 0);
|
|
const existingDate = new Date(app.updatedAt || app.createdAt || 0);
|
|
const sessionDate = new Date(session.updatedAt || session.createdAt || 0);
|
|
if (sessionDate > existingDate) {
|
|
app.id = session.id;
|
|
app.updatedAt = session.updatedAt;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge enhanced data into state.apps
|
|
state.apps = state.apps.map(app => {
|
|
const enhanced = appsMap.get(app.appId || app.id);
|
|
if (enhanced) {
|
|
return { ...app, chatCount: enhanced.chatCount, pending: enhanced.pending, source: enhanced.source || app.source };
|
|
}
|
|
return app;
|
|
});
|
|
|
|
state.filteredApps = [...state.apps];
|
|
renderApps();
|
|
} catch (error) {
|
|
document.getElementById('apps-content').innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">⚠️</div>
|
|
<h2>Error loading apps</h2>
|
|
<p>${error.message}</p>
|
|
<button class="create-app-btn" onclick="loadApps()">
|
|
Retry
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function openCreateAppModal() {
|
|
const modal = document.getElementById('create-app-modal');
|
|
const input = document.getElementById('app-name-input');
|
|
input.value = '';
|
|
modal.classList.add('active');
|
|
input.focus();
|
|
}
|
|
|
|
function closeCreateAppModal() {
|
|
const modal = document.getElementById('create-app-modal');
|
|
modal.classList.remove('active');
|
|
state.pendingTemplateId = null;
|
|
}
|
|
|
|
function openUploadModal() {
|
|
if (!state.canUpload) {
|
|
showUpgradePrompt('Uploading existing apps is available on Business and Enterprise plans.');
|
|
return;
|
|
}
|
|
const modal = document.getElementById('upload-app-modal');
|
|
const status = document.getElementById('upload-selected');
|
|
const appNameInput = document.getElementById('upload-app-name-input');
|
|
state.uploadFile = null;
|
|
if (status) status.style.display = 'none';
|
|
if (appNameInput) appNameInput.value = '';
|
|
document.getElementById('confirm-upload-app').disabled = true;
|
|
modal.classList.add('active');
|
|
}
|
|
|
|
function closeUploadModal() {
|
|
const modal = document.getElementById('upload-app-modal');
|
|
const appNameInput = document.getElementById('upload-app-name-input');
|
|
modal.classList.remove('active');
|
|
state.uploadFile = null;
|
|
if (appNameInput) appNameInput.value = '';
|
|
}
|
|
|
|
function openTemplatesModal() {
|
|
const modal = document.getElementById('templates-modal');
|
|
modal.classList.add('active');
|
|
loadTemplatesModal();
|
|
}
|
|
|
|
function closeTemplatesModal() {
|
|
const modal = document.getElementById('templates-modal');
|
|
modal.classList.remove('active');
|
|
}
|
|
|
|
async function loadTemplatesModal() {
|
|
const grid = document.getElementById('templates-grid-modal');
|
|
try {
|
|
const res = await fetch('/api/templates');
|
|
const data = await res.json();
|
|
|
|
if (!data.templates || !data.templates.length) {
|
|
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px;">No templates found.</div>';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = data.templates.map(t => `
|
|
<div class="template-card-mini">
|
|
<div class="card-image">
|
|
${t.image && !t.image.includes('placeholder') ? `<img src="${t.image}" style="width:100%; height:100%; object-fit:cover;">` : '🧩'}
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="badge">${t.category || 'General'}</div>
|
|
<h3 class="card-title">${t.name}</h3>
|
|
<p class="card-desc">${t.description}</p>
|
|
<button onclick="handleTemplateSelect('${t.id}'); closeTemplatesModal();" class="use-template-btn">Use Template</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
grid.innerHTML = '<div style="color:red; text-align:center; padding: 40px;">Failed to load templates</div>';
|
|
}
|
|
}
|
|
|
|
// Generic in-app notice modal (replaces native alert/confirm)
|
|
function isRestrictedError(msg) {
|
|
if (!msg) return false;
|
|
const l = (String(msg) || '').toLowerCase();
|
|
return /\b(limit|upgrade|allowance|quota|paid|plan|business|enterprise)\b/.test(l);
|
|
}
|
|
|
|
function showAppNotice({ title = 'Notice', message = '', primaryText = 'OK', primaryHref, onPrimary } = {}) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.getElementById('app-notice-modal');
|
|
const tit = document.getElementById('app-notice-title');
|
|
const body = document.getElementById('app-notice-body');
|
|
const primary = document.getElementById('app-notice-primary');
|
|
const secondary = document.getElementById('app-notice-secondary');
|
|
|
|
tit.textContent = title;
|
|
body.innerHTML = message || '';
|
|
primary.textContent = primaryText || 'OK';
|
|
secondary.textContent = 'Close';
|
|
|
|
// cleanup handlers
|
|
primary.onclick = null;
|
|
secondary.onclick = null;
|
|
const close = (result) => {
|
|
overlay.classList.remove('active');
|
|
primary.onclick = null;
|
|
secondary.onclick = null;
|
|
overlay.onclick = null;
|
|
resolve(result);
|
|
};
|
|
|
|
secondary.onclick = () => close(false);
|
|
primary.onclick = () => {
|
|
if (primaryHref) {
|
|
window.location.href = primaryHref;
|
|
close(true);
|
|
return;
|
|
}
|
|
if (onPrimary) {
|
|
try { onPrimary(); } catch (_) { }
|
|
close(true);
|
|
return;
|
|
}
|
|
close(true);
|
|
};
|
|
|
|
overlay.onclick = (e) => { if (e.target === overlay) close(false); };
|
|
|
|
overlay.classList.add('active');
|
|
});
|
|
}
|
|
|
|
function showUpgradePrompt(message) {
|
|
if (state.plan && state.plan.toLowerCase() === 'enterprise') {
|
|
return showAppNotice({ title: 'Enterprise Plan', message: 'You are already on the Enterprise plan with full access.', primaryText: 'OK', primaryHref: null });
|
|
}
|
|
return showAppNotice({ title: 'Upgrade required', message, primaryText: 'Upgrade', primaryHref: '/upgrade' });
|
|
}
|
|
|
|
function showConfirmModal({ title = 'Confirm', message = '', confirmText = 'Confirm', cancelText = 'Cancel' } = {}) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.getElementById('app-notice-modal');
|
|
const tit = document.getElementById('app-notice-title');
|
|
const body = document.getElementById('app-notice-body');
|
|
const primary = document.getElementById('app-notice-primary');
|
|
const secondary = document.getElementById('app-notice-secondary');
|
|
|
|
tit.textContent = title;
|
|
body.innerHTML = message || '';
|
|
primary.textContent = confirmText || 'Confirm';
|
|
secondary.textContent = cancelText || 'Cancel';
|
|
|
|
primary.onclick = null;
|
|
secondary.onclick = null;
|
|
const cleanup = (result) => {
|
|
overlay.classList.remove('active');
|
|
primary.onclick = null;
|
|
secondary.onclick = null;
|
|
overlay.onclick = null;
|
|
resolve(result);
|
|
};
|
|
|
|
secondary.onclick = () => cleanup(false);
|
|
primary.onclick = () => cleanup(true);
|
|
|
|
overlay.onclick = (e) => { if (e.target === overlay) cleanup(false); };
|
|
|
|
overlay.classList.add('active');
|
|
});
|
|
}
|
|
|
|
function handleUploadSelect(file) {
|
|
const display = document.getElementById('upload-selected');
|
|
const confirmBtn = document.getElementById('confirm-upload-app');
|
|
const appNameInput = document.getElementById('upload-app-name-input');
|
|
state.uploadFile = file || null;
|
|
if (display) {
|
|
if (file) {
|
|
const sizeMb = (file.size / (1024 * 1024)).toFixed(2);
|
|
display.style.display = 'block';
|
|
display.textContent = `${file.name} (${sizeMb} MB)`;
|
|
} else {
|
|
display.style.display = 'none';
|
|
}
|
|
}
|
|
const appName = appNameInput ? (appNameInput.value || '').trim() : '';
|
|
if (confirmBtn) confirmBtn.disabled = !file || !appName;
|
|
}
|
|
|
|
function handleAppNameChange() {
|
|
const confirmBtn = document.getElementById('confirm-upload-app');
|
|
const appNameInput = document.getElementById('upload-app-name-input');
|
|
const appName = appNameInput ? (appNameInput.value || '').trim() : '';
|
|
if (confirmBtn) confirmBtn.disabled = !state.uploadFile || !appName;
|
|
}
|
|
|
|
async function uploadAppFromZip() {
|
|
if (!state.uploadFile) return;
|
|
if (!state.canUpload) {
|
|
showUpgradePrompt('Uploading existing apps is available on Business and Enterprise plans.');
|
|
return;
|
|
}
|
|
const file = state.uploadFile;
|
|
const maxSize = state.uploadLimit || MAX_UPLOAD_BYTES;
|
|
if (file.size > maxSize) {
|
|
const maxMb = Math.ceil(maxSize / (1024 * 1024));
|
|
await showAppNotice({ title: 'File too large', message: `File too large. Max ${maxMb}MB.` });
|
|
return;
|
|
}
|
|
const confirmBtn = document.getElementById('confirm-upload-app');
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.textContent = 'Uploading...';
|
|
try {
|
|
const base64 = await new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
const appNameInput = document.getElementById('upload-app-name-input');
|
|
const appName = (appNameInput?.value || '').trim() || 'Uploaded Plugin';
|
|
const payload = { data: base64, name: file.name, title: appName };
|
|
const res = await api('/api/apps/upload', { method: 'POST', body: JSON.stringify(payload) });
|
|
if (res?.session?.id) {
|
|
recordBuilderEntry(res.session.id);
|
|
closeUploadModal();
|
|
window.location.href = `/builder?session=${res.session.id}`;
|
|
} else {
|
|
throw new Error('Upload succeeded but no session returned');
|
|
}
|
|
} catch (err) {
|
|
if (isRestrictedError(err.message)) {
|
|
await showUpgradePrompt(err.message);
|
|
} else {
|
|
await showAppNotice({ title: 'Upload failed', message: 'Failed to upload: ' + err.message });
|
|
}
|
|
} finally {
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.textContent = 'Upload & Open';
|
|
}
|
|
}
|
|
|
|
async function createNewApp() {
|
|
openCreateAppModal();
|
|
}
|
|
|
|
function slugifyName(name) {
|
|
const base = (name || '').trim().toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
const suffix = Math.random().toString(36).slice(2, 6);
|
|
return `${base || 'app'}-${suffix}`;
|
|
}
|
|
|
|
const BUILDER_ENTRY_KEY = 'apps_to_builder';
|
|
|
|
function recordBuilderEntry(appId) {
|
|
try {
|
|
const payload = { appId: appId || '', ts: Date.now() };
|
|
sessionStorage.setItem(BUILDER_ENTRY_KEY, JSON.stringify(payload));
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
async function confirmCreateApp() {
|
|
const input = document.getElementById('app-name-input');
|
|
const trimmed = (input.value || '').trim();
|
|
if (!trimmed) {
|
|
input.style.borderColor = '#dc3545';
|
|
input.focus();
|
|
return;
|
|
}
|
|
|
|
input.style.borderColor = '#dee2e6';
|
|
const appId = slugifyName(trimmed);
|
|
|
|
try {
|
|
const payload = { title: trimmed, appId };
|
|
if (state.pendingTemplateId) {
|
|
payload.templateId = state.pendingTemplateId;
|
|
}
|
|
|
|
const data = await api('/api/sessions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (data.session && data.session.id) {
|
|
recordBuilderEntry(data.session.id);
|
|
closeCreateAppModal();
|
|
window.location.href = `/builder?session=${data.session.id}`;
|
|
}
|
|
} catch (error) {
|
|
if (isRestrictedError(error.message)) {
|
|
await showUpgradePrompt(error.message);
|
|
} else {
|
|
await showAppNotice({ title: 'Failed to create app', message: String(error.message || 'Unknown error') });
|
|
}
|
|
}
|
|
}
|
|
|
|
function openApp(appId) {
|
|
recordBuilderEntry(appId);
|
|
window.location.href = `/builder?session=${appId}`;
|
|
}
|
|
|
|
async function exportApp(appId) {
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = 'Exporting...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const exportUrl = `/api/export/zip?sessionId=${encodeURIComponent(appId)}`;
|
|
const response = await fetch(exportUrl, { headers: { 'X-User-Id': state.userId } });
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.error || 'Export failed');
|
|
}
|
|
|
|
// Get filename from Content-Disposition header
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = 'shopify-app.zip';
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename="([^"]+)"/);
|
|
if (match) filename = match[1];
|
|
}
|
|
|
|
// Download the file
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
btn.textContent = '✓ Done';
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
btn.disabled = false;
|
|
}, 2000);
|
|
} catch (error) {
|
|
if (isRestrictedError(error.message)) {
|
|
await showUpgradePrompt(error.message);
|
|
} else {
|
|
await showAppNotice({ title: 'Export failed', message: String(error.message || 'Unknown error') });
|
|
}
|
|
btn.textContent = originalText;
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function deleteApp(appId) {
|
|
const confirmed = await showConfirmModal({ title: 'Delete app', message: 'Are you sure you want to delete this app? This action cannot be undone.', confirmText: 'Delete', cancelText: 'Cancel' });
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
await api(`/api/sessions/${appId}`, { method: 'DELETE' });
|
|
state.apps = state.apps.filter(app => app.id !== appId);
|
|
state.filteredApps = state.filteredApps.filter(app => app.id !== appId);
|
|
renderApps();
|
|
} catch (error) {
|
|
if (isRestrictedError(error.message)) {
|
|
await showUpgradePrompt(error.message);
|
|
} else {
|
|
await showAppNotice({ title: 'Failed to delete app', message: String(error.message || 'Unknown error') });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
document.getElementById('create-new-app').addEventListener('click', createNewApp);
|
|
document.getElementById('cancel-create-app').addEventListener('click', closeCreateAppModal);
|
|
document.getElementById('confirm-create-app').addEventListener('click', confirmCreateApp);
|
|
document.getElementById('search-input').addEventListener('input', (e) => {
|
|
filterApps(e.target.value);
|
|
});
|
|
|
|
const uploadBtn = document.getElementById('upload-app-btn');
|
|
const uploadModal = document.getElementById('upload-app-modal');
|
|
const uploadInput = document.getElementById('upload-input');
|
|
const cancelUploadBtn = document.getElementById('cancel-upload-app');
|
|
const confirmUploadBtn = document.getElementById('confirm-upload-app');
|
|
|
|
if (uploadBtn) uploadBtn.addEventListener('click', openUploadModal);
|
|
if (cancelUploadBtn) cancelUploadBtn.addEventListener('click', closeUploadModal);
|
|
if (confirmUploadBtn) confirmUploadBtn.addEventListener('click', uploadAppFromZip);
|
|
|
|
const browseTemplatesBtn = document.getElementById('browse-templates-btn');
|
|
const closeTemplatesBtn = document.getElementById('close-templates-modal');
|
|
if (browseTemplatesBtn) browseTemplatesBtn.addEventListener('click', openTemplatesModal);
|
|
if (closeTemplatesBtn) closeTemplatesBtn.addEventListener('click', closeTemplatesModal);
|
|
document.getElementById('templates-modal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'templates-modal') closeTemplatesModal();
|
|
});
|
|
if (uploadModal) {
|
|
uploadModal.addEventListener('click', (e) => {
|
|
if (e.target.id === 'upload-app-modal') closeUploadModal();
|
|
});
|
|
}
|
|
if (uploadInput) {
|
|
uploadInput.addEventListener('change', (e) => {
|
|
const file = e.target.files && e.target.files[0];
|
|
handleUploadSelect(file);
|
|
});
|
|
}
|
|
const uploadAppNameInput = document.getElementById('upload-app-name-input');
|
|
if (uploadAppNameInput) {
|
|
uploadAppNameInput.addEventListener('input', handleAppNameChange);
|
|
uploadAppNameInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
uploadAppFromZip();
|
|
} else if (e.key === 'Escape') {
|
|
closeUploadModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle Enter key in modal input
|
|
document.getElementById('app-name-input').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
confirmCreateApp();
|
|
} else if (e.key === 'Escape') {
|
|
closeCreateAppModal();
|
|
}
|
|
});
|
|
|
|
// Close modal when clicking outside
|
|
document.getElementById('create-app-modal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'create-app-modal') {
|
|
closeCreateAppModal();
|
|
}
|
|
});
|
|
|
|
// Load apps on page load
|
|
if (state.userId) {
|
|
loadApps().then(() => {
|
|
// Check for template param
|
|
const params = new URLSearchParams(window.location.search);
|
|
const templateId = params.get('template');
|
|
if (templateId) {
|
|
handleTemplateSelect(templateId);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function handleTemplateSelect(templateId) {
|
|
// Check plan
|
|
const plan = state.plan ? state.plan.toLowerCase() : 'starter';
|
|
const isPaid = ['business', 'enterprise', 'professional'].includes(plan);
|
|
|
|
if (!isPaid) {
|
|
// Show upgrade notice
|
|
showAppNotice({
|
|
title: 'Upgrade Required',
|
|
message: `Using pre-built templates is a feature available on Professional plans and above. <br><br>
|
|
Please upgrade your plan to access our library of high-quality templates.`,
|
|
onPrimary: () => { window.location.href = '/upgrade'; },
|
|
primaryText: 'View Pricing'
|
|
});
|
|
// clear param
|
|
window.history.replaceState({}, '', '/apps');
|
|
return;
|
|
}
|
|
|
|
// Proceed to create app from template
|
|
// We can reuse the create app modal but pre-fill or just directly create if we have a name?
|
|
// Let's ask for a name first using a modified create flow.
|
|
const templateName = templateId.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
|
|
document.getElementById('app-name-input').value = `My ${templateName}`;
|
|
|
|
// Open modal and store templateId in a accessible way for the confirm handler
|
|
state.pendingTemplateId = templateId;
|
|
openCreateAppModal();
|
|
}
|
|
|
|
</script>
|
|
<script>
|
|
// Onboarding Modal functionality for apps.html
|
|
(function() {
|
|
const ONBOARDING_COMPLETED_KEY = 'plugin_compass_onboarding_completed';
|
|
let currentStep = 1;
|
|
const totalSteps = 5;
|
|
let onboardingStatus = null;
|
|
|
|
async function isOnboardingCompleted() {
|
|
if (onboardingStatus !== null) {
|
|
return onboardingStatus;
|
|
}
|
|
try {
|
|
const res = await fetch('/api/onboarding');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
onboardingStatus = data.completed;
|
|
return onboardingStatus;
|
|
}
|
|
} catch (e) {
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function markOnboardingCompleted() {
|
|
try {
|
|
const res = await fetch('/api/onboarding', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ completed: true })
|
|
});
|
|
if (res.ok) {
|
|
onboardingStatus = true;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to save onboarding completion status');
|
|
}
|
|
}
|
|
|
|
const tourSteps = [
|
|
{ target: null, color: '#008060' },
|
|
{ target: '#create-new-app', color: '#5A31F4' },
|
|
{ target: '#upload-app-btn', color: '#F59E0B' },
|
|
{ target: '#browse-templates-btn', color: '#10B981' },
|
|
{ target: null, color: '#EC4899' }
|
|
];
|
|
|
|
function showStep(step) {
|
|
const totalSteps = tourSteps.length;
|
|
const config = tourSteps[step - 1];
|
|
|
|
// Update content visibility
|
|
for (let i = 1; i <= totalSteps; i++) {
|
|
const stepEl = document.getElementById('onboarding-step-' + i);
|
|
if (stepEl) stepEl.style.display = i === step ? 'block' : 'none';
|
|
}
|
|
|
|
// Update counter
|
|
const counter = document.getElementById('onboarding-step-counter');
|
|
if (counter) counter.textContent = `${step} of ${totalSteps}`;
|
|
|
|
const backBtn = document.getElementById('onboarding-back');
|
|
const nextBtn = document.getElementById('onboarding-next');
|
|
|
|
if (backBtn) backBtn.style.display = step === 1 ? 'none' : 'block';
|
|
if (nextBtn) {
|
|
nextBtn.textContent = step === totalSteps ? 'Get Started' : 'Next';
|
|
nextBtn.style.background = step === totalSteps ? '#10B981' : '#008060';
|
|
}
|
|
|
|
// Position the box
|
|
positionTourBox(config.target);
|
|
}
|
|
|
|
function positionTourBox(targetSelector) {
|
|
const container = document.getElementById('onboarding-container');
|
|
const pointer = document.getElementById('onboarding-pointer');
|
|
const modal = document.getElementById('onboarding-modal');
|
|
|
|
if (!container) return;
|
|
|
|
const isMobile = window.innerWidth < 480;
|
|
const boxWidth = isMobile ? Math.min(280, window.innerWidth - 32) : 280;
|
|
|
|
if (!targetSelector) {
|
|
// Center position
|
|
container.style.top = '50%';
|
|
container.style.left = '50%';
|
|
container.style.transform = 'translate(-50%, -50%)';
|
|
if (pointer) pointer.style.display = 'none';
|
|
modal.style.background = 'transparent';
|
|
return;
|
|
}
|
|
|
|
const target = document.querySelector(targetSelector);
|
|
if (!target) {
|
|
// Fallback to center if target missing
|
|
positionTourBox(null);
|
|
return;
|
|
}
|
|
|
|
modal.style.background = 'transparent'; // Lighter backdrop for tour
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
// Default: Position above the element
|
|
let top = rect.top - container.offsetHeight - 16;
|
|
let left = rect.left + (rect.width / 2) - (boxWidth / 2);
|
|
|
|
// Bounds checking
|
|
if (top < 16) {
|
|
// Position below instead
|
|
top = rect.bottom + 16;
|
|
if (pointer) {
|
|
pointer.style.display = 'block';
|
|
pointer.style.top = '-8px';
|
|
pointer.style.bottom = 'auto';
|
|
pointer.style.left = '50%';
|
|
pointer.style.marginLeft = '-8px';
|
|
}
|
|
} else {
|
|
if (pointer) {
|
|
pointer.style.display = 'block';
|
|
pointer.style.bottom = '-8px';
|
|
pointer.style.top = 'auto';
|
|
pointer.style.left = '50%';
|
|
pointer.style.marginLeft = '-8px';
|
|
}
|
|
}
|
|
|
|
// Keep horizontal within viewport
|
|
const padding = 16;
|
|
left = Math.max(padding, Math.min(left, window.innerWidth - boxWidth - padding));
|
|
|
|
container.style.top = top + 'px';
|
|
container.style.left = left + 'px';
|
|
container.style.transform = 'none';
|
|
}
|
|
|
|
function nextStep() {
|
|
if (currentStep < tourSteps.length) {
|
|
currentStep++;
|
|
showStep(currentStep);
|
|
} else {
|
|
completeOnboarding();
|
|
}
|
|
}
|
|
|
|
function prevStep() {
|
|
if (currentStep > 1) {
|
|
currentStep--;
|
|
showStep(currentStep);
|
|
}
|
|
}
|
|
|
|
function goToStep(step) {
|
|
if (step >= 1 && step <= tourSteps.length) {
|
|
currentStep = step;
|
|
showStep(currentStep);
|
|
}
|
|
}
|
|
|
|
function completeOnboarding() {
|
|
markOnboardingCompleted();
|
|
hideOnboarding();
|
|
if (typeof posthog !== 'undefined') {
|
|
posthog.capture('onboarding_completed');
|
|
}
|
|
}
|
|
|
|
function hideOnboarding() {
|
|
const modal = document.getElementById('onboarding-modal');
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function showOnboarding() {
|
|
const modal = document.getElementById('onboarding-modal');
|
|
if (modal) {
|
|
currentStep = 1;
|
|
modal.style.display = 'block'; // Changed to allow absolute children
|
|
showStep(currentStep);
|
|
}
|
|
}
|
|
|
|
window.showOnboarding = showOnboarding;
|
|
window.hideOnboarding = hideOnboarding;
|
|
|
|
function initOnboarding() {
|
|
const exitBtn = document.getElementById('onboarding-exit');
|
|
const backBtn = document.getElementById('onboarding-back');
|
|
const nextBtn = document.getElementById('onboarding-next');
|
|
const modal = document.getElementById('onboarding-modal');
|
|
|
|
if (exitBtn) {
|
|
exitBtn.addEventListener('click', completeOnboarding);
|
|
}
|
|
|
|
if (backBtn) {
|
|
backBtn.addEventListener('click', prevStep);
|
|
}
|
|
|
|
if (nextBtn) {
|
|
nextBtn.addEventListener('click', nextStep);
|
|
}
|
|
|
|
if (modal) {
|
|
modal.addEventListener('click', function(e) {
|
|
if (e.target === modal) {
|
|
completeOnboarding();
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (modal.style.display !== 'none') {
|
|
const config = tourSteps[currentStep - 1];
|
|
positionTourBox(config.target);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (modal.style.display !== 'none') {
|
|
const config = tourSteps[currentStep - 1];
|
|
positionTourBox(config.target);
|
|
}
|
|
}, true);
|
|
|
|
if (typeof document !== 'undefined') {
|
|
document.addEventListener('keydown', function(e) {
|
|
const onboardingModal = document.getElementById('onboarding-modal');
|
|
if (!onboardingModal || onboardingModal.style.display === 'none') return;
|
|
|
|
if (e.key === 'Escape') {
|
|
completeOnboarding();
|
|
} else if (e.key === 'ArrowRight') {
|
|
nextStep();
|
|
} else if (e.key === 'ArrowLeft') {
|
|
prevStep();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
initOnboarding();
|
|
if (!(await isOnboardingCompleted())) {
|
|
setTimeout(showOnboarding, 600);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
<style>
|
|
/* Onboarding Modal Styles */
|
|
#onboarding-exit:hover {
|
|
background: #f3f4f6;
|
|
color: #374151;
|
|
}
|
|
|
|
.onboarding-btn-primary:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 8px 20px rgba(0, 128, 96, 0.3);
|
|
}
|
|
|
|
.onboarding-btn-primary:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.onboarding-btn-secondary:hover {
|
|
background: #f9fafb;
|
|
border-color: #d1d5db;
|
|
}
|
|
|
|
.onboarding-dot.active {
|
|
background: #008060;
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.onboarding-dot:hover {
|
|
background: #008060;
|
|
opacity: 0.7;
|
|
}
|
|
</style>
|
|
<footer
|
|
style="margin-top: 80px; padding: 40px 24px; border-top: 1px solid #e1e3e5; text-align: center; color: #6d7175; font-size: 0.875rem;">
|
|
<p>© 2026 Plugin Compass. All rights reserved.</p>
|
|
<div style="margin-top: 12px; display: flex; justify-content: center; gap: 16px;">
|
|
<a href="/terms" style="color: #008060; text-decoration: none;">Terms</a>
|
|
<a href="/privacy" style="color: #008060; text-decoration: none;">Privacy</a>
|
|
<a href="/contact" style="color: #008060; text-decoration: none;">Contact Us</a>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
|
|
</html>
|