"use client"
import { dmSans125ClassName } from "@/lib/fonts"
import { OAUTH_PLUGINS } from "@/lib/oauth-plugins"
import { cn } from "@lib/utils"
import { Building2, ExternalLink, LoaderIcon, Plug, Trash2 } from "lucide-react"
import Image from "next/image"
import { useCallback, useEffect, useState } from "react"
const API_URL =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
interface Connection {
clientId: string
name: string
icon: string | null
isFirstParty: boolean
workspaceId: string | null
workspaceName: string | null
scopes: string[]
connectedAt: string | null
lastUsedAt: string | null
}
function relativeTime(iso: string | null): string | null {
if (!iso) return null
const then = new Date(iso).getTime()
if (Number.isNaN(then)) return null
const diff = Date.now() - then
const mins = Math.round(diff / 60000)
if (mins < 1) return "just now"
if (mins < 60) return `${mins}m ago`
const hrs = Math.round(mins / 60)
if (hrs < 24) return `${hrs}h ago`
const days = Math.round(hrs / 24)
if (days < 30) return `${days}d ago`
const months = Math.round(days / 30)
if (months < 12) return `${months}mo ago`
return `${Math.round(months / 12)}y ago`
}
const cardClass = cn(
"rounded-[14px] bg-[#14161A] p-5",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
)
function PluginIcon({ src, alt }: { src: string | null; alt: string }) {
const [failed, setFailed] = useState(false)
if (!src || failed) {
return (
)
}
return (
setFailed(true)}
src={src}
width={20}
/>
)
}
export default function ConnectPage() {
const [connections, setConnections] = useState(null)
const [error, setError] = useState(null)
const [revoking, setRevoking] = useState(null)
const load = useCallback(async () => {
try {
const res = await fetch(`${API_URL}/v3/oauth/grants`, {
credentials: "include",
})
if (!res.ok) throw new Error(`Failed to load connections (${res.status})`)
const data = (await res.json()) as { grants: Connection[] }
setConnections(data.grants)
setError(null)
} catch (err) {
console.error("Failed to load connections:", err)
setError(
err instanceof Error ? err.message : "Failed to load connections",
)
setConnections([])
}
}, [])
useEffect(() => {
load()
}, [load])
async function revoke(clientId: string) {
setRevoking(clientId)
try {
const res = await fetch(
`${API_URL}/v3/oauth/grants/${encodeURIComponent(clientId)}`,
{ method: "DELETE", credentials: "include" },
)
if (!res.ok && res.status !== 204)
throw new Error(`Failed to revoke (${res.status})`)
setConnections((prev) =>
prev ? prev.filter((c) => c.clientId !== clientId) : prev,
)
} catch (err) {
console.error("Failed to revoke connection:", err)
setError(err instanceof Error ? err.message : "Failed to revoke")
} finally {
setRevoking(null)
}
}
const connectedClientIds = new Set(connections?.map((c) => c.clientId) ?? [])
return (
Connections
Apps and plugins you've connected to your Supermemory account.
Connected apps
{connections === null ? (
Loading…
) : connections.length === 0 ? (
No apps connected yet
Connect a plugin below — anything you authorize will show up here.
) : (
{connections.map((c) => {
const connectedRel = relativeTime(c.connectedAt)
const usedRel = relativeTime(c.lastUsedAt)
return (
{c.name}
{!c.isFirstParty && (
external
)}
{c.workspaceName && (
{c.workspaceName}
)}
{connectedRel && Connected {connectedRel}}
{usedRel && Last used {usedRel}}
)
})}
)}
{error && {error}
}
Available plugins
{OAUTH_PLUGINS.map((p) => {
const isConnected =
p.oauthClientId != null && connectedClientIds.has(p.oauthClientId)
return (
{p.name}
{isConnected && (
Connected
)}
{p.description}
Setup guide
)
})}
)
}