Refactor website pricing toward external license service
This commit is contained in:
13198
package-lock.json
generated
13198
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,74 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import Stripe from "stripe";
|
|
||||||
|
export async function POST() {
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
return NextResponse.json(
|
||||||
apiVersion: "2026-03-25.dahlia",
|
{
|
||||||
});
|
error: "Website-local checkout is disabled. Use the external DashCaddy license service."
|
||||||
|
},
|
||||||
const PRICE_IDS: Record<string, string | undefined> = {
|
{ status: 410 }
|
||||||
monthly: process.env.STRIPE_PRICE_MONTHLY,
|
);
|
||||||
yearly: process.env.STRIPE_PRICE_YEARLY,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
const { plan, email } = body;
|
|
||||||
|
|
||||||
if (!plan || !PRICE_IDS[plan]) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid plan. Choose 'monthly' or 'yearly'." },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const priceId = PRICE_IDS[plan];
|
|
||||||
if (!priceId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Price not configured. Please contact support." },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://dashcaddy.net";
|
|
||||||
|
|
||||||
const sessionParams: Stripe.Checkout.SessionCreateParams = {
|
|
||||||
mode: "subscription",
|
|
||||||
payment_method_types: ["card"],
|
|
||||||
line_items: [
|
|
||||||
{
|
|
||||||
price: priceId,
|
|
||||||
quantity: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
success_url: `${appUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
||||||
cancel_url: `${appUrl}/pricing`,
|
|
||||||
allow_promotion_codes: true,
|
|
||||||
billing_address_collection: "required",
|
|
||||||
subscription_data: {
|
|
||||||
trial_period_days: 14,
|
|
||||||
metadata: {
|
|
||||||
plan,
|
|
||||||
source: "dashcaddy-website",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
plan,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pre-fill email if provided
|
|
||||||
if (email) {
|
|
||||||
sessionParams.customer_email = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
||||||
|
|
||||||
return NextResponse.json({ url: session.url });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Stripe checkout error:", error);
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : "Internal server error";
|
|
||||||
return NextResponse.json({ error: message }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,99 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import Stripe from "stripe";
|
|
||||||
|
export async function POST() {
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
return NextResponse.json(
|
||||||
apiVersion: "2026-03-25.dahlia",
|
{
|
||||||
});
|
error: "Website-local Stripe webhooks are disabled. Use the external DashCaddy license service."
|
||||||
|
},
|
||||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
{ status: 410 }
|
||||||
|
);
|
||||||
export async function POST(request: NextRequest) {
|
}
|
||||||
const body = await request.text();
|
|
||||||
const signature = request.headers.get("stripe-signature");
|
|
||||||
|
|
||||||
if (!signature) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Missing stripe-signature header" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let event: Stripe.Event;
|
|
||||||
|
|
||||||
try {
|
|
||||||
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : "Unknown error";
|
|
||||||
console.error(`Webhook signature verification failed: ${message}`);
|
|
||||||
return NextResponse.json({ error: message }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (event.type) {
|
|
||||||
case "checkout.session.completed": {
|
|
||||||
const session = event.data.object as Stripe.Checkout.Session;
|
|
||||||
console.log("Checkout completed:", {
|
|
||||||
sessionId: session.id,
|
|
||||||
customerEmail: session.customer_email,
|
|
||||||
plan: session.metadata?.plan,
|
|
||||||
subscriptionId: session.subscription,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Generate and deliver license key to customer
|
|
||||||
// This is where you'd:
|
|
||||||
// 1. Generate a DC-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX license code
|
|
||||||
// 2. Store it in your database
|
|
||||||
// 3. Email it to the customer
|
|
||||||
// 4. Associate it with the Stripe subscription ID
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "customer.subscription.updated": {
|
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
|
||||||
console.log("Subscription updated:", {
|
|
||||||
subscriptionId: subscription.id,
|
|
||||||
status: subscription.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Update license expiration based on subscription status
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "customer.subscription.deleted": {
|
|
||||||
const subscription = event.data.object as Stripe.Subscription;
|
|
||||||
console.log("Subscription cancelled:", {
|
|
||||||
subscriptionId: subscription.id,
|
|
||||||
status: subscription.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Deactivate/expire the license key
|
|
||||||
// The DashCaddy instance will gracefully downgrade to free tier
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "invoice.payment_failed": {
|
|
||||||
const invoice = event.data.object as Stripe.Invoice;
|
|
||||||
console.log("Payment failed:", {
|
|
||||||
invoiceId: invoice.id,
|
|
||||||
customerEmail: invoice.customer_email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Notify customer about failed payment
|
|
||||||
// Consider a grace period before deactivating license
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log(`Unhandled event type: ${event.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ received: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Webhook handler error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Webhook handler failed" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
1022
src/app/page.tsx
1022
src/app/page.tsx
File diff suppressed because it is too large
Load Diff
@@ -1,280 +1,169 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import PricingCards from '@/components/PricingCards';
|
||||||
import Navbar from '@/components/Navbar';
|
import Link from 'next/link';
|
||||||
import Footer from '@/components/Footer';
|
import Navbar from '@/components/Navbar';
|
||||||
|
import Footer from '@/components/Footer';
|
||||||
export default function PricingPage() {
|
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
export default function PricingPage() {
|
||||||
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
const plans = [
|
|
||||||
{
|
const faqs = [
|
||||||
name: 'Free',
|
{
|
||||||
price: '0',
|
question: 'Can I try Premium for free?',
|
||||||
period: 'forever',
|
answer: 'There is no free trial. Premium features unlock when you start a paid subscription.',
|
||||||
description: 'Perfect for getting started with self-hosting',
|
},
|
||||||
features: [
|
{
|
||||||
'Dashboard & monitoring',
|
question: 'What happens when my subscription ends?',
|
||||||
'Up to 10 services',
|
answer: 'If your subscription ends, DashCaddy falls back to the free tier. Your data remains intact.',
|
||||||
'50+ app templates',
|
},
|
||||||
'Automatic SSL & DNS',
|
{
|
||||||
'TOTP 2FA',
|
question: 'Can I self-host the license server?',
|
||||||
'Community support',
|
answer: 'Coming soon! We\'re working on a self-hosted license server option for enterprise deployments.',
|
||||||
],
|
},
|
||||||
cta: {
|
{
|
||||||
text: 'Get Started Free',
|
question: 'Do you offer refunds?',
|
||||||
href: '/docs',
|
answer: 'Yes, we offer a 30-day money-back guarantee. If you\'re not satisfied with Premium, contact support for a full refund.',
|
||||||
},
|
},
|
||||||
highlighted: false,
|
{
|
||||||
},
|
question: 'Is my data safe?',
|
||||||
{
|
answer: 'DashCaddy remains self-hosted for your infrastructure. Only billing and license validation touch the hosted licensing service.',
|
||||||
name: 'Premium',
|
},
|
||||||
price: isAnnual ? '99' : '20',
|
];
|
||||||
period: isAnnual ? 'per year' : 'per month',
|
|
||||||
savings: isAnnual ? 'Save 58%' : null,
|
const [expandedFaq, setExpandedFaq] = useState<number | null>(0);
|
||||||
description: 'For power users and production deployments',
|
|
||||||
features: [
|
return (
|
||||||
'Everything in Free, plus:',
|
<div className="flex flex-col min-h-screen bg-surface-950 text-surface-50">
|
||||||
'Unlimited services',
|
<Navbar />
|
||||||
'Auto-Login SSO for deployed apps',
|
|
||||||
'Recipes (multi-container stack deployment)',
|
{/* Hero Section */}
|
||||||
'Docker Swarm (multi-node cluster orchestration)',
|
<section className="relative py-16 sm:py-20 lg:py-24">
|
||||||
'Priority email support',
|
<div className="absolute inset-0 -z-10">
|
||||||
'Early access to new features',
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-brand-500/20 rounded-full blur-3xl opacity-30 animate-pulse" />
|
||||||
],
|
</div>
|
||||||
cta: {
|
|
||||||
text: 'Start 14-Day Free Trial',
|
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
|
||||||
href: '/api/checkout?plan=premium',
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold mb-6">
|
||||||
},
|
Simple, Transparent <span className="text-brand-400">Pricing</span>
|
||||||
highlighted: true,
|
</h1>
|
||||||
},
|
<p className="text-xl text-surface-300 mb-8 max-w-2xl mx-auto">
|
||||||
];
|
Start free. Upgrade when you need advanced features. No surprises, no lock-in.
|
||||||
|
</p>
|
||||||
const faqs = [
|
|
||||||
{
|
{/* Toggle for Monthly/Yearly */}
|
||||||
question: 'Can I try Premium for free?',
|
<div className="flex items-center justify-center gap-4 mb-12">
|
||||||
answer: 'Yes! Premium includes a 14-day free trial. No credit card required. You can cancel anytime.',
|
<span className={`text-sm font-medium ${!isAnnual ? 'text-surface-50' : 'text-surface-400'}`}>
|
||||||
},
|
Monthly
|
||||||
{
|
</span>
|
||||||
question: 'What happens when my subscription ends?',
|
<button
|
||||||
answer: 'Your subscription gracefully downgrades to the Free tier. All your data remains intact—no data loss. You can resubscribe at any time.',
|
onClick={() => setIsAnnual(!isAnnual)}
|
||||||
},
|
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||||||
{
|
isAnnual ? 'bg-brand-500' : 'bg-surface-700'
|
||||||
question: 'Can I self-host the license server?',
|
}`}
|
||||||
answer: 'Coming soon! We\'re working on a self-hosted license server option for enterprise deployments.',
|
aria-label="Toggle annual pricing"
|
||||||
},
|
>
|
||||||
{
|
<span
|
||||||
question: 'Do you offer refunds?',
|
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||||||
answer: 'Yes, we offer a 30-day money-back guarantee. If you\'re not satisfied with Premium, contact support for a full refund.',
|
isAnnual ? 'translate-x-7' : 'translate-x-1'
|
||||||
},
|
}`}
|
||||||
{
|
/>
|
||||||
question: 'Is my data safe?',
|
</button>
|
||||||
answer: '100% self-hosted means your data never leaves your server. DashCaddy runs entirely on your infrastructure. We have no access to your applications, configurations, or data.',
|
<span className={`text-sm font-medium ${isAnnual ? 'text-surface-50' : 'text-surface-400'}`}>
|
||||||
},
|
Annual
|
||||||
];
|
</span>
|
||||||
|
{isAnnual && (
|
||||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(0);
|
<span className="ml-2 inline-block rounded-full bg-brand-500/20 px-3 py-1 text-sm font-semibold text-brand-300">
|
||||||
|
Best Value
|
||||||
return (
|
</span>
|
||||||
<div className="flex flex-col min-h-screen bg-surface-950 text-surface-50">
|
)}
|
||||||
<Navbar />
|
</div>
|
||||||
|
</div>
|
||||||
{/* Hero Section */}
|
</section>
|
||||||
<section className="relative py-16 sm:py-20 lg:py-24">
|
|
||||||
<div className="absolute inset-0 -z-10">
|
{/* Pricing Cards */}
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-brand-500/20 rounded-full blur-3xl opacity-30 animate-pulse" />
|
<section className="relative py-12 sm:py-16 lg:py-20">
|
||||||
</div>
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<PricingCards />
|
||||||
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
|
</div>
|
||||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold mb-6">
|
</section>
|
||||||
Simple, Transparent <span className="text-brand-400">Pricing</span>
|
|
||||||
</h1>
|
{/* FAQ Section */}
|
||||||
<p className="text-xl text-surface-300 mb-8 max-w-2xl mx-auto">
|
<section className="relative py-16 sm:py-20 lg:py-24 bg-gradient-to-b from-surface-950 to-surface-900">
|
||||||
Start free. Upgrade when you need advanced features. No surprises, no lock-in.
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
||||||
</p>
|
<div className="mb-12 text-center">
|
||||||
|
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4">
|
||||||
{/* Toggle for Monthly/Yearly */}
|
Frequently Asked <span className="text-brand-400">Questions</span>
|
||||||
<div className="flex items-center justify-center gap-4 mb-12">
|
</h2>
|
||||||
<span className={`text-sm font-medium ${!isAnnual ? 'text-surface-50' : 'text-surface-400'}`}>
|
<p className="text-lg text-surface-400">
|
||||||
Monthly
|
Have a question? We've got answers.
|
||||||
</span>
|
</p>
|
||||||
<button
|
</div>
|
||||||
onClick={() => setIsAnnual(!isAnnual)}
|
|
||||||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
{/* FAQ Accordion */}
|
||||||
isAnnual ? 'bg-brand-500' : 'bg-surface-700'
|
<div className="space-y-4">
|
||||||
}`}
|
{faqs.map((faq, idx) => (
|
||||||
aria-label="Toggle annual pricing"
|
<div
|
||||||
>
|
key={idx}
|
||||||
<span
|
className="rounded-lg border border-surface-700/50 bg-surface-800/50 overflow-hidden transition-all duration-200 hover:border-surface-700"
|
||||||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
>
|
||||||
isAnnual ? 'translate-x-7' : 'translate-x-1'
|
<button
|
||||||
}`}
|
onClick={() => setExpandedFaq(expandedFaq === idx ? null : idx)}
|
||||||
/>
|
className="w-full px-6 py-4 flex items-center justify-between hover:bg-surface-800/70 transition-colors"
|
||||||
</button>
|
>
|
||||||
<span className={`text-sm font-medium ${isAnnual ? 'text-surface-50' : 'text-surface-400'}`}>
|
<h3 className="text-lg font-semibold text-surface-50 text-left">{faq.question}</h3>
|
||||||
Annual
|
<svg
|
||||||
</span>
|
className={`h-6 w-6 flex-shrink-0 text-brand-400 transition-transform duration-200 ${
|
||||||
{isAnnual && (
|
expandedFaq === idx ? 'rotate-180' : ''
|
||||||
<span className="ml-2 inline-block rounded-full bg-brand-500/20 px-3 py-1 text-sm font-semibold text-brand-300">
|
}`}
|
||||||
Best Value
|
fill="none"
|
||||||
</span>
|
viewBox="0 0 24 24"
|
||||||
)}
|
strokeWidth={2}
|
||||||
</div>
|
stroke="currentColor"
|
||||||
</div>
|
>
|
||||||
</section>
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
{/* Pricing Cards */}
|
</button>
|
||||||
<section className="relative py-12 sm:py-16 lg:py-20">
|
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
{expandedFaq === idx && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
<div className="border-t border-surface-700/50 bg-surface-900/50 px-6 py-4">
|
||||||
{plans.map((plan, idx) => (
|
<p className="text-surface-300 leading-relaxed">{faq.answer}</p>
|
||||||
<div
|
</div>
|
||||||
key={idx}
|
)}
|
||||||
className={`relative rounded-2xl border transition-all duration-300 ${
|
</div>
|
||||||
plan.highlighted
|
))}
|
||||||
? 'border-brand-500/50 bg-gradient-to-br from-surface-800 to-surface-900 shadow-2xl shadow-brand-500/20 scale-105 md:scale-105'
|
</div>
|
||||||
: 'border-surface-700/50 bg-surface-800/50 hover:border-surface-700 hover:bg-surface-800/80'
|
</div>
|
||||||
}`}
|
</section>
|
||||||
>
|
|
||||||
{/* Popular Badge */}
|
{/* Final CTA */}
|
||||||
{plan.highlighted && (
|
<section className="relative py-16 sm:py-20 lg:py-24 bg-surface-950">
|
||||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
|
||||||
<span className="inline-block rounded-full bg-brand-500 px-4 py-1 text-xs font-bold uppercase tracking-wide text-white">
|
<h2 className="text-3xl sm:text-4xl font-bold mb-6">
|
||||||
Most Popular
|
Ready to Get Started?
|
||||||
</span>
|
</h2>
|
||||||
</div>
|
<p className="text-xl text-surface-300 mb-8 max-w-2xl mx-auto">
|
||||||
)}
|
Use DashCaddy free for core self-hosting, or unlock Premium for gated features.
|
||||||
|
</p>
|
||||||
<div className="p-8 sm:p-10">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
{/* Header */}
|
<Link
|
||||||
<div className="mb-8">
|
href="/docs"
|
||||||
<h3 className="text-2xl font-bold text-surface-50 mb-2">{plan.name}</h3>
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-8 py-3 text-base font-semibold text-white hover:bg-brand-600 transition-all duration-200 hover:shadow-lg hover:shadow-brand-500/30"
|
||||||
<p className="text-surface-400 text-sm mb-6">{plan.description}</p>
|
>
|
||||||
|
View Documentation
|
||||||
{/* Price */}
|
</Link>
|
||||||
<div className="flex items-baseline gap-2 mb-2">
|
<Link
|
||||||
<span className="text-5xl font-bold text-surface-50">${plan.price}</span>
|
href="/"
|
||||||
<span className="text-surface-400">/{plan.period}</span>
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-surface-700 bg-surface-800/50 px-8 py-3 text-base font-semibold text-surface-50 hover:border-brand-400 hover:bg-surface-800 transition-colors"
|
||||||
</div>
|
>
|
||||||
{plan.savings && (
|
Back to Home
|
||||||
<p className="text-sm text-brand-400 font-semibold">{plan.savings}</p>
|
</Link>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
{/* CTA Button */}
|
|
||||||
<Link
|
<Footer />
|
||||||
href={plan.cta.href}
|
</div>
|
||||||
className={`block w-full rounded-lg px-6 py-3 text-center font-semibold transition-all duration-200 mb-8 ${
|
);
|
||||||
plan.highlighted
|
}
|
||||||
? 'bg-brand-500 text-white hover:bg-brand-600 hover:shadow-lg hover:shadow-brand-500/30'
|
|
||||||
: 'border border-surface-700 bg-surface-700/50 text-surface-50 hover:border-brand-400 hover:bg-surface-700 hover:text-brand-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{plan.cta.text}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Features List */}
|
|
||||||
<div className="border-t border-surface-700/50 pt-8">
|
|
||||||
<ul className="space-y-4">
|
|
||||||
{plan.features.map((feature, featureIdx) => (
|
|
||||||
<li key={featureIdx} className="flex items-start gap-3">
|
|
||||||
{feature.startsWith('Everything in') ? (
|
|
||||||
<span className="text-sm font-semibold text-surface-300">{feature}</span>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg className="h-5 w-5 flex-shrink-0 text-green-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="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" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-surface-300 text-sm">{feature}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* FAQ Section */}
|
|
||||||
<section className="relative py-16 sm:py-20 lg:py-24 bg-gradient-to-b from-surface-950 to-surface-900">
|
|
||||||
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="mb-12 text-center">
|
|
||||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4">
|
|
||||||
Frequently Asked <span className="text-brand-400">Questions</span>
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-surface-400">
|
|
||||||
Have a question? We've got answers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FAQ Accordion */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{faqs.map((faq, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="rounded-lg border border-surface-700/50 bg-surface-800/50 overflow-hidden transition-all duration-200 hover:border-surface-700"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setExpandedFaq(expandedFaq === idx ? null : idx)}
|
|
||||||
className="w-full px-6 py-4 flex items-center justify-between hover:bg-surface-800/70 transition-colors"
|
|
||||||
>
|
|
||||||
<h3 className="text-lg font-semibold text-surface-50 text-left">{faq.question}</h3>
|
|
||||||
<svg
|
|
||||||
className={`h-6 w-6 flex-shrink-0 text-brand-400 transition-transform duration-200 ${
|
|
||||||
expandedFaq === idx ? 'rotate-180' : ''
|
|
||||||
}`}
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{expandedFaq === idx && (
|
|
||||||
<div className="border-t border-surface-700/50 bg-surface-900/50 px-6 py-4">
|
|
||||||
<p className="text-surface-300 leading-relaxed">{faq.answer}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Final CTA */}
|
|
||||||
<section className="relative py-16 sm:py-20 lg:py-24 bg-surface-950">
|
|
||||||
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
|
|
||||||
<h2 className="text-3xl sm:text-4xl font-bold mb-6">
|
|
||||||
Ready to Get Started?
|
|
||||||
</h2>
|
|
||||||
<p className="text-xl text-surface-300 mb-8 max-w-2xl mx-auto">
|
|
||||||
Try DashCaddy free forever or upgrade to Premium for advanced features.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
||||||
<Link
|
|
||||||
href="/docs"
|
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-8 py-3 text-base font-semibold text-white hover:bg-brand-600 transition-all duration-200 hover:shadow-lg hover:shadow-brand-500/30"
|
|
||||||
>
|
|
||||||
View Documentation
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-surface-700 bg-surface-800/50 px-8 py-3 text-base font-semibold text-surface-50 hover:border-brand-400 hover:bg-surface-800 transition-colors"
|
|
||||||
>
|
|
||||||
Back to Home
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,121 +1,121 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
function SuccessContent() {
|
function SuccessContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const sessionId = searchParams.get("session_id");
|
const sessionId = searchParams.get("session_id");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main className="flex-1 flex items-center justify-center px-4 py-24">
|
<main className="flex-1 flex items-center justify-center px-4 py-24">
|
||||||
<div className="max-w-lg w-full text-center">
|
<div className="max-w-lg w-full text-center">
|
||||||
{/* Success icon */}
|
{/* Success icon */}
|
||||||
<div className="mx-auto w-20 h-20 rounded-full bg-green-500/20 flex items-center justify-center mb-8">
|
<div className="mx-auto w-20 h-20 rounded-full bg-green-500/20 flex items-center justify-center mb-8">
|
||||||
<svg
|
<svg
|
||||||
className="w-10 h-10 text-green-400"
|
className="w-10 h-10 text-green-400"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
d="M5 13l4 4L19 7"
|
d="M5 13l4 4L19 7"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold text-white mb-4">
|
<h1 className="text-4xl font-bold text-white mb-4">
|
||||||
Welcome to DashCaddy Premium!
|
Welcome to DashCaddy Premium!
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-lg text-surface-300 mb-8">
|
<p className="text-lg text-surface-300 mb-8">
|
||||||
Your subscription is active. Check your email for your license key
|
Your subscription is active. Check your email for your license key
|
||||||
and setup instructions. Your 14-day free trial has started.
|
and setup instructions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="glass-card rounded-xl p-6 mb-8 text-left">
|
<div className="glass-card rounded-xl p-6 mb-8 text-left">
|
||||||
<h2 className="text-lg font-semibold text-white mb-4">
|
<h2 className="text-lg font-semibold text-white mb-4">
|
||||||
Next Steps
|
Next Steps
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="space-y-3 text-surface-300">
|
<ol className="space-y-3 text-surface-300">
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
||||||
1
|
1
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Check your email for your license key (DC-XXXXX-...)
|
Check your email for your license key (DC-XXXXX-...)
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
||||||
2
|
2
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Open your DashCaddy dashboard and go to Admin → License
|
Open your DashCaddy dashboard and go to Admin → License
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
||||||
3
|
3
|
||||||
</span>
|
</span>
|
||||||
<span>Paste your license key and click Activate</span>
|
<span>Paste your license key and click Activate</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-500/20 text-brand-400 text-sm flex items-center justify-center font-medium">
|
||||||
4
|
4
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Enjoy SSO, Recipes, Docker Swarm, and all premium features!
|
Enjoy SSO, Recipes, Docker Swarm, and all premium features!
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sessionId && (
|
{sessionId && (
|
||||||
<p className="text-sm text-surface-500 mb-6">
|
<p className="text-sm text-surface-500 mb-6">
|
||||||
Session ID: {sessionId.substring(0, 20)}...
|
Session ID: {sessionId.substring(0, 20)}...
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<Link
|
<Link
|
||||||
href="/docs"
|
href="/docs"
|
||||||
className="px-6 py-3 rounded-lg bg-brand-600 hover:bg-brand-500 text-white font-medium transition-colors"
|
className="px-6 py-3 rounded-lg bg-brand-600 hover:bg-brand-500 text-white font-medium transition-colors"
|
||||||
>
|
>
|
||||||
View Setup Guide
|
View Setup Guide
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="px-6 py-3 rounded-lg border border-surface-700 hover:border-surface-500 text-surface-300 font-medium transition-colors"
|
className="px-6 py-3 rounded-lg border border-surface-700 hover:border-surface-500 text-surface-300 font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Back to Home
|
Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SuccessPage() {
|
export default function SuccessPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-surface-400">Loading...</div>
|
<div className="text-surface-400">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SuccessContent />
|
<SuccessContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +1,129 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const footerSections = [
|
const footerSections = [
|
||||||
{
|
{
|
||||||
title: 'Product',
|
title: 'Product',
|
||||||
links: [
|
links: [
|
||||||
{ label: 'Features', href: '/features' },
|
{ label: 'Features', href: '/features' },
|
||||||
{ label: 'Pricing', href: '/pricing' },
|
{ label: 'Pricing', href: '/pricing' },
|
||||||
{ label: 'Documentation', href: '/docs' },
|
{ label: 'Documentation', href: '/docs' },
|
||||||
{ label: 'About', href: '/about' },
|
{ label: 'About', href: '/about' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Resources',
|
title: 'Resources',
|
||||||
links: [
|
links: [
|
||||||
{ label: 'Getting Started', href: '/docs' },
|
{ label: 'Getting Started', href: '/docs' },
|
||||||
{ label: 'API Reference', href: '/docs#api' },
|
{ label: 'API Reference', href: '/docs#api' },
|
||||||
{ label: 'GitHub', href: 'https://git.dashcaddy.net/sami7777/dashcaddy' },
|
{ label: 'Community', href: '#' },
|
||||||
{ label: 'Community', href: '#' },
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Support',
|
||||||
title: 'Support',
|
links: [
|
||||||
links: [
|
{ label: 'Contact', href: 'mailto:support@dashcaddy.net' },
|
||||||
{ label: 'Contact', href: 'mailto:support@dashcaddy.net' },
|
{ label: 'Discord', href: '#' },
|
||||||
{ label: 'Discord', href: '#' },
|
{ label: 'Privacy Policy', href: '#' },
|
||||||
{ label: 'Privacy Policy', href: '#' },
|
{ label: 'Terms of Service', href: '#' },
|
||||||
{ label: 'Terms of Service', href: '#' },
|
],
|
||||||
],
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
const socialLinks = [
|
||||||
const socialLinks = [
|
{
|
||||||
{
|
icon: (
|
||||||
icon: (
|
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
<path d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515a.074.074 0 00-.079.037c-.211.375-.444.864-.607 1.25a18.27 18.27 0 00-5.487 0c-.163-.386-.395-.875-.607-1.25a.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028c.462-.63.873-1.295 1.226-1.994a.076.076 0 00-.042-.106 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.294.075.075 0 01.078-.01c3.928 1.793 8.18 1.793 12.062 0a.075.075 0 01.079.009c.12.098.246.198.373.295a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.892.076.076 0 00-.041.107c.359.698.77 1.364 1.225 1.994a.077.077 0 00.084.028 19.839 19.839 0 006.002-3.03.076.076 0 00.032-.057c.534-4.506-.9-8.4-3.821-11.865a.055.055 0 00-.032-.027zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.948-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.948 2.419-2.157 2.419zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.948-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.948 2.419-2.157 2.419z" />
|
||||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.6.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
</svg>
|
||||||
</svg>
|
),
|
||||||
),
|
label: 'Discord',
|
||||||
label: 'GitHub',
|
href: '#',
|
||||||
href: 'https://git.dashcaddy.net/sami7777/dashcaddy',
|
},
|
||||||
},
|
];
|
||||||
{
|
|
||||||
icon: (
|
return (
|
||||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
<footer className="border-t border-surface-700/50 bg-surface-950">
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515a.074.074 0 00-.079.037c-.211.375-.444.864-.607 1.25a18.27 18.27 0 00-5.487 0c-.163-.386-.395-.875-.607-1.25a.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028c.462-.63.873-1.295 1.226-1.994a.076.076 0 00-.042-.106 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.294.075.075 0 01.078-.01c3.928 1.793 8.18 1.793 12.062 0a.075.075 0 01.079.009c.12.098.246.198.373.295a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.892.076.076 0 00-.041.107c.359.698.77 1.364 1.225 1.994a.077.077 0 00.084.028 19.839 19.839 0 006.002-3.03.076.076 0 00.032-.057c.534-4.506-.9-8.4-3.821-11.865a.055.055 0 00-.032-.027zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.948-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.948 2.419-2.157 2.419zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.948-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.948 2.419-2.157 2.419z" />
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
|
||||||
</svg>
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||||
),
|
{/* Brand Section */}
|
||||||
label: 'Discord',
|
<div>
|
||||||
href: '#',
|
<Link href="/" className="flex items-center gap-2 font-bold text-lg text-brand-400 hover:text-brand-300 transition-colors mb-4">
|
||||||
},
|
<span>DashCaddy</span>
|
||||||
];
|
</Link>
|
||||||
|
<p className="text-surface-400 text-sm leading-relaxed mb-4">
|
||||||
return (
|
Self-hosted Docker dashboard with automatic SSL, DNS, and reverse proxy. Making self-hosting beautiful and effortless.
|
||||||
<footer className="border-t border-surface-700/50 bg-surface-950">
|
</p>
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
|
<div className="flex items-center gap-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
{socialLinks.map((link) => (
|
||||||
{/* Brand Section */}
|
<a
|
||||||
<div>
|
key={link.label}
|
||||||
<Link href="/" className="flex items-center gap-2 font-bold text-lg text-brand-400 hover:text-brand-300 transition-colors mb-4">
|
href={link.href}
|
||||||
<span>DashCaddy</span>
|
className="text-surface-400 hover:text-brand-400 transition-colors"
|
||||||
</Link>
|
aria-label={link.label}
|
||||||
<p className="text-surface-400 text-sm leading-relaxed mb-4">
|
target="_blank"
|
||||||
Self-hosted Docker dashboard with automatic SSL, DNS, and reverse proxy. Making self-hosting beautiful and effortless.
|
rel="noopener noreferrer"
|
||||||
</p>
|
>
|
||||||
<div className="flex items-center gap-4">
|
{link.icon}
|
||||||
{socialLinks.map((link) => (
|
</a>
|
||||||
<a
|
))}
|
||||||
key={link.label}
|
</div>
|
||||||
href={link.href}
|
</div>
|
||||||
className="text-surface-400 hover:text-brand-400 transition-colors"
|
|
||||||
aria-label={link.label}
|
{/* Link Sections */}
|
||||||
target="_blank"
|
{footerSections.map((section) => (
|
||||||
rel="noopener noreferrer"
|
<div key={section.title}>
|
||||||
>
|
<h3 className="text-sm font-semibold text-surface-50 mb-4">
|
||||||
{link.icon}
|
{section.title}
|
||||||
</a>
|
</h3>
|
||||||
))}
|
<ul className="space-y-3">
|
||||||
</div>
|
{section.links.map((link) => (
|
||||||
</div>
|
<li key={link.label}>
|
||||||
|
<Link
|
||||||
{/* Link Sections */}
|
href={link.href}
|
||||||
{footerSections.map((section) => (
|
className="text-sm text-surface-400 hover:text-brand-400 transition-colors"
|
||||||
<div key={section.title}>
|
>
|
||||||
<h3 className="text-sm font-semibold text-surface-50 mb-4">
|
{link.label}
|
||||||
{section.title}
|
</Link>
|
||||||
</h3>
|
</li>
|
||||||
<ul className="space-y-3">
|
))}
|
||||||
{section.links.map((link) => (
|
</ul>
|
||||||
<li key={link.label}>
|
</div>
|
||||||
<Link
|
))}
|
||||||
href={link.href}
|
</div>
|
||||||
className="text-sm text-surface-400 hover:text-brand-400 transition-colors"
|
|
||||||
>
|
{/* Support Link */}
|
||||||
{link.label}
|
<div className="border-t border-surface-700/50 pt-8 mb-8">
|
||||||
</Link>
|
<p className="text-sm text-surface-400">
|
||||||
</li>
|
Need help? Email us at{' '}
|
||||||
))}
|
<a
|
||||||
</ul>
|
href="mailto:support@dashcaddy.net"
|
||||||
</div>
|
className="text-brand-400 hover:text-brand-300 transition-colors font-medium"
|
||||||
))}
|
>
|
||||||
</div>
|
support@dashcaddy.net
|
||||||
|
</a>
|
||||||
{/* Support Link */}
|
</p>
|
||||||
<div className="border-t border-surface-700/50 pt-8 mb-8">
|
</div>
|
||||||
<p className="text-sm text-surface-400">
|
|
||||||
Need help? Email us at{' '}
|
{/* Copyright with samiahmed7777 logo */}
|
||||||
<a
|
<div className="border-t border-surface-700/50 pt-8">
|
||||||
href="mailto:support@dashcaddy.net"
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||||
className="text-brand-400 hover:text-brand-300 transition-colors font-medium"
|
<p className="text-sm text-surface-500">
|
||||||
>
|
© {currentYear} DashCaddy. All rights reserved. A product by
|
||||||
support@dashcaddy.net
|
</p>
|
||||||
</a>
|
<Image
|
||||||
</p>
|
src="/images/samiahmed7777-logo.png"
|
||||||
</div>
|
alt="samiahmed7777"
|
||||||
|
width={160}
|
||||||
{/* Copyright with samiahmed7777 logo */}
|
height={32}
|
||||||
<div className="border-t border-surface-700/50 pt-8">
|
className="h-7 w-auto opacity-80 hover:opacity-100 transition-opacity"
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
/>
|
||||||
<p className="text-sm text-surface-500">
|
</div>
|
||||||
© {currentYear} DashCaddy. All rights reserved. A product by
|
</div>
|
||||||
</p>
|
</div>
|
||||||
<Image
|
</footer>
|
||||||
src="/images/samiahmed7777-logo.png"
|
);
|
||||||
alt="samiahmed7777"
|
}
|
||||||
width={160}
|
|
||||||
height={32}
|
|
||||||
className="h-7 w-auto opacity-80 hover:opacity-100 transition-opacity"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
185
src/components/PricingCards.tsx
Normal file
185
src/components/PricingCards.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { createCheckoutSession } from '@/lib/licenseServer';
|
||||||
|
|
||||||
|
type Plan = {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
period: string;
|
||||||
|
description: string;
|
||||||
|
savings?: string | null;
|
||||||
|
highlighted?: boolean;
|
||||||
|
features: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PricingCards() {
|
||||||
|
const [loadingPlan, setLoadingPlan] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const plans = useMemo<Plan[]>(() => [
|
||||||
|
{
|
||||||
|
code: 'free',
|
||||||
|
name: 'Free',
|
||||||
|
price: '$0',
|
||||||
|
period: 'forever',
|
||||||
|
description: 'Everything you need to self-host DashCaddy for core use.',
|
||||||
|
features: [
|
||||||
|
'Dashboard and monitoring',
|
||||||
|
'Container deployment',
|
||||||
|
'Caddy management',
|
||||||
|
'DNS management',
|
||||||
|
'Health checks',
|
||||||
|
'App templates'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'premium_1m',
|
||||||
|
name: 'Premium, 1 Month',
|
||||||
|
price: '$25',
|
||||||
|
period: 'per month',
|
||||||
|
description: 'Premium feature access for one month.',
|
||||||
|
features: [
|
||||||
|
'Auto-Login SSO',
|
||||||
|
'Recipes (multi-container stack deployment)',
|
||||||
|
'Docker Swarm orchestration'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'premium_3m',
|
||||||
|
name: 'Premium, 3 Months',
|
||||||
|
price: '$50',
|
||||||
|
period: 'every 3 months',
|
||||||
|
savings: 'Best short-term value',
|
||||||
|
description: 'A better value plan for regular use.',
|
||||||
|
highlighted: true,
|
||||||
|
features: [
|
||||||
|
'Auto-Login SSO',
|
||||||
|
'Recipes (multi-container stack deployment)',
|
||||||
|
'Docker Swarm orchestration'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'premium_6m',
|
||||||
|
name: 'Premium, 6 Months',
|
||||||
|
price: '$65',
|
||||||
|
period: 'every 6 months',
|
||||||
|
savings: 'Strong loyalty pricing',
|
||||||
|
description: 'The commitment plan with aggressive value.',
|
||||||
|
features: [
|
||||||
|
'Auto-Login SSO',
|
||||||
|
'Recipes (multi-container stack deployment)',
|
||||||
|
'Docker Swarm orchestration'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'premium_12m',
|
||||||
|
name: 'Premium, 12 Months',
|
||||||
|
price: '$99',
|
||||||
|
period: 'per year',
|
||||||
|
savings: 'Best overall value',
|
||||||
|
description: 'The best-value annual Premium plan.',
|
||||||
|
features: [
|
||||||
|
'Auto-Login SSO',
|
||||||
|
'Recipes (multi-container stack deployment)',
|
||||||
|
'Docker Swarm orchestration'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
], []);
|
||||||
|
|
||||||
|
async function handleCheckout(planCode: string) {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setLoadingPlan(planCode);
|
||||||
|
const result = await createCheckoutSession(planCode);
|
||||||
|
window.location.href = result.url;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unable to start checkout');
|
||||||
|
} finally {
|
||||||
|
setLoadingPlan(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-6 max-w-7xl mx-auto">
|
||||||
|
{plans.map((plan) => {
|
||||||
|
const isFree = plan.code === 'free';
|
||||||
|
const isLoading = loadingPlan === plan.code;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={plan.code}
|
||||||
|
className={`relative rounded-2xl border transition-all duration-300 ${
|
||||||
|
plan.highlighted
|
||||||
|
? 'border-brand-500/50 bg-gradient-to-br from-surface-800 to-surface-900 shadow-2xl shadow-brand-500/20'
|
||||||
|
: 'border-surface-700/50 bg-surface-800/50 hover:border-surface-700 hover:bg-surface-800/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{plan.highlighted && (
|
||||||
|
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
|
||||||
|
<span className="inline-block rounded-full bg-brand-500 px-4 py-1 text-xs font-bold uppercase tracking-wide text-white">
|
||||||
|
Recommended
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-2xl font-bold text-surface-50 mb-2">{plan.name}</h3>
|
||||||
|
<p className="text-surface-400 text-sm mb-6">{plan.description}</p>
|
||||||
|
<div className="flex items-baseline gap-2 mb-2">
|
||||||
|
<span className="text-4xl font-bold text-surface-50">{plan.price}</span>
|
||||||
|
<span className="text-surface-400 text-sm">/{plan.period}</span>
|
||||||
|
</div>
|
||||||
|
{plan.savings && <p className="text-sm text-brand-400 font-semibold">{plan.savings}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFree ? (
|
||||||
|
<a
|
||||||
|
href="/docs"
|
||||||
|
className="block w-full rounded-lg px-6 py-3 text-center font-semibold transition-all duration-200 mb-8 border border-surface-700 bg-surface-700/50 text-surface-50 hover:border-brand-400 hover:bg-surface-700 hover:text-brand-400"
|
||||||
|
>
|
||||||
|
Use Free Tier
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCheckout(plan.code)}
|
||||||
|
disabled={Boolean(loadingPlan)}
|
||||||
|
className={`block w-full rounded-lg px-6 py-3 text-center font-semibold transition-all duration-200 mb-8 ${
|
||||||
|
plan.highlighted
|
||||||
|
? 'bg-brand-500 text-white hover:bg-brand-600 hover:shadow-lg hover:shadow-brand-500/30'
|
||||||
|
: 'border border-surface-700 bg-surface-700/50 text-surface-50 hover:border-brand-400 hover:bg-surface-700 hover:text-brand-400'
|
||||||
|
} disabled:opacity-60 disabled:cursor-not-allowed`}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Opening Checkout...' : 'Buy Premium'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-surface-700/50 pt-8">
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{plan.features.map((feature) => (
|
||||||
|
<li key={feature} className="flex items-start gap-3">
|
||||||
|
<svg className="h-5 w-5 flex-shrink-0 text-green-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="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" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-surface-300 text-sm">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/lib/licenseServer.ts
Normal file
20
src/lib/licenseServer.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export const LICENSE_SERVER_URL =
|
||||||
|
process.env.NEXT_PUBLIC_LICENSE_SERVER_URL || 'https://licenses.dashcaddy.net';
|
||||||
|
|
||||||
|
export async function createCheckoutSession(planCode: string, customerEmail?: string) {
|
||||||
|
const response = await fetch(`${LICENSE_SERVER_URL}/api/checkout/session`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ planCode, customerEmail })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data?.url) {
|
||||||
|
throw new Error(data?.error || 'Unable to start checkout');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user