mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-12 23:01:07 +00:00
user canvas (2/2)
This commit is contained in:
parent
3143baac87
commit
ffe9103da6
19 changed files with 271 additions and 160 deletions
|
|
@ -1,125 +1,18 @@
|
|||
"use client";
|
||||
|
||||
import { Canvas } from "@repo/ui/components/canvas/components/canvas";
|
||||
import React, { useState } from "react";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { SettingsIcon, DragIcon } from "@repo/ui/icons";
|
||||
import DraggableComponentsContainer from "@repo/ui/components/canvas/components/draggableComponent";
|
||||
import { AutocompleteIcon, blockIcon } from "@repo/ui/icons";
|
||||
import Image from "next/image";
|
||||
import { Switch } from "@repo/ui/shadcn/switch";
|
||||
import { Label } from "@repo/ui/shadcn/label";
|
||||
|
||||
function page() {
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
const [visible, setVisible] = useState(true);
|
||||
import { userHasCanvas } from "@/app/actions/fetchers";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
RectProvider,
|
||||
ResizaleLayout,
|
||||
} from "../_canvas_comp/(components)/resizableLayout";
|
||||
|
||||
export default async function page({ params }: any) {
|
||||
const canvasExists = await userHasCanvas(params.id);
|
||||
if (!canvasExists.success) {
|
||||
redirect("/canvas");
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`h-screen w-full ${!fullScreen ? "px-4 py-6" : "bg-[#1F2428]"} transition-all`}
|
||||
>
|
||||
<div>
|
||||
<PanelGroup
|
||||
onLayout={(l) => {
|
||||
l[0]! < 20 ? setVisible(false) : setVisible(true);
|
||||
}}
|
||||
className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`}
|
||||
direction="horizontal"
|
||||
>
|
||||
<Panel
|
||||
onExpand={() => {
|
||||
setTimeout(() => setFullScreen(false), 50);
|
||||
}}
|
||||
onCollapse={() => {
|
||||
setTimeout(() => setFullScreen(true), 50);
|
||||
}}
|
||||
defaultSize={30}
|
||||
collapsible={true}
|
||||
>
|
||||
<div
|
||||
className={`flex transition-all rounded-2xl ${fullScreen ? "h-screen" : "h-[calc(100vh-3rem)]"} w-full flex-col overflow-hidden bg-[#1F2428]`}
|
||||
>
|
||||
<div className="flex items-center justify-between bg-[#2C3439] px-4 py-2 text-lg font-medium text-[#989EA4]">
|
||||
Change Filters
|
||||
<Image src={SettingsIcon} alt="setting-icon" />
|
||||
</div>
|
||||
{visible ? (
|
||||
<SidePanel />
|
||||
) : (
|
||||
<h1 className="text-center py-10 text-xl">
|
||||
Need more space to show!
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle
|
||||
className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`}
|
||||
>
|
||||
<Image src={DragIcon} alt="drag-icon" />
|
||||
</div>
|
||||
</PanelResizeHandle>
|
||||
<Panel className="relative" defaultSize={70} minSize={60}>
|
||||
<div
|
||||
className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`}
|
||||
>
|
||||
<Canvas />
|
||||
</div>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
<RectProvider id={params.id}>
|
||||
<ResizaleLayout />
|
||||
</RectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SidePanel() {
|
||||
const [value, setValue] = useState("");
|
||||
const [dragAsText, setDragAsText] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className="px-3 py-5">
|
||||
<input
|
||||
placeholder="search..."
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
value={value}
|
||||
// rows={1}
|
||||
className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end px-3 py-4">
|
||||
<Switch
|
||||
className="bg-[#151515] data-[state=unchecked]:bg-red-400 data-[state=checked]:bg-blue-400"
|
||||
onCheckedChange={(e) => setDragAsText(e)}
|
||||
id="drag-text-mode"
|
||||
/>
|
||||
<Label htmlFor="drag-text-mode">Drag as Text</Label>
|
||||
</div>
|
||||
<DraggableComponentsContainer content={content} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default page;
|
||||
|
||||
const content = [
|
||||
{
|
||||
content:
|
||||
"Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.",
|
||||
icon: AutocompleteIcon,
|
||||
iconAlt: "Autocomplete",
|
||||
extraInfo:
|
||||
"Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)",
|
||||
icon: blockIcon,
|
||||
iconAlt: "Autocomplete",
|
||||
extraInfo:
|
||||
"Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { getAssetUrls } from "@tldraw/assets/selfHosted";
|
|||
import { memo } from "react";
|
||||
import DragContext from "../lib/context";
|
||||
import DropZone from "./dropComponent";
|
||||
import { useRect } from "./resizableLayout";
|
||||
// import "./canvas.css";
|
||||
|
||||
export const Canvas = memo(() => {
|
||||
|
|
@ -46,12 +47,13 @@ export const Canvas = memo(() => {
|
|||
});
|
||||
|
||||
const TldrawComponent = memo(() => {
|
||||
const { id } = useRect();
|
||||
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
|
||||
status: "loading",
|
||||
});
|
||||
useEffect(() => {
|
||||
const fetchStore = async () => {
|
||||
const store = await loadRemoteSnapshot();
|
||||
const store = await loadRemoteSnapshot(id);
|
||||
|
||||
setStoreWithStatus({
|
||||
store: store,
|
||||
|
|
@ -71,7 +73,7 @@ const TldrawComponent = memo(() => {
|
|||
});
|
||||
}, []);
|
||||
|
||||
setUserPreferences({ id: "supermemory" });
|
||||
setUserPreferences({ id: "supermemory",colorScheme: "dark" });
|
||||
|
||||
const assetUrls = getAssetUrls();
|
||||
return (
|
||||
|
|
@ -85,7 +87,7 @@ const TldrawComponent = memo(() => {
|
|||
onMount={handleMount}
|
||||
>
|
||||
<div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
|
||||
<SaveStatus />
|
||||
<SaveStatus id={id} />
|
||||
</div>
|
||||
<DropZone />
|
||||
</Tldraw>
|
||||
|
|
@ -4,7 +4,6 @@ import { useRef, useState } from "react";
|
|||
interface DraggableComponentsProps {
|
||||
content: string;
|
||||
extraInfo?: string;
|
||||
icon: string;
|
||||
iconAlt: string;
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +18,6 @@ export default function DraggableComponentsContainer({
|
|||
return (
|
||||
<DraggableComponents
|
||||
content={i.content}
|
||||
icon={i.icon}
|
||||
iconAlt={i.iconAlt}
|
||||
extraInfo={i.extraInfo}
|
||||
/>
|
||||
|
|
@ -32,7 +30,6 @@ export default function DraggableComponentsContainer({
|
|||
function DraggableComponents({
|
||||
content,
|
||||
extraInfo,
|
||||
icon,
|
||||
iconAlt,
|
||||
}: DraggableComponentsProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
|
@ -59,7 +56,6 @@ function DraggableComponents({
|
|||
draggable
|
||||
className={`flex gap-4 px-1 rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
|
||||
>
|
||||
<Image className="select-none" src={icon} alt={iconAlt} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<h1 className="line-clamp-3">{content}</h1>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
"use client";
|
||||
|
||||
import { Canvas } from "./canvas";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { SettingsIcon, DragIcon } from "@repo/ui/icons";
|
||||
import DraggableComponentsContainer from "./draggableComponent";
|
||||
import Image from "next/image";
|
||||
import { Label } from "@repo/ui/shadcn/label";
|
||||
|
||||
interface RectContextType {
|
||||
fullScreen: boolean;
|
||||
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
visible: boolean;
|
||||
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
id: string
|
||||
}
|
||||
|
||||
const RectContext = createContext<RectContextType | undefined>(undefined);
|
||||
|
||||
export const RectProvider = ({ id, children }: {id: string, children: React.ReactNode}) => {
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
const value = {
|
||||
id,
|
||||
fullScreen,
|
||||
setFullScreen,
|
||||
visible,
|
||||
setVisible,
|
||||
};
|
||||
|
||||
return <RectContext.Provider value={value}>{children}</RectContext.Provider>;
|
||||
};
|
||||
|
||||
export const useRect = () => {
|
||||
const context = useContext(RectContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useRect must be used within a RectProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
|
||||
export function ResizaleLayout() {
|
||||
const { setVisible, fullScreen, setFullScreen } = useRect();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-screen w-full ${!fullScreen ? "px-4 py-6" : "bg-[#1F2428]"} transition-all`}
|
||||
>
|
||||
<PanelGroup
|
||||
onLayout={(l) => {
|
||||
l[0]! < 20 ? setVisible(false) : setVisible(true);
|
||||
}}
|
||||
className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`}
|
||||
direction="horizontal"
|
||||
>
|
||||
<Panel
|
||||
onExpand={() => {
|
||||
setTimeout(() => setFullScreen(false), 50);
|
||||
}}
|
||||
onCollapse={() => {
|
||||
setTimeout(() => setFullScreen(true), 50);
|
||||
}}
|
||||
defaultSize={30}
|
||||
collapsible={true}
|
||||
>
|
||||
<SidePanelContainer />
|
||||
</Panel>
|
||||
<PanelResizeHandle
|
||||
className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`}
|
||||
>
|
||||
<DragIconContainer />
|
||||
</PanelResizeHandle>
|
||||
<Panel className="relative" defaultSize={70} minSize={60}>
|
||||
<CanvasContainer />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DragIconContainer() {
|
||||
const { fullScreen} = useRect();
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`}
|
||||
>
|
||||
<Image src={DragIcon} alt="drag-icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CanvasContainer() {
|
||||
const { fullScreen} = useRect();
|
||||
return (
|
||||
<div
|
||||
className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`}
|
||||
>
|
||||
<Canvas />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidePanelContainer() {
|
||||
const { fullScreen, visible} = useRect();
|
||||
return (
|
||||
<div
|
||||
className={`flex transition-all rounded-2xl ${fullScreen ? "h-screen" : "h-[calc(100vh-3rem)]"} w-full flex-col overflow-hidden bg-[#1F2428]`}
|
||||
>
|
||||
<div className="flex items-center justify-between bg-[#2C3439] px-4 py-2 text-lg font-medium text-[#989EA4]">
|
||||
Change Filters
|
||||
<Image src={SettingsIcon} alt="setting-icon" />
|
||||
</div>
|
||||
{visible ? (
|
||||
<SidePanel />
|
||||
) : (
|
||||
<h1 className="text-center py-10 text-xl">Need more space to show!</h1>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidePanel() {
|
||||
const [value, setValue] = useState("");
|
||||
// const [dragAsText, setDragAsText] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className="px-3 py-5">
|
||||
<input
|
||||
placeholder="search..."
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
value={value}
|
||||
// rows={1}
|
||||
className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end px-3 py-4">
|
||||
{/* <Switch
|
||||
className="bg-[#151515] data-[state=unchecked]:bg-red-400 data-[state=checked]:bg-blue-400"
|
||||
onCheckedChange={(e) => setDragAsText(e)}
|
||||
id="drag-text-mode"
|
||||
/> */}
|
||||
<Label htmlFor="drag-text-mode">Drag as Text</Label>
|
||||
</div>
|
||||
<DraggableComponentsContainer content={content} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const content = [
|
||||
{
|
||||
content:
|
||||
"Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.",
|
||||
iconAlt: "Autocomplete",
|
||||
extraInfo:
|
||||
"Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)",
|
||||
iconAlt: "Autocomplete",
|
||||
extraInfo:
|
||||
"Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia",
|
||||
},
|
||||
];
|
||||
|
|
@ -1,27 +1,17 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { debounce, useEditor } from "tldraw";
|
||||
import { debounce, getSnapshot, useEditor } from "tldraw";
|
||||
import { SaveCanvas } from "@/app/actions/doers";
|
||||
|
||||
export function SaveStatus() {
|
||||
export function SaveStatus({id}: {id:string}) {
|
||||
const [save, setSave] = useState("saved!");
|
||||
const editor = useEditor();
|
||||
|
||||
|
||||
const debouncedSave = useCallback(
|
||||
debounce(async () => {
|
||||
const snapshot = editor.store.getSnapshot();
|
||||
localStorage.setItem("saved", JSON.stringify(snapshot));
|
||||
|
||||
const res = await fetch(
|
||||
"https://learning-cf.pruthvirajthinks.workers.dev/post/page3",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
data: snapshot,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
console.log(await res.json());
|
||||
const snapshot = getSnapshot(editor.store)
|
||||
SaveCanvas({id, data: JSON.stringify(snapshot)})
|
||||
|
||||
setSave("saved!");
|
||||
}, 3000),
|
||||
[editor], // Dependency array ensures the function is not recreated on every render
|
||||
13
apps/web/app/(canvas)/canvas/_canvas_comp/lib/loadSnap.ts
Normal file
13
apps/web/app/(canvas)/canvas/_canvas_comp/lib/loadSnap.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw";
|
||||
import { twitterCardUtil } from "../(components)/twitterCard";
|
||||
import { textCardUtil } from "../(components)/textCard";
|
||||
import { getCanvasData } from "@/app/actions/fetchers";
|
||||
export async function loadRemoteSnapshot(id:string) {
|
||||
const snapshot = await getCanvasData(id);
|
||||
|
||||
const newStore = createTLStore({
|
||||
shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil],
|
||||
});
|
||||
loadSnapshot(newStore, snapshot);
|
||||
return newStore;
|
||||
}
|
||||
|
|
@ -415,4 +415,17 @@ export const createCanvas = async () => {
|
|||
// };
|
||||
// }
|
||||
// }
|
||||
};
|
||||
};
|
||||
|
||||
export const SaveCanvas = async ({id, data}: {id: string, data: string}) => {
|
||||
console.log({id, data})
|
||||
try {
|
||||
await process.env.CANVAS_SNAPS.put(id, data)
|
||||
return {
|
||||
success: true,
|
||||
message: "in-sync"
|
||||
}
|
||||
} catch (error) {
|
||||
return {success: false, error, message:"An error occured while saving your canvas"}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,3 +213,46 @@ export const getCanvas = async () => {
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const userHasCanvas = async (canvasId: string) => {
|
||||
const data = await auth();
|
||||
|
||||
if (!data || !data.user || !data.user.id) {
|
||||
redirect("/signin");
|
||||
return { error: "Not authenticated", success: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const canvases = await db.select().from(canvas).where(eq(canvas.userId, data.user.id))
|
||||
const exists = !!canvases.find(canvas => canvas.id === canvasId);
|
||||
return {
|
||||
success: exists,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
error: (e as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getCanvasData = async (canvasId: string) => {
|
||||
|
||||
const data = await auth();
|
||||
|
||||
if (!data || !data.user || !data.user.id) {
|
||||
redirect("/signin");
|
||||
return { error: "Not authenticated", success: false };
|
||||
}
|
||||
|
||||
const canvas = await process.env.CANVAS_SNAPS.get(canvasId)
|
||||
|
||||
console.log({canvas, canvasId})
|
||||
if (canvas){
|
||||
return JSON.parse(canvas);
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
1
apps/web/env.d.ts
vendored
1
apps/web/env.d.ts
vendored
|
|
@ -5,4 +5,5 @@ interface CloudflareEnv {
|
|||
STORAGE: R2Bucket;
|
||||
DATABASE: D1Database;
|
||||
DEV_IMAGES: R2Bucket;
|
||||
CANVAS_SNAPS: KVNamespace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ binding = "DATABASE"
|
|||
database_name = "dev-d1-anycontext"
|
||||
database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "CANVAS_SNAPS"
|
||||
id = "c6446f7190dd4afebe1c318df3400518"
|
||||
|
||||
[[env.production.d1_databases]]
|
||||
binding = "DATABASE"
|
||||
database_name = "prod-d1-supermemory"
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw";
|
||||
import { twitterCardUtil } from "../components/twitterCard";
|
||||
import { textCardUtil } from "../components/textCard";
|
||||
export async function loadRemoteSnapshot() {
|
||||
const res = await fetch(
|
||||
"https://learning-cf.pruthvirajthinks.workers.dev/get/page3",
|
||||
);
|
||||
const snapshot = JSON.parse(await res.json());
|
||||
const newStore = createTLStore({
|
||||
shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil],
|
||||
});
|
||||
loadSnapshot(newStore, snapshot);
|
||||
return newStore;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue