diff options
| -rw-r--r-- | apps/web/components/views/billing.tsx | 53 | ||||
| -rw-r--r-- | apps/web/components/views/integrations.tsx | 273 | ||||
| -rw-r--r-- | apps/web/components/views/profile.tsx | 4 |
3 files changed, 161 insertions, 169 deletions
diff --git a/apps/web/components/views/billing.tsx b/apps/web/components/views/billing.tsx index 22bed2f7..4745ff8c 100644 --- a/apps/web/components/views/billing.tsx +++ b/apps/web/components/views/billing.tsx @@ -22,23 +22,25 @@ export function BillingView() { analytics.billingViewed() }, []) - const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any) + const { + data: status = { + consumer_pro: null, + }, + isLoading: isCheckingStatus, + } = fetchSubscriptionStatus(autumn, !autumn.isLoading) + + const { data: memoriesCheck } = fetchMemoriesFeature( + autumn, + !autumn.isLoading && !isCheckingStatus, + ) const memoriesUsed = memoriesCheck?.usage ?? 0 const memoriesLimit = memoriesCheck?.included_usage ?? 0 - const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any) + const { data: connectionsCheck } = fetchConnectionsFeature(autumn) const connectionsUsed = connectionsCheck?.usage ?? 0 - // Fetch subscription status with React Query - const { - data: status = { - consumer_pro: null, - }, - isLoading: isCheckingStatus, - } = fetchSubscriptionStatus(autumn as any) - // Handle upgrade const handleUpgrade = async () => { analytics.upgradeInitiated() @@ -233,23 +235,20 @@ export function BillingView() { </div> </div> - <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}> - <Button - className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white border-0 w-full" - disabled={isLoading || isCheckingStatus} - onClick={handleUpgrade} - size="sm" - > - {isLoading || isCheckingStatus ? ( - <> - <LoaderIcon className="h-4 w-4 animate-spin mr-2" /> - Upgrading... - </> - ) : ( - <div>Upgrade to Pro - $15/month (only for first 100 users)</div> - )} - </Button> - </motion.div> + <Button + className="bg-blue-600 hover:bg-blue-700 text-white border-0 w-full" + disabled={isLoading || isCheckingStatus} + onClick={handleUpgrade} + > + {isLoading || isCheckingStatus ? ( + <> + <LoaderIcon className="h-4 w-4 animate-spin mr-2" /> + Upgrading... + </> + ) : ( + <div>Upgrade to Pro - $15/month (only for first 100 users)</div> + )} + </Button> <p className="text-xs text-muted-foreground text-center"> Cancel anytime. No questions asked. diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx index 5f9a1eeb..ef16450f 100644 --- a/apps/web/components/views/integrations.tsx +++ b/apps/web/components/views/integrations.tsx @@ -1,38 +1,39 @@ -import { $fetch } from "@lib/api"; -import { authClient } from "@lib/auth"; -import { useAuth } from "@lib/auth-context"; -import { generateId } from "@lib/generate-id"; +import { $fetch } from "@lib/api" +import { authClient } from "@lib/auth" +import { useAuth } from "@lib/auth-context" +import { generateId } from "@lib/generate-id" import { ADD_MEMORY_SHORTCUT_URL, SEARCH_MEMORY_SHORTCUT_URL, -} from "@repo/lib/constants"; +} from "@repo/lib/constants" import { fetchConnectionsFeature, fetchConsumerProProduct, -} from "@repo/lib/queries"; -import { Button } from "@repo/ui/components/button"; +} from "@repo/lib/queries" +import { Button } from "@repo/ui/components/button" import { Dialog, DialogContent, DialogHeader, DialogPortal, DialogTitle, -} from "@repo/ui/components/dialog"; -import { Skeleton } from "@repo/ui/components/skeleton"; -import type { ConnectionResponseSchema } from "@repo/validation/api"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"; -import { useCustomer } from "autumn-js/react"; -import { Check, Copy, Smartphone, Trash2 } from "lucide-react"; -import { motion } from "motion/react"; -import Image from "next/image"; -import { useEffect, useId, useState } from "react"; -import { toast } from "sonner"; -import type { z } from "zod"; -import { analytics } from "@/lib/analytics"; -import { useProject } from "@/stores"; - -type Connection = z.infer<typeof ConnectionResponseSchema>; +} from "@repo/ui/components/dialog" +import { Skeleton } from "@repo/ui/components/skeleton" +import type { ConnectionResponseSchema } from "@repo/validation/api" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" +import { useCustomer } from "autumn-js/react" +import { Check, Copy, Smartphone, Trash2 } from "lucide-react" +import { motion } from "motion/react" +import Image from "next/image" +import { useEffect, useId, useState } from "react" +import { toast } from "sonner" +import type { z } from "zod" +import { analytics } from "@/lib/analytics" +import { useProject } from "@/stores" +import { cn } from "@lib/utils" + +type Connection = z.infer<typeof ConnectionResponseSchema> const CONNECTORS = { "google-drive": { @@ -50,9 +51,9 @@ const CONNECTORS = { description: "Access your Microsoft Office documents", icon: OneDrive, }, -} as const; +} as const -type ConnectorProvider = keyof typeof CONNECTORS; +type ConnectorProvider = keyof typeof CONNECTORS const ChromeIcon = ({ className }: { className?: string }) => ( <svg @@ -83,41 +84,41 @@ const ChromeIcon = ({ className }: { className?: string }) => ( d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z" /> </svg> -); +) export function IntegrationsView() { - const { org } = useAuth(); - const queryClient = useQueryClient(); - const { selectedProject } = useProject(); - const autumn = useCustomer(); - const [showApiKeyModal, setShowApiKeyModal] = useState(false); - const [apiKey, setApiKey] = useState<string>(""); - const [copied, setCopied] = useState(false); + const { org } = useAuth() + const queryClient = useQueryClient() + const { selectedProject } = useProject() + const autumn = useCustomer() + const [showApiKeyModal, setShowApiKeyModal] = useState(false) + const [apiKey, setApiKey] = useState<string>("") + const [copied, setCopied] = useState(false) const [selectedShortcutType, setSelectedShortcutType] = useState< "add" | "search" | null - >(null); - const apiKeyId = useId(); + >(null) + const apiKeyId = useId() const handleUpgrade = async () => { try { await autumn.attach({ productId: "consumer_pro", successUrl: "https://app.supermemory.ai/", - }); - window.location.reload(); + }) + window.location.reload() } catch (error) { - console.error(error); + console.error(error) } - }; + } - const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any); - const connectionsUsed = connectionsCheck?.balance ?? 0; - const connectionsLimit = connectionsCheck?.included_usage ?? 0; + const { data: connectionsCheck } = fetchConnectionsFeature(autumn) + const connectionsUsed = connectionsCheck?.balance ?? 0 + const connectionsLimit = connectionsCheck?.included_usage ?? 0 - const { data: proCheck } = fetchConsumerProProduct(autumn as any); - const isProUser = proCheck?.allowed ?? false; + const { data: proCheck } = fetchConsumerProProduct(autumn) + const isProUser = proCheck?.allowed ?? false - const canAddConnection = connectionsUsed < connectionsLimit; + const canAddConnection = connectionsUsed < connectionsLimit const { data: connections = [], @@ -130,19 +131,17 @@ export function IntegrationsView() { body: { containerTags: [], }, - }); + }) if (response.error) { - throw new Error( - response.error?.message || "Failed to load connections", - ); + throw new Error(response.error?.message || "Failed to load connections") } - return response.data as Connection[]; + return response.data as Connection[] }, staleTime: 30 * 1000, refetchInterval: 60 * 1000, - }); + }) useEffect(() => { if (connectionsError) { @@ -151,16 +150,16 @@ export function IntegrationsView() { connectionsError instanceof Error ? connectionsError.message : "Unknown error", - }); + }) } - }, [connectionsError]); + }, [connectionsError]) const addConnectionMutation = useMutation({ mutationFn: async (provider: ConnectorProvider) => { if (!canAddConnection && !isProUser) { throw new Error( "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.", - ); + ) } const response = await $fetch("@post/connections/:provider", { @@ -169,47 +168,47 @@ export function IntegrationsView() { redirectUrl: window.location.href, containerTags: [selectedProject], }, - }); + }) // biome-ignore lint/style/noNonNullAssertion: its fine if ("data" in response && !("error" in response.data!)) { - return response.data; + return response.data } - throw new Error(response.error?.message || "Failed to connect"); + throw new Error(response.error?.message || "Failed to connect") }, onSuccess: (data, provider) => { - analytics.connectionAdded(provider); - analytics.connectionAuthStarted(); + analytics.connectionAdded(provider) + analytics.connectionAuthStarted() if (data?.authLink) { - window.location.href = data.authLink; + window.location.href = data.authLink } }, onError: (error, provider) => { - analytics.connectionAuthFailed(); + analytics.connectionAuthFailed() toast.error(`Failed to connect ${provider}`, { description: error instanceof Error ? error.message : "Unknown error", - }); + }) }, - }); + }) const deleteConnectionMutation = useMutation({ mutationFn: async (connectionId: string) => { - await $fetch(`@delete/connections/${connectionId}`); + await $fetch(`@delete/connections/${connectionId}`) }, onSuccess: () => { - analytics.connectionDeleted(); + analytics.connectionDeleted() toast.success( "Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.", - ); - queryClient.invalidateQueries({ queryKey: ["connections"] }); + ) + queryClient.invalidateQueries({ queryKey: ["connections"] }) }, onError: (error) => { toast.error("Failed to remove connection", { description: error instanceof Error ? error.message : "Unknown error", - }); + }) }, - }); + }) const createApiKeyMutation = useMutation({ mutationFn: async () => { @@ -220,72 +219,72 @@ export function IntegrationsView() { }, name: `ios-${generateId().slice(0, 8)}`, prefix: `sm_${org?.id}_`, - }); - return res.key; + }) + return res.key }, onSuccess: (apiKey) => { - setApiKey(apiKey); - setShowApiKeyModal(true); - setCopied(false); - handleCopyApiKey(); + setApiKey(apiKey) + setShowApiKeyModal(true) + setCopied(false) + handleCopyApiKey() }, onError: (error) => { toast.error("Failed to create API key", { description: error instanceof Error ? error.message : "Unknown error", - }); + }) }, - }); + }) const handleShortcutClick = (shortcutType: "add" | "search") => { - setSelectedShortcutType(shortcutType); - createApiKeyMutation.mutate(); - }; + setSelectedShortcutType(shortcutType) + createApiKeyMutation.mutate() + } const handleCopyApiKey = async () => { try { - await navigator.clipboard.writeText(apiKey); - setCopied(true); - toast.success("API key copied to clipboard!"); - setTimeout(() => setCopied(false), 2000); + await navigator.clipboard.writeText(apiKey) + setCopied(true) + toast.success("API key copied to clipboard!") + setTimeout(() => setCopied(false), 2000) } catch { - toast.error("Failed to copy API key"); + toast.error("Failed to copy API key") } - }; + } const handleOpenShortcut = () => { if (!selectedShortcutType) { - toast.error("No shortcut type selected"); - return; + toast.error("No shortcut type selected") + return } if (selectedShortcutType === "add") { - window.open(ADD_MEMORY_SHORTCUT_URL, "_blank"); + window.open(ADD_MEMORY_SHORTCUT_URL, "_blank") } else if (selectedShortcutType === "search") { - window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank"); + window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank") } - }; + } const handleDialogClose = (open: boolean) => { - setShowApiKeyModal(open); + setShowApiKeyModal(open) if (!open) { // Reset state when dialog closes - setSelectedShortcutType(null); - setApiKey(""); - setCopied(false); + setSelectedShortcutType(null) + setApiKey("") + setCopied(false) } - }; + } return ( - <div className="space-y-4 sm:space-y-4 custom-scrollbar"> + <div className="space-y-2 sm:space-y-4 custom-scrollbar"> {/* iOS Shortcuts */} <div className="bg-card border border-border rounded-xl overflow-hidden"> <div className="p-4 sm:p-5"> <div className="flex items-start gap-3 mb-3"> - <div className="p-2 bg-blue-500/20 rounded-lg flex-shrink-0"> - <Smartphone className="h-5 w-5 text-blue-600 dark:text-blue-400" /> + <div className="p-2 rounded-lg flex-shrink-0 bg-muted/70"> + <Smartphone className="h-5 w-5" /> </div> <div className="flex-1 min-w-0"> - <h3 className="text-foreground font-semibold text-base mb-1"> + <h3 className="text-foreground font-semibold text-base"> Apple shortcuts </h3> <p className="text-muted-foreground text-sm leading-relaxed"> @@ -296,9 +295,10 @@ export function IntegrationsView() { <div className="flex flex-col sm:flex-row gap-2 sm:gap-3"> <Button variant="outline" - className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30" + className="flex-1 bg-muted/50 border-none" onClick={() => handleShortcutClick("add")} disabled={createApiKeyMutation.isPending} + size="lg" > <Image src="/images/ios-shortcuts.png" @@ -312,9 +312,10 @@ export function IntegrationsView() { </Button> <Button variant="outline" - className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30" + className="flex-1 bg-muted/50 border-none" onClick={() => handleShortcutClick("search")} disabled={createApiKeyMutation.isPending} + size="lg" > <Image src="/images/ios-shortcuts.png" @@ -334,15 +335,15 @@ export function IntegrationsView() { <div className="bg-card border border-border rounded-xl overflow-hidden opacity-75"> <div className="p-4 sm:p-5"> <div className="flex items-start gap-3"> - <div className="p-2 bg-orange-500/20 rounded-lg flex-shrink-0"> + <div className="p-2 rounded-lg flex-shrink-0 bg-muted/70"> <ChromeIcon className="h-5 w-5 text-orange-600 dark:text-orange-400" /> </div> <div className="flex-1 min-w-0"> - <div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-1"> + <div className="flex flex-col sm:flex-row sm:items-center gap-2"> <h3 className="text-foreground font-semibold text-base"> Chrome Extension </h3> - <div className="px-2 py-1 bg-orange-500/20 text-orange-600 dark:text-orange-400 text-xs rounded-full flex-shrink-0 w-fit"> + <div className="px-2 py-1 bg-muted/70 text-xs rounded-full flex-shrink-0 w-fit"> Coming Soon </div> </div> @@ -357,8 +358,8 @@ export function IntegrationsView() { {/* Connections Section */} <div className="bg-card border border-border rounded-xl overflow-hidden"> <div className="p-4 sm:p-5"> - <div className="flex items-start gap-3 mb-3"> - <div className="p-2 bg-green-500/20 rounded-lg flex-shrink-0"> + <div className="flex items-start gap-3"> + <div className="p-2 rounded-lg flex-shrink-0 bg-muted/70"> <svg className="h-5 w-5 text-green-600 dark:text-green-400" fill="none" @@ -375,43 +376,35 @@ export function IntegrationsView() { </svg> </div> <div className="flex-1 min-w-0"> - <h3 className="text-foreground font-semibold text-base mb-1"> + <h3 className="text-foreground font-semibold text-base"> Connections </h3> <p className="text-muted-foreground text-sm leading-relaxed mb-2"> Connect your accounts to sync document. </p> - {!isProUser && ( - <p className="text-xs text-muted-foreground"> - Connections require a Pro subscription - </p> - )} </div> </div> {/* Show upgrade prompt for free users */} {!isProUser && ( - <motion.div - animate={{ opacity: 1, y: 0 }} - className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-3" - initial={{ opacity: 0, y: -10 }} - > - <p className="text-sm text-yellow-600 dark:text-yellow-400 mb-2"> - 🔌 Connections are a Pro feature - </p> - <p className="text-xs text-muted-foreground mb-3"> - Connect Google Drive, Notion, OneDrive and more to automatically - sync your documents. - </p> + <div className="flex p-4 bg-muted/80 rounded-lg mb-2 justify-between items-center"> + <div> + <p className="text-sm mb-1">🔌 Connections are a Pro feature</p> + <p className="text-xs text-muted-foreground"> + Connect Google Drive, Notion, OneDrive to automatically + sync your documents. + </p> + </div> + <Button - className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-600 dark:text-yellow-400 border-yellow-500/30 w-full sm:w-auto" + className="bg-[#267ffa] hover:bg-[#267ffa]/90 text-white border-0 w-full sm:w-auto rounded-full" onClick={handleUpgrade} - size="sm" + size="lg" variant="secondary" > Upgrade to Pro </Button> - </motion.div> + </div> )} {/* All Connections with Status */} @@ -430,13 +423,13 @@ export function IntegrationsView() { ))} </div> ) : ( - <div className="space-y-2"> + <div className={cn("space-y-2", !isProUser && "opacity-50 pointer-events-none cursor-not-allowed")}> {Object.entries(CONNECTORS).map(([provider, config], index) => { - const Icon = config.icon; + const Icon = config.icon const connection = connections.find( (conn) => conn.provider === provider, - ); - const isConnected = !!connection; + ) + const isConnected = !!connection return ( <motion.div @@ -462,14 +455,14 @@ export function IntegrationsView() { </p> {isConnected ? ( <div className="flex items-center gap-1"> - <div className="w-2 h-2 bg-green-500 rounded-full"></div> + <div className="w-2 h-2 bg-green-500 rounded-full" /> <span className="text-xs text-green-600 dark:text-green-400 font-medium"> Connected </span> </div> ) : ( <div className="hidden sm:flex items-center gap-1"> - <div className="w-2 h-2 bg-muted-foreground rounded-full"></div> + <div className="w-2 h-2 bg-muted-foreground rounded-full" /> <span className="text-xs text-muted-foreground font-medium"> Disconnected </span> @@ -509,7 +502,7 @@ export function IntegrationsView() { ) : ( <div className="flex items-center justify-between gap-2 w-full sm:w-auto"> <div className="sm:hidden flex items-center gap-1"> - <div className="w-2 h-2 bg-muted-foreground rounded-full"></div> + <div className="w-2 h-2 bg-muted-foreground rounded-full" /> <span className="text-xs text-muted-foreground font-medium"> Disconnected </span> @@ -520,12 +513,12 @@ export function IntegrationsView() { className="flex-shrink-0" > <Button - className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-600 dark:text-blue-400 border-blue-500/30 min-w-[80px]" + className=" text-white border-0 min-w-[80px]" disabled={addConnectionMutation.isPending} onClick={() => { addConnectionMutation.mutate( provider as ConnectorProvider, - ); + ) }} size="sm" variant="outline" @@ -540,7 +533,7 @@ export function IntegrationsView() { )} </div> </motion.div> - ); + ) })} </div> )} @@ -555,7 +548,7 @@ export function IntegrationsView() { href="https://x.com/supermemoryai" target="_blank" rel="noopener noreferrer" - className="text-orange-600 dark:text-orange-400 hover:text-orange-500 underline" + className="text-[#267ffa] hover:text-[#267ffa]/90 underline" > X </a> @@ -665,5 +658,5 @@ export function IntegrationsView() { </DialogPortal> </Dialog> </div> - ); + ) } diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx index 7a6699c4..550f1b97 100644 --- a/apps/web/components/views/profile.tsx +++ b/apps/web/components/views/profile.tsx @@ -238,10 +238,10 @@ export function ProfileView() { </Button> ) : ( <Button - className="w-full bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white border-0" + className="w-full bg-[#267ffa] hover:bg-[#267ffa]/90 text-white border-0" disabled={isLoading || isCheckingStatus} onClick={handleUpgrade} - size="sm" + size="lg" > {isLoading || isCheckingStatus ? ( <> |