aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components/views/integrations.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components/views/integrations.tsx')
-rw-r--r--apps/web/components/views/integrations.tsx264
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>
- )
+ );
}