diff --git a/client/src/App.tsx b/client/src/App.tsx index 1b0e615..c4b65ac 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,6 @@ import { BrowserRouter as Router, Routes, Route, - Navigate, } from "react-router-dom"; import { ThemeProvider } from "./components/ui/theme-provider"; import { Toaster } from "./components/ui/toaster"; diff --git a/client/src/components/icons/PlanIcons.tsx b/client/src/components/icons/PlanIcons.tsx index 02d77b9..5a87b7f 100644 --- a/client/src/components/icons/PlanIcons.tsx +++ b/client/src/components/icons/PlanIcons.tsx @@ -1,5 +1,11 @@ import React from "react"; +export const FreePlanIcon: React.FC> = (props) => ( + + + +); + export const StarterPlanIcon: React.FC> = (props) => ( void; + onChangePlan: () => void; onCancelSubscription: () => void; } export function PlanSummary({ customerProfile, cancellingSubscription, - onContactSales, + onChangePlan, onCancelSubscription, }: PlanSummaryProps) { const formatDate = (dateInput?: string | { $date: string }) => { @@ -235,26 +235,14 @@ export function PlanSummary({

)} -
-

Customer ID

-

- {customerProfile?.id || customerProfile?.currentSubscription?.customerId || "-"} -

-
-
-

Account Created

-

- {formatDate(customerProfile?.createdAt)} -

-
{hasActiveSubscription() && ( @@ -270,10 +258,7 @@ export function PlanSummary({ Cancelling... ) : ( - <> - - Cancel Subscription - + "Cancel Subscription" )} diff --git a/client/src/components/subscription/PlanUpgrade.tsx b/client/src/components/subscription/PlanUpgrade.tsx index b15d20d..783b6f7 100644 --- a/client/src/components/subscription/PlanUpgrade.tsx +++ b/client/src/components/subscription/PlanUpgrade.tsx @@ -24,7 +24,7 @@ export function PlanUpgrade({ customerProfile, onUpgradePlan }: PlanUpgradeProps const getAvailableUpgrades = () => { // Prioritize currentSubscription over subscription const currentPlan = customerProfile?.currentSubscription?.planType?.toLowerCase() || - customerProfile?.subscription?.planType?.toLowerCase(); + customerProfile?.subscription?.planType?.toLowerCase(); console.log("PlanUpgrade: Current plan type for upgrades:", currentPlan); if (!currentPlan || currentPlan === 'free' || currentPlan === 'prepaid') { diff --git a/client/src/pages/PaymentsPage.tsx b/client/src/pages/PaymentsPage.tsx index 947d799..ce93bee 100644 --- a/client/src/pages/PaymentsPage.tsx +++ b/client/src/pages/PaymentsPage.tsx @@ -557,7 +557,8 @@ export function PaymentsPage() {
)) ) : ( -

No subscription history available

+ //

No subscription history available

+ <> )} diff --git a/client/src/pages/ProjectsPage.tsx b/client/src/pages/ProjectsPage.tsx index 57a13b8..0f2d115 100644 --- a/client/src/pages/ProjectsPage.tsx +++ b/client/src/pages/ProjectsPage.tsx @@ -1,10 +1,10 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/useToast"; import { Badge } from "@/components/ui/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; +import { Card, CardContent } from "@/components/ui/card"; +// import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -13,19 +13,20 @@ import { Lock, Calendar, Folder, - ExternalLink, Trash2, Link, - Copy, CheckCircle, + Copy, + MoreVertical, Clock, AlertCircle, + ArrowUpRightSquare, } from "lucide-react"; import { getProjects, deleteDeployedProject, setupCustomDomain, deleteCustomDomain } from "@/api/projects"; import { getUserProfile } from "@/api/user"; import SpinnerShape from "@/components/SpinnerShape"; import { ProjectsPythagoraIcon, PremiumPlanIcon } from "@/components/icons/PlanIcons"; -import { cn } from "@/lib/utils"; + import { AlertDialog, AlertDialogAction, @@ -44,8 +45,13 @@ import { DialogFooter, DialogHeader, DialogTitle, - DialogTrigger, } from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface Project { id: string; @@ -80,7 +86,6 @@ export function ProjectsPage() { const [deployments, setDeployments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [deletingDeployment, setDeletingDeployment] = useState(null); const [settingUpDomain, setSettingUpDomain] = useState(null); const [customDomainDialog, setCustomDomainDialog] = useState<{ open: boolean; deployment: Deployment | null }>({ open: false, deployment: null }); const [dnsInstructionsDialog, setDnsInstructionsDialog] = useState<{ open: boolean; deployment: Deployment | null }>({ open: false, deployment: null }); @@ -90,7 +95,7 @@ export function ProjectsPage() { return (tab === 'deployed' || tab === 'projects') ? tab : 'projects'; }); const [copiedField, setCopiedField] = useState(null); - const [deletingCustomDomain, setDeletingCustomDomain] = useState(null); + const [openDropdownId, setOpenDropdownId] = useState(null); const { toast } = useToast(); @@ -99,7 +104,7 @@ export function ProjectsPage() { setSearchParams({ tab: value }); }; - const fetchData = async () => { + const fetchData = useCallback(async () => { try { setLoading(true); setError(null); @@ -116,7 +121,17 @@ export function ProjectsPage() { // Handle projects if (projectsResponse && projectsResponse.projects) { - const mappedProjects = projectsResponse.projects.map((project: any) => ({ + type ApiProject = { + id: string; + name: string; + folder_name: string; + updated_at?: string; + created_at?: string; + isPublic?: boolean; + deploymentUrl?: string; + }; + + const mappedProjects = projectsResponse.projects.map((project: ApiProject) => ({ id: project.id, name: project.name, folder_name: project.folder_name, @@ -128,7 +143,7 @@ export function ProjectsPage() { })); // Sort projects by time (most recent first) - const sortedProjects = mappedProjects.sort((a, b) => { + const sortedProjects = mappedProjects.sort((a: Project, b: Project) => { const dateA = new Date(a.updated_at || a.created_at || 0); const dateB = new Date(b.updated_at || b.created_at || 0); return dateB.getTime() - dateA.getTime(); // Descending order (newest first) @@ -146,13 +161,19 @@ export function ProjectsPage() { console.log("ProjectsPage: Deployments found:", profileResponse.deployments); // Sort deployments by time (most recent first) - const sortedDeployments = [...profileResponse.deployments].sort((a, b) => { - const getDateString = (dateObj: { $date: string } | string) => { - return typeof dateObj === 'object' && dateObj.$date ? dateObj.$date : dateObj; + const sortedDeployments = [...profileResponse.deployments].sort((a: Deployment, b: Deployment) => { + const getDateString = (dateObj: { $date: string } | string | undefined): string | undefined => { + if (!dateObj) return undefined; + if (typeof dateObj === 'object' && (dateObj as { $date?: string }).$date) { + return (dateObj as { $date: string }).$date; + } + return dateObj as string; }; - const dateA = new Date(getDateString(a.updatedAt) || getDateString(a.createdAt) || 0); - const dateB = new Date(getDateString(b.updatedAt) || getDateString(b.createdAt) || 0); + const aStr = getDateString(a.updatedAt) || getDateString(a.createdAt) || undefined; + const bStr = getDateString(b.updatedAt) || getDateString(b.createdAt) || undefined; + const dateA = aStr ? new Date(aStr) : new Date(0); + const dateB = bStr ? new Date(bStr) : new Date(0); return dateB.getTime() - dateA.getTime(); // Descending order (newest first) }); @@ -179,11 +200,10 @@ export function ProjectsPage() { } finally { setLoading(false); } - }; + }, [toast]); const handleDeleteDeployment = async (deployment: Deployment) => { try { - setDeletingDeployment(deployment.projectId); console.log('ProjectsPage: Deleting deployment:', deployment); await deleteDeployedProject(deployment.projectId, deployment.folderPath); @@ -208,8 +228,6 @@ export function ProjectsPage() { title: "Error", description: errorMessage, }); - } finally { - setDeletingDeployment(null); } }; @@ -224,7 +242,6 @@ export function ProjectsPage() { } try { - setDeletingCustomDomain(deployment.projectId); console.log('ProjectsPage: Deleting custom domain:', { deployment: deployment, domain: deployment.customDomain @@ -252,8 +269,6 @@ export function ProjectsPage() { title: "Error", description: errorMessage, }); - } finally { - setDeletingCustomDomain(null); } }; @@ -354,7 +369,7 @@ export function ProjectsPage() { useEffect(() => { fetchData(); - }, []); + }, [fetchData]); const formatDate = (dateString?: string | { $date: string }) => { if (!dateString) return "Unknown"; @@ -450,8 +465,7 @@ export function ProjectsPage() { {/* Projects Tab Content */} - - +
{projects.length === 0 ? (
@@ -461,18 +475,17 @@ export function ProjectsPage() {

) : ( -
- {projects.map((project, index) => ( -
-
-
-
-

+
+ {projects.map((project) => ( + + +
+

{project.name}

{project.isPublic ? ( <> @@ -487,31 +500,29 @@ export function ProjectsPage() { )}
-
- +
+
{formatDate(project.updated_at || project.created_at)} - - +
+
+ {project.folder_name} -
- {index < projects.length - 1 && } -
+ + ))}

)} - - +
{/* Deployed Tab Content */} - - +
{deployments.length === 0 ? (
@@ -521,13 +532,14 @@ export function ProjectsPage() {

) : ( -
- {deployments.map((deployment, index) => ( -
-
-
-
-

+
+ {deployments.map((deployment) => ( + + +
+
+
+

{deployment.folderPath}

@@ -536,23 +548,122 @@ export function ProjectsPage() { {deployment.customDomain && getCustomDomainStatusBadge(deployment.customDomainStatus)}
-
+ setOpenDropdownId(open ? deployment.instanceId : null)} + > + + + + + openDeployment(deployment.url)}> + + Open + + { + if (!deployment.customDomain) { + setCustomDomainDialog({ open: true, deployment }); + setOpenDropdownId(null); + } + }} + > + + {deployment.customDomain ? "Domain Set" : "Custom Domain"} + + {deployment.customDomain && ( + { + setDnsInstructionsDialog({ open: true, deployment }); + setOpenDropdownId(null); + }}> + + DNS Info + + )} + {deployment.customDomain && ( + + + e.preventDefault()} + > + + Delete Domain + + + + + Delete Custom Domain + + Are you sure you want to delete the custom domain "{deployment.customDomain}"? + This will remove the custom domain configuration but won't delete the deployment itself. + + + + Cancel + handleDeleteCustomDomain(deployment)} + className="bg-orange-600 hover:bg-orange-700 text-white" + > + Delete Domain + + + + + )} + + + e.preventDefault()} + > + + Delete Project + + + + + Delete Project + + Are you sure you want to delete the project "{deployment.folderPath}"? + This action cannot be undone and will permanently remove the deployed application. + + + + Cancel + handleDeleteDeployment(deployment)} + className="bg-red-600 hover:bg-red-700 text-white" + > + Delete + + + + + + +
+
- {formatDate(deployment.updatedAt)} - - - - {deployment.instanceName} - - - {deployment.url} + {formatDate(deployment.createdAt)}
{deployment.customDomain && (
Custom Domain: - {deployment.customDomain} + + {deployment.customDomain} + {deployment.customDomainStatus === 'pending' && ( (Propagation pending) @@ -560,18 +671,6 @@ export function ProjectsPage() { )}
)} -
-
- - {deployment.customDomain && deployment.publicIp && ( - - - + DNS Configuration @@ -685,55 +774,7 @@ export function ProjectsPage() { )} - - {deployment.customDomain && ( - - - - - - - Delete Custom Domain - - Are you sure you want to delete the custom domain "{deployment.customDomain}"? - This will remove the custom domain configuration but won't delete the deployment itself. - - - - Cancel - handleDeleteCustomDomain(deployment)} - className={cn( - "bg-orange-600 hover:bg-orange-700 focus:ring-orange-500", - "dark:bg-orange-600 dark:hover:bg-orange-700" - )} - > - Delete Domain - - - - - )} - + { @@ -743,22 +784,6 @@ export function ProjectsPage() { } }} > - - - Setup Custom Domain @@ -804,60 +829,13 @@ export function ProjectsPage() { - - - - - - - Delete Deployment - - Are you sure you want to delete the deployment "{deployment.folderPath}"? - This action cannot be undone and will permanently remove the deployed application. - - - - Cancel - handleDeleteDeployment(deployment)} - className={cn( - "bg-red-600 hover:bg-red-700 focus:ring-red-500", - "dark:bg-red-600 dark:hover:bg-red-700" - )} - > - Delete - - - - -
- {index < deployments.length - 1 && } -
+ + ))}

)} - - +
diff --git a/client/src/pages/SubscriptionPage.tsx b/client/src/pages/SubscriptionPage.tsx index 5086b68..5a1c9f5 100644 --- a/client/src/pages/SubscriptionPage.tsx +++ b/client/src/pages/SubscriptionPage.tsx @@ -9,7 +9,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Check, ExternalLink, AlertCircle, Key, ShieldAlert, CreditCard, ArrowRight, Coins } from "lucide-react"; +import { Check, AlertCircle, Key, ShieldAlert, CreditCard, ArrowRight, Coins } from "lucide-react"; import { Separator } from "@/components/ui/separator"; import { getCustomerProfile, @@ -19,12 +19,11 @@ import { cancelSubscription } from "@/api/stripe"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { PAYMENT_LINKS, TOPUP_TOKENS, PLAN_LINKS } from "@/constants/plans"; +import { PAYMENT_LINKS, TOPUP_TOKENS, PLAN_LINKS, PLAN_FEATURES, PLAN_PRICES } from "@/constants/plans"; import SpinnerShape from "@/components/SpinnerShape"; import { PlanSummary } from "@/components/subscription/PlanSummary"; -import { PlanUpgrade } from "@/components/subscription/PlanUpgrade"; import { TokenUsage } from "@/components/subscription/TokenUsage"; -import { PremiumPlanIcon } from "@/components/icons/PlanIcons"; +import { FreePlanIcon, ProPlanIcon, PremiumPlanIcon } from "@/components/icons/PlanIcons"; interface CustomerProfile { id?: string; @@ -97,6 +96,7 @@ export function SubscriptionPage() { const [error, setError] = useState(null); const [currentUser, setCurrentUser] = useState(null); const [showTopUpModal, setShowTopUpModal] = useState(false); + const [showChangePlanModal, setShowChangePlanModal] = useState(false); const [cancellingSubscription, setCancellingSubscription] = useState(false); const { toast } = useToast(); @@ -144,9 +144,8 @@ export function SubscriptionPage() { fetchData(); }, [toast]); - const handleContactForEnterprise = () => { - // Point to - window.open("https://www.pythagora.ai/contact", "_blank"); + const handleChangePlan = () => { + setShowChangePlanModal(true); }; const handleShowTopUp = () => { @@ -399,32 +398,14 @@ export function SubscriptionPage() { )} - {/* Free Trial Status */} -
-

Free Trial Status

-
- - {customerProfile?.isFreeTrial ? "On Free Trial" : "Not on Free Trial"} - -
-
- - - {/* Plan Summary */} - {/* Plan Upgrade Section */} - - {/* Token Usage */}
+ {/* Change Plan Modal */} + + + + Change Plan + + +
+
+ {/* Free Plan */} + + {(!customerProfile?.currentSubscription?.planType || + customerProfile?.currentSubscription?.planType?.toLowerCase() === 'free') && ( +
+
+ Current plan +
+
+ )} + +
+ +
+ Free Plan + + $0/month + +
+
+
+ +
    +
  • + + Use your own API keys +
  • +
  • + + Build frontend-only apps +
  • +
  • + + 1 deployed app +
  • +
  • + + Watermark on deployed apps +
  • +
+
+ + {(!customerProfile?.currentSubscription?.planType || + customerProfile?.currentSubscription?.planType?.toLowerCase() === 'free') ? ( + + ) : ( + + )} + +
+ + {/* Pro Plan */} + + {customerProfile?.currentSubscription?.planType?.toLowerCase() === 'pro' && ( +
+
+ Current plan +
+
+ )} + +
+ +
+ Pro Plan + + ${PLAN_PRICES.Pro}/month + +
+
+
+ +
    + {PLAN_FEATURES.Pro.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+ + {customerProfile?.currentSubscription?.planType?.toLowerCase() === 'pro' ? ( + + ) : ( + + )} + +
+ + {/* Premium Plan */} + + {customerProfile?.currentSubscription?.planType?.toLowerCase() === 'premium' && ( +
+
+ Current plan +
+
+ )} + +
+ +
+ Premium Plan + + ${PLAN_PRICES.Premium}/month + +
+
+
+ +
    + {PLAN_FEATURES.Premium.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+ + {customerProfile?.currentSubscription?.planType?.toLowerCase() === 'premium' ? ( + + ) : ( + + )} + +
+
+
+ + +
+
+ {/* Top Up Modal */}