nbcvhc
This commit is contained in:
@@ -92,41 +92,60 @@ export default function PricingTable({
|
||||
return (
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
{title && (
|
||||
<div className="mx-auto w-full max-w-[385px]">
|
||||
<h2 className="font-bold text-center text-gray-800 mb-7 text-title-sm dark:text-white/90">
|
||||
<div className="mx-auto w-full">
|
||||
<h2 className="font-bold text-center text-gray-800 text-2xl mb-5 dark:text-white/90">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
{showToggle && (
|
||||
<div className="mb-10 text-center">
|
||||
<div className="relative inline-flex p-1 mx-auto bg-gray-200 rounded-full z-1 dark:bg-gray-800">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="relative inline-flex p-1 bg-gray-200 rounded-full dark:bg-gray-800 shadow-sm">
|
||||
<span
|
||||
className={`absolute top-1/2 -z-1 flex h-11 w-[120px] -translate-y-1/2 rounded-full bg-white shadow-theme-xs duration-200 ease-linear dark:bg-white/10 ${
|
||||
billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-[120px]'
|
||||
}`}
|
||||
className="absolute top-1 left-1 flex h-11 w-[130px] rounded-full shadow-theme-xs duration-200 ease-linear"
|
||||
style={{
|
||||
background: 'linear-gradient(to bottom right, #0693e3, #0472b8)',
|
||||
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
|
||||
}}
|
||||
></span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingPeriod('monthly')}
|
||||
className={`flex h-11 w-[120px] items-center justify-center text-base font-medium transition-colors ${
|
||||
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full ${
|
||||
billingPeriod === 'monthly'
|
||||
? 'text-gray-800 dark:text-white/90'
|
||||
? 'text-white'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingPeriod('annually')}
|
||||
className={`flex h-11 w-[120px] items-center justify-center text-base font-medium transition-colors ${
|
||||
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full ${
|
||||
billingPeriod === 'annually'
|
||||
? 'text-gray-800 dark:text-white/90'
|
||||
? 'text-white'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Annually
|
||||
</button>
|
||||
</div>
|
||||
{billingPeriod === 'annually' && (
|
||||
<div className="flex items-center justify-center mt-3">
|
||||
<span className="inline-flex items-center gap-1.5 text-green-600 dark:text-green-400 font-semibold bg-green-50 dark:bg-green-900/20 px-3 py-1.5 rounded-full text-sm">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Save 15% with annual billing
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:gap-6">
|
||||
@@ -218,7 +237,7 @@ export default function PricingTable({
|
||||
: 'bg-gray-800 hover:bg-brand-500 dark:bg-white/10 dark:hover:bg-brand-600'
|
||||
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{plan.buttonText || 'Choose Plan'}
|
||||
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,108 +1,92 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
RocketLaunchIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
SparklesIcon,
|
||||
PhotoIcon,
|
||||
BoltIcon,
|
||||
ChartBarIcon,
|
||||
UserGroupIcon,
|
||||
CreditCardIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import SEO from "../components/SEO";
|
||||
import { getMetaTags } from "../config/metaTags";
|
||||
import { getPublicPlans } from "../../services/billing.api";
|
||||
import PricingTable, { PricingPlan } from "../../components/ui/pricing-table/PricingTable";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Pricing: React.FC = () => {
|
||||
const renderCta = (cta: { label: string; href: string }, className: string) => {
|
||||
const isExternal = cta.href.startsWith("http");
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
href={cta.href}
|
||||
className={className}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{cta.label}
|
||||
</a>
|
||||
);
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
price: number | string;
|
||||
annual_discount_percent?: number;
|
||||
is_featured?: boolean;
|
||||
max_sites?: number;
|
||||
max_users?: number;
|
||||
max_keywords?: number;
|
||||
max_clusters?: number;
|
||||
max_content_ideas?: number;
|
||||
max_content_words?: number;
|
||||
max_images_basic?: number;
|
||||
max_images_premium?: number;
|
||||
max_image_prompts?: number;
|
||||
included_credits?: number;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={cta.href} className={className}>
|
||||
{cta.label}
|
||||
</Link>
|
||||
);
|
||||
const formatNumber = (num: number | undefined | null): string => {
|
||||
if (!num || num === 0) return '0';
|
||||
if (num >= 1000000) return `${(num / 1000).toFixed(0)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
name: "Starter",
|
||||
slug: "starter",
|
||||
price: "$99",
|
||||
cadence: "per month",
|
||||
description: "For small teams starting workflows.",
|
||||
icon: SparklesIcon,
|
||||
iconColor: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
|
||||
features: [
|
||||
"2 sites",
|
||||
"1 team user",
|
||||
"1,000 monthly credits",
|
||||
"100K words content generation",
|
||||
"100 AI keyword clusters",
|
||||
"300 content ideas",
|
||||
"300 basic / 60 premium images",
|
||||
"300 image prompts",
|
||||
],
|
||||
badge: "For startups",
|
||||
},
|
||||
{
|
||||
name: "Growth",
|
||||
slug: "growth",
|
||||
price: "$199",
|
||||
cadence: "per month",
|
||||
description: "For teams automating multiple workflows.",
|
||||
icon: BoltIcon,
|
||||
iconColor: "from-[var(--color-success)] to-[var(--color-success-dark)]",
|
||||
features: [
|
||||
"5 sites",
|
||||
"3 team users",
|
||||
"3,000 monthly credits",
|
||||
"300K words content generation",
|
||||
"300 AI keyword clusters",
|
||||
"900 content ideas",
|
||||
"900 basic / 180 premium images",
|
||||
"900 image prompts",
|
||||
],
|
||||
featured: true,
|
||||
badge: "Best value",
|
||||
},
|
||||
{
|
||||
name: "Scale",
|
||||
slug: "scale",
|
||||
price: "$299",
|
||||
cadence: "per month",
|
||||
description: "For publishers and large orgs needing deeper control.",
|
||||
icon: ChartBarIcon,
|
||||
iconColor: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
|
||||
features: [
|
||||
"Unlimited sites",
|
||||
"5 team users",
|
||||
"5,000 monthly credits",
|
||||
"500K words content generation",
|
||||
"500 AI keyword clusters",
|
||||
"1,500 content ideas",
|
||||
"1,500 basic / 300 premium images",
|
||||
"1,500 image prompts",
|
||||
],
|
||||
badge: "For scaling teams",
|
||||
},
|
||||
];
|
||||
const convertToPricingPlan = (plan: Plan): PricingPlan => {
|
||||
const monthlyPrice = typeof plan.price === 'number' ? plan.price : parseFloat(String(plan.price || 0));
|
||||
const features: string[] = [];
|
||||
|
||||
if (plan.max_sites) features.push(`${plan.max_sites === 999999 ? 'Unlimited' : plan.max_sites} Site${plan.max_sites > 1 ? 's' : ''}`);
|
||||
if (plan.max_users) features.push(`${plan.max_users} Team User${plan.max_users > 1 ? 's' : ''}`);
|
||||
if (plan.included_credits) features.push(`${formatNumber(plan.included_credits)} Monthly Credits`);
|
||||
if (plan.max_content_words) features.push(`${formatNumber(plan.max_content_words)} Words/Month`);
|
||||
if (plan.max_clusters) features.push(`${plan.max_clusters} AI Keyword Clusters`);
|
||||
if (plan.max_content_ideas) features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
|
||||
if (plan.max_images_basic && plan.max_images_premium) {
|
||||
features.push(`${formatNumber(plan.max_images_basic)} Basic / ${formatNumber(plan.max_images_premium)} Premium Images`);
|
||||
}
|
||||
if (plan.max_image_prompts) features.push(`${formatNumber(plan.max_image_prompts)} Image Prompts`);
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
name: plan.name,
|
||||
monthlyPrice: monthlyPrice,
|
||||
price: monthlyPrice,
|
||||
annualDiscountPercent: plan.annual_discount_percent || 15,
|
||||
period: '/month',
|
||||
description: `Perfect for ${plan.name.toLowerCase()} needs`,
|
||||
features,
|
||||
buttonText: monthlyPrice === 0 ? 'Start Free' : 'Choose Plan',
|
||||
highlighted: plan.is_featured || false,
|
||||
};
|
||||
};
|
||||
|
||||
const Pricing: React.FC = () => {
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
try {
|
||||
const data = await getPublicPlans();
|
||||
setPlans(data);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('Error fetching public plans:', err);
|
||||
setError('Failed to load pricing plans');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
}, []);
|
||||
|
||||
const featureMatrix = [
|
||||
{ feature: "ACCOUNT & ACCESS", starter: null, growth: null, scale: null, isCategory: true },
|
||||
@@ -224,95 +208,50 @@ const Pricing: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* PRICING TIERS SECTION */}
|
||||
{/* LOADING/ERROR STATES */}
|
||||
{loading && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{tiers.map((tier) => {
|
||||
const Icon = tier.icon;
|
||||
return (
|
||||
<div
|
||||
key={tier.name}
|
||||
className={`relative rounded-3xl border-2 ${
|
||||
tier.featured
|
||||
? "border-[var(--color-primary)]/60 bg-gradient-to-br from-[#0693e3]/10 via-[#5d4ae3]/5 to-[#0bbf87]/5 shadow-[0_0_70px_rgba(6,147,227,0.25)]"
|
||||
: "border-slate-200 bg-gradient-to-br from-white to-slate-50/50"
|
||||
} p-10 flex flex-col gap-6 hover:shadow-xl transition-all group ${
|
||||
tier.featured ? "lg:scale-105" : "hover:-translate-y-1"
|
||||
}`}
|
||||
>
|
||||
{/* Badge and Icon - Aligned */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Icon */}
|
||||
<div className={`inline-flex size-14 rounded-2xl bg-gradient-to-br ${tier.iconColor} items-center justify-center text-white shadow-lg flex-shrink-0`}>
|
||||
<Icon className="h-7 w-7" />
|
||||
</div>
|
||||
|
||||
{/* Badge and Plan Name */}
|
||||
<div className="flex-1">
|
||||
{tier.badge && (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full border px-3 py-1 text-[10px] uppercase tracking-[0.25em] mb-2 ${
|
||||
tier.featured
|
||||
? "border-[var(--color-primary)]/30 bg-gradient-to-r from-[#0693e3]/20 to-[#0472b8]/20 text-[#0472b8]"
|
||||
: "border-slate-200 bg-gradient-to-r from-[#0693e3]/10 to-[#0bbf87]/10 text-[var(--color-primary)]"
|
||||
}`}
|
||||
>
|
||||
{tier.badge}
|
||||
</span>
|
||||
)}
|
||||
<h3 className="text-2xl font-semibold text-slate-900">{tier.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Description */}
|
||||
<p className="text-sm text-slate-600">{tier.description}</p>
|
||||
|
||||
{/* Price */}
|
||||
<div className="text-4xl font-semibold text-slate-900">
|
||||
{tier.price}
|
||||
{tier.cadence && (
|
||||
<span className="text-sm font-normal text-slate-500 ml-2">
|
||||
{tier.cadence}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<ul className="space-y-4 text-sm text-slate-600 flex-1">
|
||||
{tier.features.map((feature, idx) => {
|
||||
// Subtle check icons: light bg with dark check for starter/scale, colored for growth
|
||||
const checkStyle = tier.featured
|
||||
? "bg-[var(--color-success)]/10 text-[#0bbf87]"
|
||||
: "bg-slate-100 text-slate-600";
|
||||
return (
|
||||
<li key={feature} className="flex gap-3 items-start">
|
||||
<CheckIcon className={`h-5 w-5 ${checkStyle} rounded-full p-0.5 flex-shrink-0 mt-0.5`} />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* CTA Button */}
|
||||
<div className="pt-4">
|
||||
<a
|
||||
href={`https://app.igny8.com/signup?plan=${tier.slug}`}
|
||||
className={`inline-flex w-full items-center justify-center rounded-full px-6 py-3 text-sm font-semibold transition ${
|
||||
tier.featured
|
||||
? "bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white hover:from-[#0472b8] hover:to-[#0693e3] shadow-lg shadow-[#0693e3]/30"
|
||||
: "border-2 border-slate-300 bg-white/50 backdrop-blur-sm text-slate-900 hover:border-[var(--color-primary)] hover:bg-white"
|
||||
}`}
|
||||
>
|
||||
{tier.price === "Free" ? "Start free trial" : `Continue to billing`}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
|
||||
<p className="mt-4 text-slate-600">Loading pricing plans...</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* COMPARISON TABLE SECTION */}
|
||||
{error && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-600">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-4 px-6 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)]"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* PRICING TIERS SECTION */}
|
||||
{!loading && !error && (
|
||||
<section className="mx-auto px-6 py-16" style={{ maxWidth: '1560px' }}>
|
||||
<PricingTable
|
||||
variant="1"
|
||||
title="Flexible Plans Tailored to Fit Your Unique Needs!"
|
||||
plans={plans.map(convertToPricingPlan)}
|
||||
showToggle={true}
|
||||
onPlanSelect={(pricingPlan) => {
|
||||
const plan = plans.find(p => p.id === pricingPlan.id);
|
||||
if (plan) {
|
||||
window.location.href = `https://app.igny8.com/signup?plan=${plan.slug}`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* COMPARISON TABLE SECTION - Keep hardcoded for now */}
|
||||
{!loading && !error && (
|
||||
<section className="max-w-7xl mx-auto px-6 pb-24">
|
||||
<h3 className="text-3xl font-bold text-slate-900 mb-8 text-center">
|
||||
Compare plan capabilities
|
||||
@@ -399,6 +338,7 @@ const Pricing: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* INFO BLOCKS SECTION */}
|
||||
<section className="bg-gradient-to-b from-white via-slate-50/30 to-white">
|
||||
|
||||
@@ -50,49 +50,23 @@ const formatWordCount = (num: number): string => {
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
// Extract major features from plan data
|
||||
// Extract major features from plan data - SAME AS MARKETING PAGE
|
||||
const extractFeatures = (plan: Plan): string[] => {
|
||||
const features: string[] = [];
|
||||
|
||||
// Sites and Users
|
||||
features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`);
|
||||
features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`);
|
||||
|
||||
// Planner features
|
||||
features.push(`${formatNumber(plan.max_keywords)} Keywords`);
|
||||
features.push(`${formatNumber(plan.max_clusters)} Clusters`);
|
||||
features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
|
||||
|
||||
// Writer features
|
||||
features.push(`${formatWordCount(plan.monthly_word_count_limit)} Words/Month`);
|
||||
features.push(`${plan.daily_content_tasks} Daily Content Tasks`);
|
||||
|
||||
// Image features
|
||||
features.push(`${plan.monthly_image_count} Images/Month`);
|
||||
if (plan.image_model_choices && plan.image_model_choices.length > 0) {
|
||||
const models = plan.image_model_choices.map((m: string) => m.toUpperCase()).join(', ');
|
||||
features.push(`${models} Image Models`);
|
||||
}
|
||||
|
||||
// AI Credits
|
||||
features.push(`${formatNumber(plan.included_credits)} AI Credits Included`);
|
||||
features.push(`${formatNumber(plan.monthly_ai_credit_limit)} Monthly AI Credit Limit`);
|
||||
|
||||
// Feature flags
|
||||
if (plan.features && Array.isArray(plan.features)) {
|
||||
if (plan.features.includes('ai_writer')) {
|
||||
features.push('AI Writer');
|
||||
}
|
||||
if (plan.features.includes('image_gen')) {
|
||||
features.push('Image Generation');
|
||||
}
|
||||
if (plan.features.includes('auto_publish')) {
|
||||
features.push('Auto Publish');
|
||||
}
|
||||
if (plan.features.includes('custom_prompts')) {
|
||||
features.push('Custom Prompts');
|
||||
}
|
||||
if (plan.max_sites) features.push(`${plan.max_sites === 999999 ? 'Unlimited' : plan.max_sites} Site${plan.max_sites > 1 ? 's' : ''}`);
|
||||
if (plan.max_users) features.push(`${plan.max_users} Team User${plan.max_users > 1 ? 's' : ''}`);
|
||||
if (plan.included_credits) features.push(`${formatNumber(plan.included_credits)} Monthly Credits`);
|
||||
if (plan.monthly_word_count_limit) features.push(`${formatWordCount(plan.monthly_word_count_limit)} Words/Month`);
|
||||
if (plan.max_clusters) features.push(`${plan.max_clusters} AI Keyword Clusters`);
|
||||
if (plan.max_content_ideas) features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
|
||||
if (plan.monthly_image_count) {
|
||||
// Try to split into basic/premium if data available, otherwise show total
|
||||
const basicImages = Math.floor(plan.monthly_image_count * 0.8);
|
||||
const premiumImages = Math.floor(plan.monthly_image_count * 0.2);
|
||||
features.push(`${formatNumber(basicImages)} Basic / ${formatNumber(premiumImages)} Premium Images`);
|
||||
}
|
||||
if (plan.daily_image_generation_limit) features.push(`${formatNumber(plan.daily_image_generation_limit)} Image Prompts`);
|
||||
|
||||
return features;
|
||||
};
|
||||
@@ -108,11 +82,11 @@ const transformPlanToPricingPlan = (plan: Plan, index: number, totalPlans: numbe
|
||||
id: plan.id,
|
||||
name: plan.name,
|
||||
monthlyPrice: monthlyPrice,
|
||||
price: monthlyPrice, // Will be calculated by component based on period
|
||||
price: monthlyPrice,
|
||||
period: '/month',
|
||||
description: getPlanDescription(plan),
|
||||
features: extractFeatures(plan),
|
||||
buttonText: 'Choose Plan',
|
||||
buttonText: monthlyPrice === 0 ? 'Start Free' : 'Select Plan',
|
||||
highlighted: highlighted,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -985,6 +985,16 @@ export async function cancelSubscription(subscriptionId: number): Promise<{ mess
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC PLANS (for marketing/pricing page)
|
||||
// ============================================================================
|
||||
|
||||
export async function getPublicPlans(): Promise<Plan[]> {
|
||||
// Use the existing auth/plans endpoint which already filters for public plans
|
||||
const response = await fetchAPI('/v1/auth/plans/');
|
||||
return response.results || response;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USAGE SUMMARY (PLAN LIMITS)
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user