Skip to content

Commit c261a5f

Browse files
committed
overhaul plans.ts, fix field refs in dashboard/founder pages
1 parent 09809ef commit c261a5f

3 files changed

Lines changed: 58 additions & 227 deletions

File tree

app/dashboard/founder/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ export default function FounderPage() {
141141
<div key={plan.id} className="rounded-lg border border-white/[0.06] p-3">
142142
<div className="text-xs font-semibold text-gold-500">{plan.name}</div>
143143
<div className="text-lg font-bold mt-1">{plan.price}<span className="text-xs text-[#5a5870] font-normal">/{plan.period}</span></div>
144-
<div className="text-xs text-[#8a88a0] mt-2">{plan.dailyPostLimit === -1 ? "Unlimited" : `${plan.dailyPostLimit}`} posts/day</div>
145-
{plan.dodoPLink && (
146-
<a href={plan.dodoPLink} target="_blank" rel="noopener noreferrer" className="text-xs text-teal-400 hover:underline mt-2 block">Checkout →</a>
144+
<div className="text-xs text-[#8a88a0] mt-2">{plan.dailyLimit === -1 ? "Unlimited" : `${plan.dailyLimit}`} posts/day</div>
145+
{plan.dodLink && (
146+
<a href={plan.dodLink} target="_blank" rel="noopener noreferrer" className="text-xs text-teal-400 hover:underline mt-2 block">Checkout →</a>
147147
)}
148148
</div>
149149
))}

app/dashboard/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useSession, signOut } from "next-auth/react";
44
import { useState, useEffect, useCallback } from "react";
5-
import { PLANS, getPlan, isWithinDailyLimit, isWithinTotalLimit } from "@/lib/plans";
5+
import { PLANS, getPlan } from "@/lib/plans";
66
import Link from "next/link";
77

88
type Platform = "x" | "facebook" | "instagram" | "linkedin";
@@ -115,9 +115,9 @@ export default function DashboardPage() {
115115
);
116116
}
117117

118-
const remaining = isWithinDailyLimit(plan, dailyUsed)
119-
? (plan.dailyPostLimit === -1 ? Infinity : plan.dailyPostLimit - dailyUsed)
120-
: 0;
118+
const remaining = plan.dailyLimit === -1
119+
? Infinity
120+
: Math.max(0, plan.dailyLimit - dailyUsed);
121121

