chore: quick bugs squash across the elements and added few more changes (#671)

This commit is contained in:
MaheshtheDev 2026-01-14 01:53:32 +00:00
parent 34c58c37fd
commit baf2848db4
19 changed files with 7174 additions and 96 deletions

View file

@ -13,7 +13,7 @@ import { Title1Bold } from "@ui/text/title/title-1-bold"
import { InitialHeader } from "@/components/initial-header"
import { useRouter, useSearchParams } from "next/navigation"
import { useState, useEffect } from "react"
import { motion } from "framer-motion"
import { motion } from "motion/react"
import { dmSansClassName } from "@/utils/fonts"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"

View file

@ -0,0 +1,156 @@
import ogs from "open-graph-scraper"
export const runtime = "nodejs"
interface OGResponse {
title: string
description: string
image?: string
}
function isValidUrl(urlString: string): boolean {
try {
const url = new URL(urlString)
return url.protocol === "http:" || url.protocol === "https:"
} catch {
return false
}
}
function isPrivateHost(hostname: string): boolean {
const lowerHost = hostname.toLowerCase()
// Block localhost variants
if (
lowerHost === "localhost" ||
lowerHost === "127.0.0.1" ||
lowerHost === "::1" ||
lowerHost.startsWith("127.") ||
lowerHost.startsWith("0.0.0.0")
) {
return true
}
// Block RFC 1918 private IP ranges
const privateIpPatterns = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
]
return privateIpPatterns.some((pattern) => pattern.test(hostname))
}
function extractImageUrl(image: unknown): string | undefined {
if (!image) return undefined
if (typeof image === "string") {
return image
}
if (Array.isArray(image) && image.length > 0) {
const first = image[0]
if (first && typeof first === "object" && "url" in first) {
return String(first.url)
}
}
if (typeof image === "object" && image !== null && "url" in image) {
return String(image.url)
}
return undefined
}
function resolveImageUrl(
imageUrl: string | undefined,
baseUrl: string,
): string | undefined {
if (!imageUrl) return undefined
try {
const url = new URL(imageUrl)
return url.href
} catch {
try {
const base = new URL(baseUrl)
return new URL(imageUrl, base.href).href
} catch {
return undefined
}
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const url = searchParams.get("url")
if (!url || !url.trim()) {
return Response.json(
{ error: "Missing or invalid url parameter" },
{ status: 400 },
)
}
const trimmedUrl = url.trim()
if (!isValidUrl(trimmedUrl)) {
return Response.json(
{ error: "Invalid URL. Must be http:// or https://" },
{ status: 400 },
)
}
const urlObj = new URL(trimmedUrl)
if (isPrivateHost(urlObj.hostname)) {
return Response.json(
{ error: "Private/localhost URLs are not allowed" },
{ status: 400 },
)
}
const { result, error } = await ogs({
url: trimmedUrl,
timeout: 8000,
fetchOptions: {
headers: {
"User-Agent":
"Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
},
},
})
if (error || !result) {
console.error("OG scraping error:", error)
return Response.json(
{ error: "Failed to fetch Open Graph data" },
{ status: 500 },
)
}
const ogTitle = result.ogTitle || result.twitterTitle || ""
const ogDescription =
result.ogDescription || result.twitterDescription || ""
const ogImageUrl =
extractImageUrl(result.ogImage) || extractImageUrl(result.twitterImage)
const resolvedImageUrl = resolveImageUrl(ogImageUrl, trimmedUrl)
const response: OGResponse = {
title: ogTitle,
description: ogDescription,
...(resolvedImageUrl && { image: resolvedImageUrl }),
}
return Response.json(response, {
headers: {
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
})
} catch (error) {
console.error("OG route error:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
}

View file

@ -9,7 +9,7 @@ import { AddDocumentModal } from "@/components/new/add-document"
import { MCPModal } from "@/components/new/mcp-modal"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "framer-motion"
import { AnimatePresence } from "motion/react"
export default function NewPage() {
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)

View file

@ -12,7 +12,7 @@ import {
} from "lucide-react"
import { NavMenu } from "./nav-menu"
import { useOnboarding } from "./onboarding-context"
import { motion, AnimatePresence, type ResolvedValues } from "framer-motion"
import { motion, AnimatePresence, type ResolvedValues } from "motion/react"
import { useEffect, useMemo, useRef, useState, useLayoutEffect } from "react"
import React from "react"
import { cn } from "@lib/utils"

View file

@ -14,7 +14,7 @@ import { CheckIcon, CircleCheckIcon, CopyIcon, LoaderIcon } from "lucide-react"
import { TextMorph } from "@/components/text-morph"
import { NavMenu } from "./nav-menu"
import { cn } from "@lib/utils"
import { motion, AnimatePresence } from "framer-motion"
import { motion, AnimatePresence } from "motion/react"
import { useQuery } from "@tanstack/react-query"
import { $fetch } from "@lib/api"

View file

@ -11,7 +11,7 @@ import {
TwitterIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
import { motion } from "framer-motion"
import { motion } from "motion/react"
import Image from "next/image"
import { analytics } from "@/lib/analytics"
import { useIsMobile } from "@hooks/use-mobile"

View file

@ -32,7 +32,7 @@ import { z } from "zod/v4"
import { analytics } from "@/lib/analytics"
import { cn } from "@lib/utils"
import type { Project } from "@repo/lib/types"
import { motion, AnimatePresence } from "framer-motion"
import { motion, AnimatePresence } from "motion/react"
const clients = {
cursor: "Cursor",

View file

@ -53,6 +53,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
const [isProUser, setIsProUser] = useState(false)
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(null)
const [isUpgrading, setIsUpgrading] = useState(false)
// Check Pro status
useEffect(() => {
@ -65,6 +66,20 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
}
}, [autumn.isLoading, autumn.customer])
const handleUpgrade = async () => {
setIsUpgrading(true)
try {
await autumn.attach({
productId: "consumer_pro",
successUrl: window.location.href,
})
} catch (error) {
console.error("Upgrade error:", error)
toast.error("Failed to start upgrade process")
setIsUpgrading(false)
}
}
// Check connections feature limits
const { data: connectionsCheck } = fetchConnectionsFeature(
autumn,
@ -359,15 +374,25 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
{!isProUser ? (
<>
<p className="text-[14px] text-[#737373] mb-4 text-center">
<a
href="/pricing"
className="underline text-[#737373] hover:text-white"
>
Upgrade to Pro
</a>{" "}
to get
<br />
Supermemory Connections
{isUpgrading || autumn.isLoading ? (
<span className="inline-flex items-center gap-2">
<Loader className="h-4 w-4 animate-spin" />
Upgrading...
</span>
) : (
<>
<button
type="button"
onClick={handleUpgrade}
className="underline text-[#737373] hover:text-white transition-colors cursor-pointer"
>
Upgrade to Pro
</button>{" "}
to get
<br />
Supermemory Connections
</>
)}
</p>
<div className="space-y-2 text-[14px]">
<div className="flex items-center gap-2">

View file

@ -30,7 +30,9 @@ import {
DropdownMenuTrigger,
} from "@repo/ui/components/dropdown-menu"
import { toast } from "sonner"
import { useDocumentMutations } from "./useDocumentMutations"
import { useDocumentMutations } from "../../../hooks/use-document-mutations"
import { useCustomer } from "autumn-js/react"
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
type TabType = "note" | "link" | "file" | "connect"
@ -132,6 +134,15 @@ export function AddDocument({
onClose,
})
const autumn = useCustomer()
const {
memoriesUsed,
memoriesLimit,
hasProProduct,
isLoading: isLoadingMemories,
usagePercent,
} = useMemoriesUsage(autumn)
useEffect(() => {
setLocalSelectedProject(globalSelectedProject)
}, [globalSelectedProject])
@ -242,7 +253,7 @@ export function AddDocument({
noteMutation.isPending || linkMutation.isPending || fileMutation.isPending
return (
<div className="h-full flex flex-row text-white space-x-6">
<div className="h-full flex flex-row text-white space-x-5">
<div className="w-1/3 flex flex-col justify-between">
<div className="flex flex-col gap-1">
{tabs.map((tab) => (
@ -266,7 +277,7 @@ export function AddDocument({
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
<div className="flex justify-between items-center mb-2">
<div className="flex justify-between items-center">
<span
className={cn(
"text-white text-[16px] font-medium",
@ -276,19 +287,25 @@ export function AddDocument({
Memories
</span>
<span className={cn("text-[#737373] text-sm", dmSansClassName())}>
120/200
{isLoadingMemories
? "…"
: hasProProduct
? "Unlimited"
: `${memoriesUsed}/${memoriesLimit}`}
</span>
</div>
<div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden">
<div
className="h-full bg-[#2261CA] rounded-full"
style={{ width: "60%" }}
/>
</div>
{!hasProProduct && (
<div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
<div
className="h-full bg-[#2261CA] rounded-full"
style={{ width: `${usagePercent}%` }}
/>
</div>
)}
</div>
</div>
<div className="w-2/3 overflow-auto flex flex-col justify-between">
<div className="w-2/3 overflow-auto flex flex-col justify-between px-1 scrollbar-thin">
{activeTab === "note" && (
<NoteContent
onSubmit={handleNoteSubmit}

View file

@ -5,11 +5,14 @@ import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { dmSansClassName } from "@/utils/fonts"
import { useHotkeys } from "react-hotkeys-hook"
import { Image as ImageIcon, Loader2 } from "lucide-react"
import { toast } from "sonner"
export interface LinkData {
url: string
title: string
description: string
image?: string
}
interface LinkContentProps {
@ -19,26 +22,50 @@ interface LinkContentProps {
isOpen?: boolean
}
export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: LinkContentProps) {
export function LinkContent({
onSubmit,
onDataChange,
isSubmitting,
isOpen,
}: LinkContentProps) {
const [url, setUrl] = useState("")
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [image, setImage] = useState<string | undefined>(undefined)
const [isPreviewLoading, setIsPreviewLoading] = useState(false)
const canSubmit = url.trim().length > 0 && !isSubmitting
const handleSubmit = () => {
if (canSubmit && onSubmit) {
onSubmit({ url, title, description })
let normalizedUrl = url.trim()
if (
!normalizedUrl.startsWith("http://") &&
!normalizedUrl.startsWith("https://")
) {
normalizedUrl = `https://${normalizedUrl}`
}
onSubmit({ url: normalizedUrl, title, description })
}
}
const updateData = (newUrl: string, newTitle: string, newDescription: string) => {
onDataChange?.({ url: newUrl, title: newTitle, description: newDescription })
const updateData = (
newUrl: string,
newTitle: string,
newDescription: string,
newImage?: string,
) => {
onDataChange?.({
url: newUrl,
title: newTitle,
description: newDescription,
...(newImage && { image: newImage }),
})
}
const handleUrlChange = (newUrl: string) => {
setUrl(newUrl)
updateData(newUrl, title, description)
updateData(newUrl, title, description, image)
}
const handleTitleChange = (newTitle: string) => {
@ -48,7 +75,60 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
const handleDescriptionChange = (newDescription: string) => {
setDescription(newDescription)
updateData(url, title, newDescription)
updateData(url, title, newDescription, image)
}
const handlePreviewLink = async () => {
if (!url.trim()) {
toast.error("Please enter a URL first")
return
}
let normalizedUrl = url.trim()
if (
!normalizedUrl.startsWith("http://") &&
!normalizedUrl.startsWith("https://")
) {
normalizedUrl = `https://${normalizedUrl}`
setUrl(normalizedUrl)
updateData(normalizedUrl, title, description, image)
}
setIsPreviewLoading(true)
try {
const response = await fetch(
`/api/og?url=${encodeURIComponent(normalizedUrl)}`,
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.error || "Failed to fetch preview")
}
const data = await response.json()
const newTitle = data.title || ""
const newDescription = data.description || ""
const newImage = data.image || undefined
setTitle(newTitle)
setDescription(newDescription)
setImage(newImage)
updateData(url, newTitle, newDescription, newImage)
if (!newTitle && !newDescription && !newImage) {
toast.info("No Open Graph data found for this URL")
} else {
toast.success("Preview loaded successfully")
}
} catch (error) {
console.error("Preview error:", error)
toast.error(
error instanceof Error ? error.message : "Failed to load preview",
)
} finally {
setIsPreviewLoading(false)
}
}
useHotkeys("mod+enter", handleSubmit, {
@ -62,12 +142,13 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
setUrl("")
setTitle("")
setDescription("")
setImage(undefined)
onDataChange?.({ url: "", title: "", description: "" })
}
}, [isOpen, onDataChange])
return (
<div className={cn("flex flex-col space-y-4 pt-4", dmSansClassName())}>
<div className={cn("flex flex-col space-y-4 pt-4 mb-4", dmSansClassName())}>
<div>
<p
className={cn("text-[16px] font-medium pl-2 pb-2", dmSansClassName())}
@ -79,14 +160,24 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
type="text"
value={url}
onChange={(e) => handleUrlChange(e.target.value)}
placeholder="https://maheshthedev.me"
placeholder="https://example.com"
disabled={isSubmitting}
className={cn(
"w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50",
)}
className="w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
/>
<Button variant="linkPreview" className="absolute right-2 top-2" disabled={isSubmitting}>
Preview Link
<Button
variant="linkPreview"
className="absolute right-2 top-2"
disabled={isSubmitting || isPreviewLoading || !url.trim()}
onClick={handlePreviewLink}
>
{isPreviewLoading ? (
<>
<Loader2 className="size-4 animate-spin mr-2" />
Loading...
</>
) : (
"Preview Link"
)}
</Button>
</div>
</div>
@ -100,8 +191,8 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
value={title}
onChange={(e) => handleTitleChange(e.target.value)}
placeholder="Mahesh Sanikommu - Portfolio"
disabled={isSubmitting}
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50"
disabled
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
/>
</div>
<div>
@ -112,17 +203,34 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
value={description}
onChange={(e) => handleDescriptionChange(e.target.value)}
placeholder="Portfolio website of Mahesh Sanikommu"
disabled={isSubmitting}
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50"
disabled
className="w-full px-4 py-3 bg-[#0F1217] rounded-xl resize-none disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
/>
</div>
<div>
<p className="pl-2 pb-2 font-semibold text-[16px] text-[#737373]">
Link Preview
Link Preview Image
</p>
<div className="w-full px-4 py-3 bg-[#0F1217] rounded-xl">
<p>{description || "Portfolio website of Mahesh Sanikommu"}</p>
</div>
{image ? (
<div className="w-full max-w-md aspect-4/2 bg-[#0F1217] rounded-xl overflow-hidden">
<img
src={image}
alt={title || "Link preview"}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = "none"
e.currentTarget.parentElement?.classList.add("opacity-50")
e.currentTarget.parentElement?.classList.add("flex")
e.currentTarget.parentElement?.classList.add("items-center")
e.currentTarget.parentElement?.classList.add("justify-center")
}}
/>
</div>
) : (
<div className="w-full max-w-md aspect-4/2 bg-[#0F1217] opacity-50 rounded-xl flex items-center justify-center">
<ImageIcon className="w-8 h-8 text-[#737373]" />
</div>
)}
</div>
</div>
</div>

View file

@ -10,7 +10,12 @@ interface NoteContentProps {
isOpen?: boolean
}
export function NoteContent({ onSubmit, onContentChange, isSubmitting, isOpen }: NoteContentProps) {
export function NoteContent({
onSubmit,
onContentChange,
isSubmitting,
isOpen,
}: NoteContentProps) {
const [content, setContent] = useState("")
const canSubmit = content.trim().length > 0 && !isSubmitting
@ -45,7 +50,7 @@ export function NoteContent({ onSubmit, onContentChange, isSubmitting, isOpen }:
onChange={(e) => handleContentChange(e.target.value)}
placeholder="Write your note here..."
disabled={isSubmitting}
className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50"
className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50 outline-none"
/>
)
}

View file

@ -12,6 +12,9 @@ import {
FolderIcon,
LogOut,
Settings,
Home,
Code2,
ExternalLink,
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
@ -31,6 +34,7 @@ import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { useProject } from "@/stores"
import { useRouter } from "next/navigation"
import Link from "next/link"
import type { Project } from "@repo/lib/types"
interface HeaderProps {
@ -69,19 +73,62 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
return (
<div className="flex p-4 justify-between items-center">
<div className="flex items-center justify-center gap-4 z-10!">
<div className="flex items-center">
<Logo className="h-7" />
{name && (
<div className="flex flex-col items-start justify-center ml-2">
<p className="text-[#8B8B8B] text-[11px] leading-tight">
{userName}
</p>
<p className="text-white font-bold text-xl leading-none -mt-1">
supermemory
</p>
</div>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors"
>
<Logo className="h-7" />
{name && (
<div className="flex flex-col items-start justify-center ml-2">
<p className="text-[#8B8B8B] text-[11px] leading-tight">
{userName}
</p>
<p className="text-white font-bold text-xl leading-none -mt-1">
supermemory
</p>
</div>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-56 bg-[#0D121A] rounded-xl border-none p-1.5 ml-4 shadow-[0_0_20px_rgba(255,255,255,0.15)]"
>
<DropdownMenuItem
asChild
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
>
<Link href="/new">
<Home className="h-4 w-4" />
Home
</Link>
</DropdownMenuItem>
<DropdownMenuItem
asChild
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
>
<a
href="https://console.supermemory.ai"
target="_blank"
rel="noreferrer"
>
<Code2 className="h-4 w-4" />
Developer console
</a>
</DropdownMenuItem>
<DropdownMenuItem
asChild
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
>
<a href="https://supermemory.ai" target="_blank" rel="noreferrer">
<ExternalLink className="h-4 w-4" />
supermemory.ai
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="self-stretch w-px bg-[#FFFFFF33]" />
<div className="flex items-center gap-2">
<p>📁 {projectName}</p>
@ -212,7 +259,10 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
<Settings className="h-4 w-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => authClient.signOut()}>
<DropdownMenuItem onClick={() => {
authClient.signOut()
router.push("/login/new")
}}>
<LogOut className="h-4 w-4" />
Logout
</DropdownMenuItem>

View file

@ -3,8 +3,8 @@
import { dmSans125ClassName } from "@/utils/fonts"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
import {
Dialog,
DialogContent,
@ -82,26 +82,13 @@ export default function Account() {
const [deleteConfirmText, setDeleteConfirmText] = useState("")
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
// Billing data
const {
data: status = {
consumer_pro: { allowed: false, status: null },
},
memoriesUsed,
memoriesLimit,
hasProProduct,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const proStatus = status.consumer_pro
const hasProProduct = proStatus?.status !== null
const { data: memoriesCheck } = fetchMemoriesFeature(
autumn,
!autumn.isLoading && !isCheckingStatus,
)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 200
// Calculate progress percentage
const usagePercent = Math.min((memoriesUsed / memoriesLimit) * 100, 100)
usagePercent,
} = useMemoriesUsage(autumn)
// Handlers
const handleUpgrade = async () => {

View file

@ -40,7 +40,7 @@ export function ActionButtons({
className="flex-1 sm:flex-initial"
>
<Button
className="w-full cursor-pointer"
className="w-full cursor-pointer text-black dark:text-white"
disabled={isSubmitting || isSubmitDisabled}
onClick={submitType === "button" ? onSubmit : undefined}
type={submitType}

View file

@ -72,11 +72,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.setQueryData(
["documents-with-memories", project],
(old: DocumentsQueryData | undefined) => {
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
const existingDocs = old?.documents ?? []
return {
...old,
documents: [optimisticMemory, ...old.documents],
totalCount: old.totalCount + 1,
documents: [optimisticMemory, ...existingDocs],
totalCount: (old?.totalCount ?? 0) + 1,
}
},
)
@ -155,11 +155,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.setQueryData(
["documents-with-memories", project],
(old: DocumentsQueryData | undefined) => {
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
const existingDocs = old?.documents ?? []
return {
...old,
documents: [optimisticMemory, ...old.documents],
totalCount: old.totalCount + 1,
documents: [optimisticMemory, ...existingDocs],
totalCount: (old?.totalCount ?? 0) + 1,
}
},
)
@ -272,11 +272,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.setQueryData(
["documents-with-memories", project],
(old: DocumentsQueryData | undefined) => {
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
const existingDocs = old?.documents ?? []
return {
...old,
documents: [optimisticMemory, ...old.documents],
totalCount: old.totalCount + 1,
documents: [optimisticMemory, ...existingDocs],
totalCount: (old?.totalCount ?? 0) + 1,
}
},
)

View file

@ -0,0 +1,35 @@
import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
import type { useCustomer } from "autumn-js/react"
export function useMemoriesUsage(autumn: ReturnType<typeof useCustomer>) {
const {
data: status = {
consumer_pro: { allowed: false, status: null },
},
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const proStatus = status.consumer_pro
const hasProProduct = proStatus?.status !== null
const { data: memoriesCheck, isLoading: isLoadingMemories } =
fetchMemoriesFeature(autumn, !isCheckingStatus && !autumn.isLoading)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
const isLoading = autumn.isLoading || isCheckingStatus || isLoadingMemories
const usagePercent =
memoriesLimit <= 0
? 0
: Math.min(Math.max((memoriesUsed / memoriesLimit) * 100, 0), 100)
return {
memoriesUsed,
memoriesLimit,
hasProProduct,
isLoading,
usagePercent,
}
}

View file

@ -41,7 +41,7 @@
"@react-router/fs-routes": "^7.6.2",
"@react-router/node": "^7.6.2",
"@react-router/serve": "^7.6.2",
"@sentry/nextjs": "^10.22.0",
"@sentry/nextjs": "^10.33.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-form": "^1.12.4",
"@tanstack/react-query": "^5.90.14",
@ -59,15 +59,15 @@
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"flubber": "^0.4.2",
"framer-motion": "^12.23.12",
"idb-keyval": "^6.2.2",
"is-hotkey": "^0.2.0",
"lucide-react": "^0.525.0",
"masonic": "^4.1.0",
"motion": "^12.19.2",
"motion": "^12.26.2",
"next": "16.0.9",
"next-themes": "^0.4.6",
"nuqs": "^2.5.2",
"open-graph-scraper": "^6.11.0",
"pdfjs-dist": "5.4.296",
"posthog-js": "^1.257.0",
"random-word-slugs": "^0.1.7",

6695
bun.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ const buttonVariants = cva(
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
headers:
"border-[#161F2C] border bg-gradient-to-b from-neutral-900 to-black !text-[14px]",
"border-[#161F2C] border bg-gradient-to-b from-neutral-900 to-black !text-[14px] hover:from-neutral-800 hover:to-neutral-950 hover:border-[#2a3a4f] active:from-neutral-950 active:to-black transition-all",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",