diff options
Diffstat (limited to 'apps/web/components/views/integrations.tsx')
| -rw-r--r-- | apps/web/components/views/integrations.tsx | 264 |
1 files changed, 133 insertions, 131 deletions
diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx index b3e3c92d..1724231f 100644 --- a/apps/web/components/views/integrations.tsx +++ b/apps/web/components/views/integrations.tsx @@ -1,35 +1,35 @@ -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" -import { fetchConnectionsFeature } from "@repo/lib/queries" -import { Button } from "@repo/ui/components/button" +} from "@repo/lib/constants"; +import { fetchConnectionsFeature } 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"; + +type Connection = z.infer<typeof ConnectionResponseSchema>; const CONNECTORS = { "google-drive": { @@ -47,66 +47,66 @@ 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 - xmlns="http://www.w3.org/2000/svg" + className={className} preserveAspectRatio="xMidYMid" viewBox="0 0 190.5 190.5" - className={className} + xmlns="http://www.w3.org/2000/svg" > <title>Google Chrome Icon</title> <path - fill="#fff" d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z" + fill="#fff" /> <path - fill="#229342" d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z" + fill="#229342" /> <path - fill="#fbc116" d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z" + fill="#fbc116" /> <path - fill="#1a73e8" d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z" + fill="#1a73e8" /> <path - fill="#e33b2e" 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" + fill="#e33b2e" /> </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 [isProUser, setIsProUser] = 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 [isProUser, setIsProUser] = 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); } - } + }; useEffect(() => { if (!autumn.isLoading) { @@ -114,15 +114,15 @@ export function IntegrationsView() { autumn.customer?.products.some( (product) => product.id === "consumer_pro", ) ?? false, - ) + ); } - }, [autumn.isLoading, autumn.customer]) + }, [autumn.isLoading, autumn.customer]); - const { data: connectionsCheck } = fetchConnectionsFeature(autumn) - 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 canAddConnection = connectionsUsed < connectionsLimit + const canAddConnection = connectionsUsed < connectionsLimit; const { data: connections = [], @@ -135,17 +135,19 @@ 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) { @@ -154,16 +156,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", { @@ -172,47 +174,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 () => { @@ -223,60 +225,60 @@ 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"> @@ -298,32 +300,32 @@ export function IntegrationsView() { </div> <div className="flex flex-col sm:flex-row gap-2 sm:gap-3"> <Button - variant="ghost" className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75 " - onClick={() => handleShortcutClick("add")} disabled={createApiKeyMutation.isPending} + onClick={() => handleShortcutClick("add")} + variant="ghost" > <Image - src="/images/ios-shortcuts.png" alt="iOS Shortcuts" - width={20} height={20} + src="/images/ios-shortcuts.png" + width={20} /> {createApiKeyMutation.isPending ? "Creating..." : "Add Memory Shortcut"} </Button> <Button - variant="ghost" className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75" - onClick={() => handleShortcutClick("search")} disabled={createApiKeyMutation.isPending} + onClick={() => handleShortcutClick("search")} + variant="ghost" > <Image - src="/images/ios-shortcuts.png" alt="iOS Shortcuts" - width={20} height={20} + src="/images/ios-shortcuts.png" + width={20} /> {createApiKeyMutation.isPending ? "Creating..." @@ -352,8 +354,8 @@ export function IntegrationsView() { "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc", "_blank", "noopener,noreferrer", - ) - analytics.extensionInstallClicked() + ); + analytics.extensionInstallClicked(); }} size="sm" variant="ghost" @@ -465,11 +467,11 @@ export function IntegrationsView() { ) : ( <div className="space-y-2"> {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 @@ -548,9 +550,9 @@ export function IntegrationsView() { </span> </div> <motion.div + className="flex-shrink-0" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} - className="flex-shrink-0" > <Button className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px] disabled:cursor-not-allowed" @@ -560,7 +562,7 @@ export function IntegrationsView() { onClick={() => { addConnectionMutation.mutate( provider as ConnectorProvider, - ) + ); }} size="sm" variant="outline" @@ -575,7 +577,7 @@ export function IntegrationsView() { )} </div> </motion.div> - ) + ); })} </div> )} @@ -587,10 +589,10 @@ export function IntegrationsView() { More integrations are coming soon! Have a suggestion? Share it with us on{" "} <a + className="text-orange-500 hover:text-orange-400 underline" href="https://x.com/supermemoryai" - target="_blank" rel="noopener noreferrer" - className="text-orange-500 hover:text-orange-400 underline" + target="_blank" > X </a> @@ -599,7 +601,7 @@ export function IntegrationsView() { </div> {/* API Key Modal */} - <Dialog open={showApiKeyModal} onOpenChange={handleDialogClose}> + <Dialog onOpenChange={handleDialogClose} open={showApiKeyModal}> <DialogPortal> <DialogContent className="bg-[#0f1419] border-white/10 text-white md:max-w-md z-[100]"> <DialogHeader> @@ -618,24 +620,24 @@ export function IntegrationsView() { {/* API Key Section */} <div className="space-y-2"> <label - htmlFor={apiKeyId} className="text-sm font-medium text-white/80" + htmlFor={apiKeyId} > Your API Key </label> <div className="flex items-center gap-2"> <input + className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono" id={apiKeyId} + readOnly type="text" value={apiKey} - readOnly - className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono" /> <Button + className="text-white/70 hover:text-white hover:bg-white/10" + onClick={handleCopyApiKey} size="sm" variant="ghost" - onClick={handleCopyApiKey} - className="text-white/70 hover:text-white hover:bg-white/10" > {copied ? ( <Check className="h-4 w-4 text-green-400" /> @@ -681,16 +683,16 @@ export function IntegrationsView() { <div className="flex gap-2 pt-2"> <Button - onClick={handleOpenShortcut} className="flex-1 bg-blue-600 hover:bg-blue-700 text-white" disabled={!selectedShortcutType} + onClick={handleOpenShortcut} > <Image - src="/images/ios-shortcuts.png" alt="iOS Shortcuts" - width={16} - height={16} className="mr-2" + height={16} + src="/images/ios-shortcuts.png" + width={16} /> Add to Shortcuts </Button> @@ -700,5 +702,5 @@ export function IntegrationsView() { </DialogPortal> </Dialog> </div> - ) + ); } |