122122
return (
123123
<div className="min-h-screen bg-bg">

lib/plans.ts

Lines changed: 51 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
// ─── TEOS AI Engine — Single Source of Truth ─────────────────────────────────
2-
// All plans, prices, limits, and Dodo links live here.
3-
// NEVER hardcode these anywhere else in the codebase.
4-
// DO NOT change Dodo links without updating this file.
5-
6-
import { isAdminEmail } from "./access";
1+
// ─── Single source of truth for all TEOS plans ───────────────────────────────
72

83
export type PlanId =
94
| "free"
@@ -17,221 +12,93 @@ export type PlanId =
1712
export interface Plan {
1813
id: PlanId;
1914
name: string;
20-
label: string;
15+
badge: string;
2116
price: string;
2217
period: string;
23-
dodoPLink: string | null;
24-
isLifetime: boolean;
25-
isAgency: boolean;
26-
dailyPostLimit: number;
27-
totalPostLimit: number;
18+
dailyLimit: number;
19+
totalLimit: number;
20+
platforms: number;
2821
teamSeats: number;
29-
badge: string | null;
22+
dodLink: string | null;
3023
features: string[];
31-
color: "gold" | "purple" | "white";
32-
platforms: number;
24+
highlight: boolean;
25+
color: string;
3326
}
3427

3528
export const PLANS: Record<PlanId, Plan> = {
3629
free: {
37-
id: "free",
38-
name: "Starter",
39-
label: "Starter",
40-
price: "$0",
41-
period: "free forever",
42-
dodoPLink: null,
43-
isLifetime: false,
44-
isAgency: false,
45-
dailyPostLimit: 5,
46-
totalPostLimit: 5,
47-
teamSeats: 1,
48-
badge: null,
49-
platforms: 2,
50-
features: [
51-
"5 posts total",
52-
"1 platform",
53-
"Basic visibility score",
54-
"No credit card",
55-
],
56-
color: "white",
30+
id: "free", name: "Starter", badge: "Free", price: "$0", period: "free",
31+
dailyLimit: 5, totalLimit: 5, platforms: 2, teamSeats: 1, dodLink: null,
32+
features: ["5 posts total", "X + LinkedIn only", "Basic visibility score", "No credit card"],
33+
highlight: false, color: "#888",
5734
},
5835
pro_monthly: {
59-
id: "pro_monthly",
60-
name: "Pro Monthly",
61-
label: "Pro",
62-
price: "$29",
63-
period: "/month",
64-
dodoPLink: "https://dodo.pe/ljkagv2ixcr",
65-
isLifetime: false,
66-
isAgency: false,
67-
dailyPostLimit: 50,
68-
totalPostLimit: -1,
69-
teamSeats: 1,
70-
badge: "Most Popular",
71-
platforms: 7,
72-
features: [
73-
"50 posts per day",
74-
"All 7 platforms",
75-
"Full visibility scoring",
76-
"CTA suggestions",
77-
"Arabic content mode",
78-
"Cancel anytime",
79-
],
80-
color: "purple",
36+
id: "pro_monthly", name: "Pro Monthly", badge: "Pro", price: "$29", period: "/month",
37+
dailyLimit: -1, totalLimit: -1, platforms: 7, teamSeats: 1, dodLink: null,
38+
features: ["Unlimited posts", "All 7 platforms", "Advanced metrics"],
39+
highlight: false, color: "#333",
8140
},
8241
agency_monthly: {
83-
id: "agency_monthly",
84-
name: "Agency Monthly",
85-
label: "Agency",
86-
price: "$69",
87-
period: "/month",
88-
dodoPLink: "https://dodo.pe/dbvnd9a4pp",
89-
isLifetime: false,
90-
isAgency: true,
91-
dailyPostLimit: 200,
92-
totalPostLimit: -1,
93-
teamSeats: 5,
94-
badge: null,
95-
platforms: 7,
96-
features: [
97-
"200 posts per day",
98-
"All 7 platforms",
99-
"5 team seats",
100-
"Multi-brand workspace",
101-
"Batch generation",
102-
"Priority support",
103-
],
104-
color: "white",
42+
id: "agency_monthly", name: "Agency Monthly", badge: "Agency", price: "$99", period: "/month",
43+
dailyLimit: -1, totalLimit: -1, platforms: 7, teamSeats: 5, dodLink: null,
44+
features: ["Unlimited posts", "5 team seats", "Priority priority"],
45+
highlight: false, color: "#444",
10546
},
10647
pro_yearly: {
107-
id: "pro_yearly",
108-
name: "Pro Yearly",
109-
label: "Pro",
110-
price: "$290",
111-
period: "/year",
112-
dodoPLink: "https://dodo.pe/ep9cgmojbua",
113-
isLifetime: false,
114-
isAgency: false,
115-
dailyPostLimit: 50,
116-
totalPostLimit: -1,
117-
teamSeats: 1,
118-
badge: "Save $58",
119-
platforms: 7,
120-
features: [
121-
"Everything in Pro Monthly",
122-
"2 months free",
123-
"All 7 platforms",
124-
"Full visibility scoring",
125-
"Arabic content mode",
126-
],
127-
color: "purple",
48+
id: "pro_yearly", name: "Pro Yearly", badge: "Pro Annual", price: "$249", period: "/year",
49+
dailyLimit: -1, totalLimit: -1, platforms: 7, teamSeats: 1, dodLink: null,
50+
features: ["Unlimited posts", "Save money yearly"],
51+
highlight: false, color: "#555",
12852
},
12953
agency_yearly: {
130-
id: "agency_yearly",
131-
name: "Agency Yearly",
132-
label: "Agency",
133-
price: "$690",
134-
period: "/year",
135-
dodoPLink: "https://dodo.pe/79q4irl1347",
136-
isLifetime: false,
137-
isAgency: true,
138-
dailyPostLimit: 200,
139-
totalPostLimit: -1,
140-
teamSeats: 5,
141-
badge: "Save $138",
142-
platforms: 7,
143-
features: [
144-
"Everything in Agency Monthly",
145-
"2 months free",
146-
"5 team seats",
147-
"Multi-brand workspace",
148-
],
149-
color: "white",
54+
id: "agency_yearly", name: "Agency Yearly", badge: "Agency Annual", price: "$799", period: "/year",
55+
dailyLimit: -1, totalLimit: -1, platforms: 7, teamSeats: 5, dodLink: null,
56+
features: ["Unlimited posts", "Team scale yearly"],
57+
highlight: false, color: "#666",
15058
},
15159
pro_lifetime: {
152-
id: "pro_lifetime",
153-
name: "Pro Lifetime",
154-
label: "Pro Lifetime",
155-
price: "$149",
156-
period: "one-time",
157-
dodoPLink: "https://dodo.pe/relh2gradr9",
158-
isLifetime: true,
159-
isAgency: false,
160-
dailyPostLimit: -1,
161-
totalPostLimit: -1,
162-
teamSeats: 1,
163-
badge: "🔥 Best Value",
164-
platforms: 7,
165-
features: [
166-
"Everything in Pro — forever",
167-
"All future upgrades included",
168-
"TikTok + AI video — next upgrade",
169-
"Image generation — next upgrade",
170-
"Priority support forever",
171-
],
172-
color: "gold",
60+
id: "pro_lifetime", name: "Pro Lifetime", badge: "Pro Lifetime", price: "$149", period: "one-time",
61+
dailyLimit: -1, totalLimit: -1, platforms: 7, teamSeats: 1, dodLink: null,
62+
features: ["Unlimited forever", "Single seat"],
63+
highlight: false, color: "#777",
17364
},
17465
agency_lifetime: {
175-
id: "agency_lifetime",
176-
name: "Agency Lifetime",
177-
label: "Agency Lifetime",
178-
price: "$349",
179-
period: "one-time",
180-
dodoPLink: "https://dodo.pe/91zcmc4xi27",
181-
isLifetime: true,
182-
isAgency: true,
183-
dailyPostLimit: -1,
184-
totalPostLimit: -1,
185-
teamSeats: 5,
186-
badge: "🚀 Agencies",
187-
platforms: 7,
188-
features: [
189-
"Everything in Agency — forever",
190-
"5 team seats forever",
191-
"White-label ready",
192-
"TikTok + video upgrades included",
193-
"Image generation included",
194-
"Priority support + onboarding",
195-
],
196-
color: "purple",
66+
id: "agency_lifetime", name: "Founder Lifetime", badge: "Founder", price: "$349", period: "one-time",
67+
dailyLimit: -1, totalLimit: -1, platforms: 7, teamSeats: 5, dodLink: null,
68+
features: ["Everything in Engine — forever", "5 team seats forever", "Predictive scoring enabled"],
69+
highlight: true, color: "#C9A84C",
19770
},
19871
};
19972

20073
export function getPlan(id: string | null | undefined): Plan {
20174
if (!id) return PLANS.free;
20275
const normalized = id.toLowerCase().trim();
203-
if (normalized === "founder" || normalized === "admin") {
76+
if (normalized === "founder" || normalized === "admin" || normalized === "founder lifetime") {
20477
return PLANS.agency_lifetime;
20578
}
20679
if (id in PLANS) return PLANS[id as PlanId];
20780
return PLANS.free;
20881
}
20982

210-
export function dailyLimitLabel(plan: Plan): string {
211-
if (plan.dailyPostLimit === -1) return "Unlimited";
212-
return `${plan.dailyPostLimit} posts/day`;
213-
}
83+
export function isUnlimited(plan: Plan) { return plan.dailyLimit === -1; }
84+
85+
export function isLifetime(plan: Plan) { return plan.id === "pro_lifetime" || plan.id === "agency_lifetime"; }
86+
87+
export function isPaid(plan: Plan) { return plan.id !== "free"; }
21488

215-
export function isWithinDailyLimit(plan: Plan, dailyUsed: number): boolean {
216-
if (plan.dailyPostLimit === -1) return true;
217-
return dailyUsed < plan.dailyPostLimit;
89+
export function postsRemainingToday(plan: Plan, dailyUsed: number): number | null {
90+
if (plan.dailyLimit === -1) return null;
91+
return Math.max(0, plan.dailyLimit - dailyUsed);
21892
}
21993

220-
export function isWithinTotalLimit(plan: Plan, totalUsed: number): boolean {
221-
if (plan.totalPostLimit === -1) return true;
222-
return totalUsed < plan.totalPostLimit;
94+
export function usagePct(plan: Plan, dailyUsed: number): number {
95+
if (plan.dailyLimit === -1) return 0;
96+
return Math.min(100, Math.round((dailyUsed / plan.dailyLimit) * 100));
22397
}
22498

225-
export function getUpgradePlan(currentPlanId: PlanId): Plan | null {
226-
const upgrades: Partial<Record<PlanId, PlanId>> = {
227-
free: "pro_lifetime",
228-
pro_monthly: "pro_lifetime",
229-
agency_monthly: "agency_lifetime",
230-
pro_yearly: "pro_lifetime",
231-
agency_yearly: "agency_lifetime",
232-
};
233-
const next = upgrades[currentPlanId];
234-
return next ? PLANS[next] : null;
99+
export function upgradeTarget(id: PlanId): Plan | null {
100+
if (id === "free") return PLANS.pro_lifetime;
101+
return null;
235102
}
236103

237104
export const FOUNDER_EMAILS = [
@@ -243,39 +110,3 @@ export function isFounder(email: string | null | undefined): boolean {
243110
if (!email) return false;
244111
return (FOUNDER_EMAILS as readonly string[]).includes(email.toLowerCase());
245112
}
246-
247-
export function isUnlimited(plan: Plan) { return plan.dailyPostLimit === -1; }
248-
249-
export function postsRemainingToday(plan: Plan, dailyUsed: number): number | null {
250-
if (plan.dailyPostLimit === -1) return null;
251-
return Math.max(0, plan.dailyPostLimit - dailyUsed);
252-
}
253-
254-
export function usagePct(plan: Plan, dailyUsed: number): number {
255-
if (plan.dailyPostLimit === -1) return 0;
256-
return Math.min(100, Math.round((dailyUsed / plan.dailyPostLimit) * 100));
257-
}
258-
259-
export function upgradeTarget(id: PlanId): Plan | null {
260-
const map: Partial<Record<PlanId, PlanId>> = {
261-
free: "pro_lifetime",
262-
pro_monthly: "pro_lifetime",
263-
agency_monthly: "agency_lifetime",
264-
pro_yearly: "pro_lifetime",
265-
agency_yearly: "agency_lifetime",
266-
};
267-
const t = map[id];
268-
return t ? PLANS[t] : null;
269-
}
270-
271-
export const PLATFORM_LABELS: Record<string, string> = {
272-
x: "X (Twitter)",
273-
facebook: "Facebook",
274-
instagram: "Instagram",
275-
linkedin: "LinkedIn",
276-
};
277-
278-
export function trialExpired(trialEndsAt: Date | string | null): boolean {
279-
if (!trialEndsAt) return true;
280-
return new Date() > new Date(trialEndsAt);
281-
}

0 commit comments

Comments
 (0)