feat: add platform marketing homepage with signup flow
Implement complete marketing homepage for Wizamart targeting Letzshop vendors in Luxembourg. Includes: - Marketing homepage with hero, pricing tiers, and add-ons - 4-step signup wizard with Stripe card collection (30-day trial) - Letzshop vendor lookup for shop claiming - Platform API endpoints for pricing, vendors, and signup - Stripe SetupIntent integration for trial with card upfront - Database fields for Letzshop vendor identity tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{# Dynamic page title #}
|
||||
<title>{% block title %}Multi-Vendor Marketplace Platform{% endblock %}</title>
|
||||
<title>{% block title %}Wizamart - Order Management for Letzshop Sellers{% endblock %}</title>
|
||||
|
||||
{# SEO Meta Tags #}
|
||||
<meta name="description" content="{% block meta_description %}Leading multi-vendor marketplace platform connecting vendors with customers worldwide.{% endblock %}">
|
||||
<meta name="keywords" content="{% block meta_keywords %}marketplace, multi-vendor, e-commerce, online shopping{% endblock %}">
|
||||
<meta name="description" content="{% block meta_description %}Lightweight OMS for Letzshop vendors in Luxembourg. Order management, inventory, and invoicing made simple.{% endblock %}">
|
||||
<meta name="keywords" content="{% block meta_keywords %}letzshop, order management, oms, luxembourg, e-commerce, invoicing, inventory{% endblock %}">
|
||||
|
||||
{# Favicon #}
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='favicon.ico') }}">
|
||||
@@ -57,22 +57,33 @@
|
||||
{# Logo / Brand #}
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 rounded-lg gradient-primary flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xl">M</span>
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-r from-indigo-600 to-purple-600 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xl">W</span>
|
||||
</div>
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Marketplace
|
||||
Wizamart
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Desktop Navigation #}
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
{# Main Navigation #}
|
||||
<a href="/pricing" class="text-gray-700 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 font-medium transition-colors">
|
||||
Pricing
|
||||
</a>
|
||||
<a href="/find-shop" class="text-gray-700 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 font-medium transition-colors">
|
||||
Find Your Shop
|
||||
</a>
|
||||
<a href="/signup" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors">
|
||||
Start Free Trial
|
||||
</a>
|
||||
|
||||
{# Dynamic header navigation from CMS #}
|
||||
{% if header_pages %}
|
||||
{% for page in header_pages %}
|
||||
<a href="/{{ page.slug }}"
|
||||
class="text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors">
|
||||
class="text-gray-700 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 font-medium transition-colors">
|
||||
{{ page.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -137,15 +148,15 @@
|
||||
{# Brand Column #}
|
||||
<div class="col-span-1">
|
||||
<div class="flex items-center space-x-3 mb-4">
|
||||
<div class="w-8 h-8 rounded-lg gradient-primary flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xl">M</span>
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-r from-indigo-600 to-purple-600 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xl">W</span>
|
||||
</div>
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Marketplace
|
||||
Wizamart
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Connecting vendors with customers worldwide. Build your online store today.
|
||||
Lightweight OMS for Letzshop sellers. Manage orders, inventory, and invoicing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -199,7 +210,7 @@
|
||||
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
© 2024 Marketplace Platform. All rights reserved.
|
||||
© 2025 Wizamart. Built for Luxembourg e-commerce.
|
||||
</p>
|
||||
<div class="flex space-x-6 mt-4 md:mt-0">
|
||||
<a href="/privacy" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
|
||||
|
||||
168
app/templates/platform/find-shop.html
Normal file
168
app/templates/platform/find-shop.html
Normal file
@@ -0,0 +1,168 @@
|
||||
{# app/templates/platform/find-shop.html #}
|
||||
{# Letzshop Vendor Finder Page #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}Find Your Shop - Wizamart{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="vendorFinderData()" class="py-16 lg:py-24">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{# Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Find Your Letzshop Shop
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600 dark:text-gray-400">
|
||||
Enter your Letzshop shop URL or search by name to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Search Form #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 border border-gray-200 dark:border-gray-700 shadow-lg">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<input
|
||||
type="text"
|
||||
x-model="searchQuery"
|
||||
@keyup.enter="lookupVendor()"
|
||||
placeholder="Enter Letzshop URL or shop name..."
|
||||
class="flex-1 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
@click="lookupVendor()"
|
||||
:disabled="loading || !searchQuery.trim()"
|
||||
class="px-8 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl transition-colors disabled:opacity-50 flex items-center justify-center min-w-[140px]">
|
||||
<template x-if="loading">
|
||||
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="!loading">
|
||||
<span>Search</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Examples #}
|
||||
<div class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<strong>Examples:</strong>
|
||||
<ul class="list-disc list-inside mt-1">
|
||||
<li>https://letzshop.lu/vendors/my-shop</li>
|
||||
<li>letzshop.lu/vendors/my-shop</li>
|
||||
<li>my-shop (just the shop name)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Results #}
|
||||
<template x-if="result">
|
||||
<div class="mt-8 bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<template x-if="result.found">
|
||||
<div class="p-8">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white" x-text="result.vendor.name"></h2>
|
||||
<a :href="result.vendor.letzshop_url" target="_blank"
|
||||
class="text-indigo-600 dark:text-indigo-400 hover:underline mt-1 inline-block"
|
||||
x-text="result.vendor.letzshop_url"></a>
|
||||
|
||||
<template x-if="result.vendor.description">
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-4" x-text="result.vendor.description"></p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template x-if="result.vendor.logo_url">
|
||||
<img :src="result.vendor.logo_url" :alt="result.vendor.name"
|
||||
class="w-20 h-20 rounded-xl object-cover border border-gray-200 dark:border-gray-700"/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex items-center gap-4">
|
||||
<template x-if="!result.vendor.is_claimed">
|
||||
<a :href="'/signup?letzshop=' + result.vendor.slug"
|
||||
class="px-8 py-3 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-xl transition-colors">
|
||||
Claim This Shop & Start Free Trial
|
||||
</a>
|
||||
</template>
|
||||
<template x-if="result.vendor.is_claimed">
|
||||
<div class="px-6 py-3 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-xl">
|
||||
This shop has already been claimed. If this is your shop, please
|
||||
<a href="/contact" class="text-indigo-600 hover:underline">contact support</a>.
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!result.found">
|
||||
<div class="p-8 text-center">
|
||||
<svg class="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">Shop Not Found</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400" x-text="result.error || 'We could not find a Letzshop shop with that URL. Please check and try again.'"></p>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="/signup" class="text-indigo-600 dark:text-indigo-400 hover:underline">
|
||||
Or sign up without a Letzshop connection →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{# Help Section #}
|
||||
<div class="mt-12 text-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Need Help?</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Don't have a Letzshop account yet? No problem!
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="https://letzshop.lu" target="_blank"
|
||||
class="px-6 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
Create a Letzshop Account
|
||||
</a>
|
||||
<a href="/signup"
|
||||
class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-xl transition-colors">
|
||||
Sign Up Without Letzshop
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function vendorFinderData() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
result: null,
|
||||
loading: false,
|
||||
|
||||
async lookupVendor() {
|
||||
if (!this.searchQuery.trim()) return;
|
||||
|
||||
this.loading = true;
|
||||
this.result = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/platform/letzshop-vendors/lookup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: this.searchQuery })
|
||||
});
|
||||
|
||||
this.result = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Lookup error:', error);
|
||||
this.result = { found: false, error: 'Failed to lookup. Please try again.' };
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
407
app/templates/platform/homepage-wizamart.html
Normal file
407
app/templates/platform/homepage-wizamart.html
Normal file
@@ -0,0 +1,407 @@
|
||||
{# app/templates/platform/homepage-wizamart.html #}
|
||||
{# Wizamart Marketing Homepage - Letzshop OMS Platform #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}Wizamart - Order Management for Letzshop Sellers{% endblock %}
|
||||
{% block meta_description %}Lightweight OMS for Letzshop vendors. Manage orders, inventory, and invoicing. Start your 30-day free trial today.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="homepageData()" class="bg-gray-50 dark:bg-gray-900">
|
||||
|
||||
{# =========================================================================
|
||||
HERO SECTION
|
||||
========================================================================= #}
|
||||
<section class="relative overflow-hidden">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-24">
|
||||
<div class="text-center">
|
||||
{# Badge #}
|
||||
<div class="inline-flex items-center px-4 py-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-full text-indigo-700 dark:text-indigo-300 text-sm font-medium mb-6">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
30-Day Free Trial - No Credit Card Required to Start
|
||||
</div>
|
||||
|
||||
{# Headline #}
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-extrabold text-gray-900 dark:text-white leading-tight mb-6">
|
||||
Lightweight OMS for
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600">Letzshop Sellers</span>
|
||||
</h1>
|
||||
|
||||
{# Subheadline #}
|
||||
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto mb-10">
|
||||
Order management, inventory, and invoicing built for Luxembourg e-commerce.
|
||||
Stop juggling spreadsheets. Start running your business.
|
||||
</p>
|
||||
|
||||
{# CTA Buttons #}
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/signup"
|
||||
class="inline-flex items-center justify-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg shadow-indigo-500/30 transition-all hover:scale-105">
|
||||
Start Free Trial
|
||||
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#find-shop"
|
||||
class="inline-flex items-center justify-center px-8 py-4 bg-white dark:bg-gray-800 text-gray-900 dark:text-white font-semibold rounded-xl border-2 border-gray-200 dark:border-gray-700 hover:border-indigo-500 transition-all">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
Find Your Letzshop Shop
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Background Decoration #}
|
||||
<div class="absolute inset-0 -z-10 overflow-hidden">
|
||||
<div class="absolute -top-1/2 -right-1/4 w-96 h-96 bg-indigo-200 dark:bg-indigo-900/20 rounded-full blur-3xl opacity-50"></div>
|
||||
<div class="absolute -bottom-1/2 -left-1/4 w-96 h-96 bg-purple-200 dark:bg-purple-900/20 rounded-full blur-3xl opacity-50"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# =========================================================================
|
||||
PRICING SECTION
|
||||
========================================================================= #}
|
||||
<section id="pricing" class="py-16 lg:py-24 bg-white dark:bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{# Section Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Simple, Transparent Pricing
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Choose the plan that fits your business. All plans include a {{ trial_days }}-day free trial.
|
||||
</p>
|
||||
|
||||
{# Billing Toggle #}
|
||||
<div class="flex items-center justify-center mt-8 space-x-4">
|
||||
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': !annual }">Monthly</span>
|
||||
<button @click="annual = !annual"
|
||||
class="relative w-14 h-7 rounded-full transition-colors"
|
||||
:class="annual ? 'bg-indigo-600' : 'bg-gray-300 dark:bg-gray-600'">
|
||||
<span class="absolute top-1 left-1 w-5 h-5 bg-white rounded-full shadow transition-transform"
|
||||
:class="annual ? 'translate-x-7' : ''"></span>
|
||||
</button>
|
||||
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': annual }">
|
||||
Annual
|
||||
<span class="text-green-600 text-sm font-medium ml-1">Save 2 months!</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Pricing Cards Grid #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{% for tier in tiers %}
|
||||
<div class="relative bg-gray-50 dark:bg-gray-900 rounded-2xl p-6 border-2 transition-all hover:shadow-xl
|
||||
{% if tier.is_popular %}border-indigo-500 shadow-lg{% else %}border-gray-200 dark:border-gray-700{% endif %}">
|
||||
|
||||
{# Popular Badge #}
|
||||
{% if tier.is_popular %}
|
||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span class="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full">
|
||||
MOST POPULAR
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Tier Name #}
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">{{ tier.name }}</h3>
|
||||
|
||||
{# Price #}
|
||||
<div class="mb-6">
|
||||
<template x-if="!annual">
|
||||
<div>
|
||||
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ tier.price_monthly }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">/month</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="annual">
|
||||
<div>
|
||||
{% if tier.price_annual %}
|
||||
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ (tier.price_annual / 12)|round(0)|int }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">/month</span>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ tier.price_annual }}/year
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-2xl font-bold text-gray-900 dark:text-white">Custom</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{# Features List #}
|
||||
<ul class="space-y-3 mb-8">
|
||||
{# Orders #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.orders_per_month %}{{ tier.orders_per_month }} orders/month{% else %}Unlimited orders{% endif %}
|
||||
</li>
|
||||
{# Products #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.products_limit %}{{ tier.products_limit }} products{% else %}Unlimited products{% endif %}
|
||||
</li>
|
||||
{# Team Members #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.team_members %}{{ tier.team_members }} team member{% if tier.team_members > 1 %}s{% endif %}{% else %}Unlimited team{% endif %}
|
||||
</li>
|
||||
{# Letzshop Sync #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Letzshop order sync
|
||||
</li>
|
||||
{# Tier-specific features #}
|
||||
{% if 'invoice_eu_vat' in tier.features %}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
EU VAT invoicing
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'analytics_dashboard' in tier.features %}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Analytics dashboard
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'api_access' in tier.features %}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
API access
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{# CTA Button #}
|
||||
{% if tier.is_enterprise %}
|
||||
<a href="/contact?tier=enterprise"
|
||||
class="block w-full py-3 px-4 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold rounded-xl text-center hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
Contact Sales
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/signup?tier={{ tier.code }}"
|
||||
:href="'/signup?tier={{ tier.code }}&annual=' + annual"
|
||||
class="block w-full py-3 px-4 font-semibold rounded-xl text-center transition-colors
|
||||
{% if tier.is_popular %}bg-indigo-600 hover:bg-indigo-700 text-white{% else %}bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-900/50{% endif %}">
|
||||
Start Free Trial
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# =========================================================================
|
||||
ADD-ONS SECTION
|
||||
========================================================================= #}
|
||||
<section id="addons" class="py-16 lg:py-24 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{# Section Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Enhance Your Platform
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Add custom branding, professional email, and enhanced security.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Add-ons Grid #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{% for addon in addons %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||
{# Icon #}
|
||||
<div class="w-14 h-14 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl flex items-center justify-center mb-6">
|
||||
{% if addon.icon == 'globe' %}
|
||||
<svg class="w-7 h-7 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
||||
</svg>
|
||||
{% elif addon.icon == 'shield-check' %}
|
||||
<svg class="w-7 h-7 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
{% elif addon.icon == 'mail' %}
|
||||
<svg class="w-7 h-7 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Name & Description #}
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">{{ addon.name }}</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ addon.description }}</p>
|
||||
|
||||
{# Price #}
|
||||
<div class="flex items-baseline">
|
||||
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ addon.price }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400 ml-1">/{{ addon.billing_period }}</span>
|
||||
</div>
|
||||
|
||||
{# Options for email packages #}
|
||||
{% if addon.options %}
|
||||
<div class="mt-4 space-y-2">
|
||||
{% for opt in addon.options %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ opt.quantity }} addresses: {{ opt.price }}/month
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# =========================================================================
|
||||
LETZSHOP VENDOR FINDER
|
||||
========================================================================= #}
|
||||
<section id="find-shop" class="py-16 lg:py-24 bg-white dark:bg-gray-800">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{# Section Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Find Your Letzshop Shop
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||
Already selling on Letzshop? Enter your shop URL to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Search Form #}
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-2xl p-8 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<input
|
||||
type="text"
|
||||
x-model="shopUrl"
|
||||
placeholder="Enter your Letzshop URL (e.g., letzshop.lu/vendors/my-shop)"
|
||||
class="flex-1 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
@click="lookupVendor()"
|
||||
:disabled="loading"
|
||||
class="px-8 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl transition-colors disabled:opacity-50 flex items-center justify-center">
|
||||
<template x-if="loading">
|
||||
<svg class="animate-spin w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||
</svg>
|
||||
</template>
|
||||
Find My Shop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Result #}
|
||||
<template x-if="vendorResult">
|
||||
<div class="mt-6 p-6 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
|
||||
<template x-if="vendorResult.found">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white" x-text="vendorResult.vendor.name"></h3>
|
||||
<a :href="vendorResult.vendor.letzshop_url" target="_blank" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline" x-text="vendorResult.vendor.letzshop_url"></a>
|
||||
</div>
|
||||
<template x-if="!vendorResult.vendor.is_claimed">
|
||||
<a :href="'/signup?letzshop=' + vendorResult.vendor.slug"
|
||||
class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors">
|
||||
Claim This Shop
|
||||
</a>
|
||||
</template>
|
||||
<template x-if="vendorResult.vendor.is_claimed">
|
||||
<span class="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-lg">
|
||||
Already Claimed
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!vendorResult.found">
|
||||
<div class="text-center text-gray-600 dark:text-gray-400">
|
||||
<p x-text="vendorResult.error || 'Shop not found. Please check your URL and try again.'"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{# Help Text #}
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||
Don't have a Letzshop account? <a href="https://letzshop.lu" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">Sign up with Letzshop first</a>, then come back to connect your shop.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# =========================================================================
|
||||
FINAL CTA SECTION
|
||||
========================================================================= #}
|
||||
<section class="py-16 lg:py-24 bg-gradient-to-r from-indigo-600 to-purple-600">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-6">
|
||||
Ready to Streamline Your Orders?
|
||||
</h2>
|
||||
<p class="text-xl text-indigo-100 mb-10">
|
||||
Join Letzshop vendors who trust Wizamart for their order management.
|
||||
Start your {{ trial_days }}-day free trial today.
|
||||
</p>
|
||||
<a href="/signup"
|
||||
class="inline-flex items-center px-10 py-4 bg-white text-indigo-600 font-bold rounded-xl shadow-lg hover:shadow-xl transition-all hover:scale-105">
|
||||
Start Free Trial
|
||||
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function homepageData() {
|
||||
return {
|
||||
annual: false,
|
||||
shopUrl: '',
|
||||
vendorResult: null,
|
||||
loading: false,
|
||||
|
||||
async lookupVendor() {
|
||||
if (!this.shopUrl.trim()) return;
|
||||
|
||||
this.loading = true;
|
||||
this.vendorResult = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/platform/letzshop-vendors/lookup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: this.shopUrl })
|
||||
});
|
||||
|
||||
this.vendorResult = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Lookup error:', error);
|
||||
this.vendorResult = { found: false, error: 'Failed to lookup. Please try again.' };
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
119
app/templates/platform/pricing.html
Normal file
119
app/templates/platform/pricing.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{# app/templates/platform/pricing.html #}
|
||||
{# Standalone Pricing Page #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}Pricing - Wizamart{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="{ annual: false }" class="py-16 lg:py-24">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{# Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Choose Your Plan
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
All plans include a {{ trial_days }}-day free trial. No credit card required to start.
|
||||
</p>
|
||||
|
||||
{# Billing Toggle #}
|
||||
<div class="flex items-center justify-center mt-8 space-x-4">
|
||||
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': !annual }">Monthly</span>
|
||||
<button @click="annual = !annual"
|
||||
class="relative w-14 h-7 rounded-full transition-colors"
|
||||
:class="annual ? 'bg-indigo-600' : 'bg-gray-300 dark:bg-gray-600'">
|
||||
<span class="absolute top-1 left-1 w-5 h-5 bg-white rounded-full shadow transition-transform"
|
||||
:class="annual ? 'translate-x-7' : ''"></span>
|
||||
</button>
|
||||
<span class="text-gray-700 dark:text-gray-300" :class="{ 'font-semibold': annual }">
|
||||
Annual
|
||||
<span class="text-green-600 text-sm font-medium ml-1">Save 2 months!</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Pricing Cards #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{% for tier in tiers %}
|
||||
<div class="relative bg-white dark:bg-gray-800 rounded-2xl p-6 border-2 transition-all hover:shadow-xl
|
||||
{% if tier.is_popular %}border-indigo-500 shadow-lg{% else %}border-gray-200 dark:border-gray-700{% endif %}">
|
||||
|
||||
{% if tier.is_popular %}
|
||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span class="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full">RECOMMENDED</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">{{ tier.name }}</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<template x-if="!annual">
|
||||
<div>
|
||||
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ tier.price_monthly }}</span>
|
||||
<span class="text-gray-500">/month</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="annual">
|
||||
<div>
|
||||
{% if tier.price_annual %}
|
||||
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ (tier.price_annual / 12)|round(0)|int }}</span>
|
||||
<span class="text-gray-500">/month</span>
|
||||
<div class="text-sm text-gray-500">Billed {{ tier.price_annual }}/year</div>
|
||||
{% else %}
|
||||
<span class="text-2xl font-bold text-gray-900 dark:text-white">Custom Pricing</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 mb-8 text-sm">
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.orders_per_month %}{{ tier.orders_per_month }} orders/month{% else %}Unlimited orders{% endif %}
|
||||
</li>
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.products_limit %}{{ tier.products_limit }} products{% else %}Unlimited products{% endif %}
|
||||
</li>
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.team_members %}{{ tier.team_members }} team member{% if tier.team_members > 1 %}s{% endif %}{% else %}Unlimited team{% endif %}
|
||||
</li>
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.order_history_months %}{{ tier.order_history_months }} months history{% else %}Unlimited history{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if tier.is_enterprise %}
|
||||
<a href="/contact" class="block w-full py-3 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold rounded-xl text-center hover:bg-gray-300 transition-colors">
|
||||
Contact Sales
|
||||
</a>
|
||||
{% else %}
|
||||
<a :href="'/signup?tier={{ tier.code }}&annual=' + annual"
|
||||
class="block w-full py-3 font-semibold rounded-xl text-center transition-colors
|
||||
{% if tier.is_popular %}bg-indigo-600 hover:bg-indigo-700 text-white{% else %}bg-indigo-100 text-indigo-700 hover:bg-indigo-200{% endif %}">
|
||||
Start Free Trial
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Back to Home #}
|
||||
<div class="text-center mt-12">
|
||||
<a href="/" class="text-indigo-600 dark:text-indigo-400 hover:underline">
|
||||
← Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
81
app/templates/platform/signup-success.html
Normal file
81
app/templates/platform/signup-success.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{# app/templates/platform/signup-success.html #}
|
||||
{# Signup Success Page #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}Welcome to Wizamart!{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen py-16 bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||
<div class="max-w-lg mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
|
||||
{# Success Icon #}
|
||||
<div class="w-24 h-24 mx-auto mb-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{# Welcome Message #}
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Welcome to Wizamart!
|
||||
</h1>
|
||||
|
||||
<p class="text-xl text-gray-600 dark:text-gray-400 mb-8">
|
||||
Your account has been created and your {{ trial_days }}-day free trial has started.
|
||||
</p>
|
||||
|
||||
{# Next Steps #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 border border-gray-200 dark:border-gray-700 text-left mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">What's Next?</h2>
|
||||
<ul class="space-y-4">
|
||||
<li class="flex items-start">
|
||||
<div class="w-6 h-6 bg-indigo-100 dark:bg-indigo-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span class="text-indigo-600 dark:text-indigo-400 text-sm font-bold">1</span>
|
||||
</div>
|
||||
<span class="ml-3 text-gray-700 dark:text-gray-300">
|
||||
<strong>Connect Letzshop:</strong> Add your API key to start syncing orders automatically.
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<div class="w-6 h-6 bg-indigo-100 dark:bg-indigo-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span class="text-indigo-600 dark:text-indigo-400 text-sm font-bold">2</span>
|
||||
</div>
|
||||
<span class="ml-3 text-gray-700 dark:text-gray-300">
|
||||
<strong>Set Up Invoicing:</strong> Configure your invoice settings for Luxembourg compliance.
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<div class="w-6 h-6 bg-indigo-100 dark:bg-indigo-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span class="text-indigo-600 dark:text-indigo-400 text-sm font-bold">3</span>
|
||||
</div>
|
||||
<span class="ml-3 text-gray-700 dark:text-gray-300">
|
||||
<strong>Import Products:</strong> Sync your product catalog from Letzshop.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# CTA Button #}
|
||||
{% if vendor_code %}
|
||||
<a href="/vendor/{{ vendor_code }}/dashboard"
|
||||
class="inline-flex items-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg transition-all hover:scale-105">
|
||||
Go to Dashboard
|
||||
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/admin/login"
|
||||
class="inline-flex items-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg transition-all">
|
||||
Login to Dashboard
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# Support Link #}
|
||||
<p class="mt-8 text-gray-500 dark:text-gray-400">
|
||||
Need help getting started?
|
||||
<a href="/contact" class="text-indigo-600 dark:text-indigo-400 hover:underline">Contact our support team</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
515
app/templates/platform/signup.html
Normal file
515
app/templates/platform/signup.html
Normal file
@@ -0,0 +1,515 @@
|
||||
{# app/templates/platform/signup.html #}
|
||||
{# Multi-step Signup Wizard #}
|
||||
{% extends "platform/base.html" %}
|
||||
|
||||
{% block title %}Start Your Free Trial - Wizamart{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{# Stripe.js for payment #}
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="signupWizard()" class="min-h-screen py-12 bg-gray-50 dark:bg-gray-900">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{# Progress Steps #}
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between">
|
||||
<template x-for="(stepName, index) in ['Select Plan', 'Claim Shop', 'Account', 'Payment']" :key="index">
|
||||
<div class="flex items-center" :class="index < 3 ? 'flex-1' : ''">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-full font-semibold transition-colors"
|
||||
:class="currentStep > index + 1 ? 'bg-green-500 text-white' : currentStep === index + 1 ? 'bg-indigo-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-500'">
|
||||
<template x-if="currentStep > index + 1">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep <= index + 1">
|
||||
<span x-text="index + 1"></span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="ml-2 text-sm font-medium hidden sm:inline"
|
||||
:class="currentStep >= index + 1 ? 'text-gray-900 dark:text-white' : 'text-gray-500'"
|
||||
x-text="stepName"></span>
|
||||
<template x-if="index < 3">
|
||||
<div class="flex-1 h-1 mx-4 bg-gray-200 dark:bg-gray-700 rounded">
|
||||
<div class="h-full bg-indigo-600 rounded transition-all"
|
||||
:style="'width: ' + (currentStep > index + 1 ? '100%' : '0%')"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Form Card #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
|
||||
{# ===============================================================
|
||||
STEP 1: SELECT PLAN
|
||||
=============================================================== #}
|
||||
<div x-show="currentStep === 1" class="p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Choose Your Plan</h2>
|
||||
|
||||
{# Billing Toggle #}
|
||||
<div class="flex items-center justify-center mb-8 space-x-4">
|
||||
<span :class="!isAnnual ? 'font-semibold text-gray-900 dark:text-white' : 'text-gray-500'">Monthly</span>
|
||||
<button @click="isAnnual = !isAnnual"
|
||||
class="relative w-12 h-6 rounded-full transition-colors"
|
||||
:class="isAnnual ? 'bg-indigo-600' : 'bg-gray-300'">
|
||||
<span class="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow transition-transform"
|
||||
:class="isAnnual ? 'translate-x-6' : ''"></span>
|
||||
</button>
|
||||
<span :class="isAnnual ? 'font-semibold text-gray-900 dark:text-white' : 'text-gray-500'">
|
||||
Annual <span class="text-green-600 text-xs">Save 17%</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Tier Options #}
|
||||
<div class="space-y-4">
|
||||
{% for tier in tiers %}
|
||||
{% if not tier.is_enterprise %}
|
||||
<label class="block">
|
||||
<input type="radio" name="tier" value="{{ tier.code }}"
|
||||
x-model="selectedTier" class="hidden peer"/>
|
||||
<div class="p-4 border-2 rounded-xl cursor-pointer transition-all
|
||||
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/20
|
||||
border-gray-200 dark:border-gray-700 hover:border-gray-300">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">{{ tier.name }}</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
{% if tier.orders_per_month %}{{ tier.orders_per_month }} orders/mo{% else %}Unlimited{% endif %}
|
||||
•
|
||||
{% if tier.team_members %}{{ tier.team_members }} user{% if tier.team_members > 1 %}s{% endif %}{% else %}Unlimited{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<template x-if="!isAnnual">
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ tier.price_monthly }}/mo</span>
|
||||
</template>
|
||||
<template x-if="isAnnual">
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white">{{ (tier.price_annual / 12)|round(0)|int }}/mo</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Free Trial Note #}
|
||||
<div class="mt-6 p-4 bg-green-50 dark:bg-green-900/20 rounded-xl">
|
||||
<p class="text-sm text-green-800 dark:text-green-300">
|
||||
<strong>{{ trial_days }}-day free trial.</strong>
|
||||
We'll collect your payment info, but you won't be charged until the trial ends.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button @click="startSignup()"
|
||||
:disabled="!selectedTier || loading"
|
||||
class="mt-8 w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl transition-colors disabled:opacity-50">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# ===============================================================
|
||||
STEP 2: CLAIM LETZSHOP SHOP (Optional)
|
||||
=============================================================== #}
|
||||
<div x-show="currentStep === 2" class="p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Connect Your Letzshop Shop</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">Optional: Link your Letzshop account to sync orders automatically.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
x-model="letzshopUrl"
|
||||
placeholder="letzshop.lu/vendors/your-shop"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"
|
||||
/>
|
||||
|
||||
<template x-if="letzshopVendor">
|
||||
<div class="p-4 bg-green-50 dark:bg-green-900/20 rounded-xl">
|
||||
<p class="text-green-800 dark:text-green-300">
|
||||
Found: <strong x-text="letzshopVendor.name"></strong>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="letzshopError">
|
||||
<div class="p-4 bg-red-50 dark:bg-red-900/20 rounded-xl">
|
||||
<p class="text-red-800 dark:text-red-300" x-text="letzshopError"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex gap-4">
|
||||
<button @click="currentStep = 1"
|
||||
class="flex-1 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-semibold rounded-xl">
|
||||
Back
|
||||
</button>
|
||||
<button @click="claimVendor()"
|
||||
:disabled="loading"
|
||||
class="flex-1 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl disabled:opacity-50">
|
||||
<span x-text="letzshopUrl.trim() ? 'Connect & Continue' : 'Skip This Step'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ===============================================================
|
||||
STEP 3: CREATE ACCOUNT
|
||||
=============================================================== #}
|
||||
<div x-show="currentStep === 3" class="p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Create Your Account</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">First Name</label>
|
||||
<input type="text" x-model="account.firstName" required
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Last Name</label>
|
||||
<input type="text" x-model="account.lastName" required
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Company Name</label>
|
||||
<input type="text" x-model="account.companyName" required
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
|
||||
<input type="email" x-model="account.email" required
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
|
||||
<input type="password" x-model="account.password" required minlength="8"
|
||||
class="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white"/>
|
||||
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
|
||||
</div>
|
||||
|
||||
<template x-if="accountError">
|
||||
<div class="p-4 bg-red-50 dark:bg-red-900/20 rounded-xl">
|
||||
<p class="text-red-800 dark:text-red-300" x-text="accountError"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex gap-4">
|
||||
<button @click="currentStep = 2"
|
||||
class="flex-1 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-semibold rounded-xl">
|
||||
Back
|
||||
</button>
|
||||
<button @click="createAccount()"
|
||||
:disabled="loading || !isAccountValid()"
|
||||
class="flex-1 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl disabled:opacity-50">
|
||||
Continue to Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ===============================================================
|
||||
STEP 4: PAYMENT
|
||||
=============================================================== #}
|
||||
<div x-show="currentStep === 4" class="p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Add Payment Method</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">You won't be charged until your {{ trial_days }}-day trial ends.</p>
|
||||
|
||||
{# Stripe Card Element #}
|
||||
<div id="card-element" class="p-4 border border-gray-300 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-900"></div>
|
||||
<div id="card-errors" class="text-red-600 text-sm mt-2"></div>
|
||||
|
||||
<div class="mt-8 flex gap-4">
|
||||
<button @click="currentStep = 3"
|
||||
class="flex-1 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 font-semibold rounded-xl">
|
||||
Back
|
||||
</button>
|
||||
<button @click="submitPayment()"
|
||||
:disabled="loading || paymentProcessing"
|
||||
class="flex-1 py-3 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-xl disabled:opacity-50">
|
||||
<template x-if="paymentProcessing">
|
||||
<span>Processing...</span>
|
||||
</template>
|
||||
<template x-if="!paymentProcessing">
|
||||
<span>Start Free Trial</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function signupWizard() {
|
||||
return {
|
||||
currentStep: 1,
|
||||
loading: false,
|
||||
sessionId: null,
|
||||
|
||||
// Step 1: Plan
|
||||
selectedTier: '{{ selected_tier or "professional" }}',
|
||||
isAnnual: {{ 'true' if is_annual else 'false' }},
|
||||
|
||||
// Step 2: Letzshop
|
||||
letzshopUrl: '',
|
||||
letzshopVendor: null,
|
||||
letzshopError: null,
|
||||
|
||||
// Step 3: Account
|
||||
account: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
companyName: '',
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
accountError: null,
|
||||
|
||||
// Step 4: Payment
|
||||
stripe: null,
|
||||
cardElement: null,
|
||||
paymentProcessing: false,
|
||||
clientSecret: null,
|
||||
|
||||
init() {
|
||||
// Check URL params for pre-selection
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('tier')) {
|
||||
this.selectedTier = params.get('tier');
|
||||
}
|
||||
if (params.get('annual') === 'true') {
|
||||
this.isAnnual = true;
|
||||
}
|
||||
if (params.get('letzshop')) {
|
||||
this.letzshopUrl = params.get('letzshop');
|
||||
}
|
||||
|
||||
// Initialize Stripe when we get to step 4
|
||||
this.$watch('currentStep', (step) => {
|
||||
if (step === 4) {
|
||||
this.initStripe();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async startSignup() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/v1/platform/signup/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tier_code: this.selectedTier,
|
||||
is_annual: this.isAnnual
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.sessionId = data.session_id;
|
||||
this.currentStep = 2;
|
||||
} else {
|
||||
alert(data.detail || 'Failed to start signup');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to start signup. Please try again.');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async claimVendor() {
|
||||
if (this.letzshopUrl.trim()) {
|
||||
this.loading = true;
|
||||
this.letzshopError = null;
|
||||
|
||||
try {
|
||||
// First lookup the vendor
|
||||
const lookupResponse = await fetch('/api/v1/platform/letzshop-vendors/lookup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: this.letzshopUrl })
|
||||
});
|
||||
|
||||
const lookupData = await lookupResponse.json();
|
||||
|
||||
if (lookupData.found && !lookupData.vendor.is_claimed) {
|
||||
this.letzshopVendor = lookupData.vendor;
|
||||
|
||||
// Claim the vendor
|
||||
const claimResponse = await fetch('/api/v1/platform/signup/claim-vendor', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: this.sessionId,
|
||||
letzshop_slug: lookupData.vendor.slug
|
||||
})
|
||||
});
|
||||
|
||||
if (claimResponse.ok) {
|
||||
const claimData = await claimResponse.json();
|
||||
this.account.companyName = claimData.vendor_name || '';
|
||||
this.currentStep = 3;
|
||||
} else {
|
||||
const error = await claimResponse.json();
|
||||
this.letzshopError = error.detail || 'Failed to claim vendor';
|
||||
}
|
||||
} else if (lookupData.vendor?.is_claimed) {
|
||||
this.letzshopError = 'This shop has already been claimed.';
|
||||
} else {
|
||||
this.letzshopError = lookupData.error || 'Shop not found.';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
this.letzshopError = 'Failed to lookup vendor.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
} else {
|
||||
// Skip this step
|
||||
this.currentStep = 3;
|
||||
}
|
||||
},
|
||||
|
||||
isAccountValid() {
|
||||
return this.account.firstName.trim() &&
|
||||
this.account.lastName.trim() &&
|
||||
this.account.companyName.trim() &&
|
||||
this.account.email.trim() &&
|
||||
this.account.password.length >= 8;
|
||||
},
|
||||
|
||||
async createAccount() {
|
||||
this.loading = true;
|
||||
this.accountError = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/platform/signup/create-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: this.sessionId,
|
||||
email: this.account.email,
|
||||
password: this.account.password,
|
||||
first_name: this.account.firstName,
|
||||
last_name: this.account.lastName,
|
||||
company_name: this.account.companyName
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.currentStep = 4;
|
||||
} else {
|
||||
this.accountError = data.detail || 'Failed to create account';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
this.accountError = 'Failed to create account. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async initStripe() {
|
||||
{% if stripe_publishable_key %}
|
||||
this.stripe = Stripe('{{ stripe_publishable_key }}');
|
||||
const elements = this.stripe.elements();
|
||||
|
||||
this.cardElement = elements.create('card', {
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#374151',
|
||||
'::placeholder': { color: '#9CA3AF' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.cardElement.mount('#card-element');
|
||||
this.cardElement.on('change', (event) => {
|
||||
const displayError = document.getElementById('card-errors');
|
||||
displayError.textContent = event.error ? event.error.message : '';
|
||||
});
|
||||
|
||||
// Get SetupIntent
|
||||
try {
|
||||
const response = await fetch('/api/v1/platform/signup/setup-payment', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_id: this.sessionId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
this.clientSecret = data.client_secret;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting SetupIntent:', error);
|
||||
}
|
||||
{% else %}
|
||||
console.warn('Stripe not configured');
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
async submitPayment() {
|
||||
if (!this.stripe || !this.clientSecret) {
|
||||
alert('Payment not configured. Please contact support.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.paymentProcessing = true;
|
||||
|
||||
try {
|
||||
const { setupIntent, error } = await this.stripe.confirmCardSetup(
|
||||
this.clientSecret,
|
||||
{ payment_method: { card: this.cardElement } }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
document.getElementById('card-errors').textContent = error.message;
|
||||
this.paymentProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete signup
|
||||
const response = await fetch('/api/v1/platform/signup/complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: this.sessionId,
|
||||
setup_intent_id: setupIntent.id
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
window.location.href = '/signup/success?vendor_code=' + data.vendor_code;
|
||||
} else {
|
||||
alert(data.detail || 'Failed to complete signup');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
alert('Payment failed. Please try again.');
|
||||
} finally {
|
||||
this.paymentProcessing = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user