This commit is contained in:
codetorso 2024-07-25 23:34:45 +05:30
parent 3c91c188a4
commit 5def889576
8 changed files with 236 additions and 192 deletions

View file

@ -6,6 +6,7 @@ import Sidepanel from "./sidepanel";
import Image from "next/image";
import { DragIcon } from "@repo/ui/icons";
import { useRef, useState } from "react";
import { ChevronRight } from "lucide-react";
export default function ResizableLayout({ id }: { id: string }) {
const panelGroupRef = useRef(null);
@ -19,7 +20,7 @@ export default function ResizableLayout({ id }: { id: string }) {
return (
<PanelGroup
className="text-white h-screen py-[1vh] px-[1vh] bg-[#111417]"
className="text-white h-screen py-[1vh] px-[1vh] bg-[#171B1F]"
direction="horizontal"
ref={panelGroupRef}
onLayout={(sizes) => {
@ -31,9 +32,9 @@ export default function ResizableLayout({ id }: { id: string }) {
</Panel>
<PanelResizeHandle onClick={handleResize} className="w-4 h-[98vh] relative">
<div
className={`rounded-lg bg-[#2F363B] absolute top-1/2 -translate-y-1/2 px-1 transition-all py-2`}
className={`rounded-lg bg-[#2F363B] absolute top-1/2 z-[1000000] -translate-y-1/2 transition-all py-2 ${!isLeftPanelCollapsed && "px-1"}`}
>
<Image src={DragIcon} alt="drag-icon" />
{isLeftPanelCollapsed ? <ChevronRight className="text-[#989EA4] h-6 w-4 cursor-pointer" /> : <Image src={DragIcon} alt="drag-icon" />}
</div>
</PanelResizeHandle>
<Panel defaultSize={70} minSize={60}>

View file

@ -4,6 +4,7 @@ import Link from "next/link";
import Card from "./sidepanelcard";
import { sourcesZod } from "@repo/shared-types";
import { toast } from "sonner";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
type card = {
title: string;
@ -17,7 +18,7 @@ function Sidepanel() {
const [content, setContent] = useState<card[]>([]);
return (
<div className="h-[98vh] bg-[#1f2428] rounded-xl overflow-hidden">
<div className="flex justify-between bg-[#2C3439] items-center py-2 px-4 mb-2 text-lg">
<div className="flex bg-[#2C3439] items-center py-2 px-4 mb-2 text-lg">
<Link
href="/thinkpad"
className="p-2 px-4 transition-colors rounded-lg hover:bg-[#334044] flex items-center gap-2"
@ -25,10 +26,6 @@ function Sidepanel() {
<ArrowLeftIcon className="h-5 w-5" />
Back
</Link>
<div className="p-2 px-4 transition-colors rounded-lg hover:bg-[#334044] flex items-center gap-2">
<Cog6ToothIcon className="h-5 w-5" />
Options
</div>
</div>
<div className="h-full px-2">
<div className=" p-2 h-full">
@ -47,6 +44,7 @@ function Sidepanel() {
function Search({ setContent }: { setContent: (e: any) => void }) {
return (
<form
className="flex justify-between items-center rounded-md bg-[#121718]"
action={async (FormData) => {
const search = FormData.get("search") as string;
@ -91,9 +89,12 @@ function Search({ setContent }: { setContent: (e: any) => void }) {
<input
name="search"
placeholder="search memories..."
className="rounded-md w-full bg-[#121718] p-3 text-lg outline-none"
className="w-full bg-transparent p-3 text-lg outline-none flex-grow"
type="text"
/>
<button type="submit" className=" border-l-2 border-gray-600 h-full p-3">
<MagnifyingGlassIcon className=" h-5 text-[#fff]" />
</button>
</form>
);
}

View file

@ -3,6 +3,7 @@ import { TwitterIcon, TypeIcon } from "lucide-react";
import { useState } from "react";
import { motion } from "framer-motion";
import useMeasure from "react-use-measure";
type CardType = string;
export default function Card({
@ -17,15 +18,32 @@ export default function Card({
source?: string;
}) {
const [ref, bounds] = useMeasure();
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
const handleDragStart = (event: React.DragEvent<HTMLDivElement>, dragSource: 'icon' | 'link' | 'parent') => {
setIsDragging(true);
event.dataTransfer.setData(
"application/json",
JSON.stringify({ type, title, content, source })
);
switch(dragSource) {
case 'icon':
event.dataTransfer.setData("application/json", JSON.stringify({
type,
title,
content,
text: true
}));
break;
case 'link':
event.dataTransfer.setData("text/plain", source || '');
break;
case 'parent':
event.dataTransfer.setData("application/json", JSON.stringify({
type,
title,
content,
text: false
}));
break;
}
};
const handleDragEnd = () => {
@ -37,13 +55,21 @@ export default function Card({
<div
draggable
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
onDragStart={(e) => handleDragStart(e, 'parent')}
className={`rounded-lg hover:scale-[1.02] group cursor-grab scale-[0.98] select-none transition-all border-[#232c2f] hover:bg-[#232c32] ${
isDragging ? "border-blue-600 border-dashed border-2" : ""
}`}
>
<div ref={ref} className="flex gap-4 px-3 py-2 items-center">
<a href={source} className={`${source && "cursor-pointer"}`}>
<a
href={source}
className={`${source && "cursor-pointer"}`}
draggable
onDragStart={(e) => {
e.stopPropagation();
handleDragStart(e, 'icon');
}}
>
<Icon type={type} />
</a>
<div>
@ -51,6 +77,17 @@ export default function Card({
<p className="group-hover:line-clamp-[12] transition-all line-clamp-3 text-gray-200">
{content}
</p>
<a
draggable
href={source}
className="line-clamp-[1] text-blue-500"
onDragStart={(e) => {
e.stopPropagation();
handleDragStart(e, 'link');
}}
>
{source}
</a>
</div>
</div>
</div>

View file

@ -12,9 +12,8 @@ import { textCardUtil } from "./custom_nodes/textcard";
import DropZone from "./tldrawDrop";
import { loadRemoteSnapshot } from "@/lib/loadSnap";
import { createAssetFromUrl } from "@/lib/createAssetUrl";
import createEmbedsFromUrl from "@/lib/ExternalDroppedContent";
import { getAssetUrls } from "@tldraw/assets/selfHosted";
import { SaveStatus } from "./savesnap";
import createEmbedsFromUrl from "@/lib/createEmbeds";
interface DragContextType {
isDraggingOver: boolean;

View file

@ -6,10 +6,10 @@ import { useEditor } from "tldraw";
import { DragContext } from "./tldrawComponent";
type CardData = {
type: any; // Adjust this according to your actual type
title: string;
type: string;
content: string;
url: string;
text: boolean;
};
function DropZone() {

View file

@ -1,103 +1,5 @@
import {
AssetRecordType,
Editor,
TLAsset,
TLAssetId,
TLBookmarkShape,
TLExternalContentSource,
TLShapePartial,
Vec,
VecLike,
createShapeId,
getEmbedInfo,
getHashForString,
} from "tldraw";
import { unfirlSite } from "@/app/actions/fetchers";
export default async function createEmbedsFromUrl({
url,
point,
sources,
editor,
}: {
url: string;
point?: VecLike | undefined;
sources?: TLExternalContentSource[] | undefined;
editor: Editor;
}) {
const position =
point ??
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center);
const urlPattern = /https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/;
if (urlPattern.test(url)) {
return editor.createShape({
type: "Twittercard",
x: position.x - 250,
y: position.y - 150,
props: { url: url },
});
}
// try to paste as an embed first
const embedInfo = getEmbedInfo(url);
if (embedInfo) {
return editor.putExternalContent({
type: "embed",
url: embedInfo.url,
point,
embed: embedInfo.definition,
});
}
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url));
const shape = createEmptyBookmarkShape(editor, url, position);
let asset = editor.getAsset(assetId) as TLAsset;
let shouldAlsoCreateAsset = false;
if (!asset) {
shouldAlsoCreateAsset = true;
try {
const bookmarkAsset = await editor.getAssetForExternalContent({
type: "url",
url,
});
const value = await unfirlSite(url);
if (bookmarkAsset) {
if (bookmarkAsset.type === "bookmark" ){
if (value.title ) bookmarkAsset.props.title = value.title;
if (value.image) bookmarkAsset.props.image = value.image;
if (value.description)
bookmarkAsset.props.description = value.description;
}
}
if (!bookmarkAsset) throw Error("Could not create an asset");
asset = bookmarkAsset;
} catch (e) {
console.log(e);
return;
}
}
editor.batch(() => {
if (shouldAlsoCreateAsset) {
editor.createAssets([asset]);
}
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: {
assetId: asset.id,
},
},
]);
});
}
import {Editor} from "tldraw";
import createEmbedsFromUrl from "./createEmbeds";
function processURL(input: string): string | null {
let str = input.trim();
@ -116,6 +18,19 @@ function processURL(input: string): string | null {
}
}
function formatContent(title: string, content: string): string {
const totalLength = title.length + content.length;
const totalHeight = Math.ceil(totalLength * (2 / 3));
const titleLines = Math.ceil(totalHeight * (2 / 5));
const contentLines = totalHeight - titleLines;
const titleWithNewLines = title + '\n'.repeat(titleLines);
const contentWithNewLines = content + '\n'.repeat(contentLines);
return `${titleWithNewLines.trim()}\n\n${contentWithNewLines.trim()}`;
}
function formatTextToRatio(text: string): { height: number; width: number } {
const RATIO = 4 / 3;
const FONT_SIZE = 15;
@ -162,9 +77,8 @@ function formatTextToRatio(text: string): { height: number; width: number } {
type CardData = {
title: string;
type: string;
source: string;
content: string;
numChunks: string;
text: boolean;
};
type DroppedData = CardData | string | { imageUrl: string };
@ -188,17 +102,20 @@ export function handleExternalDroppedContent({
createTextCard(editor, position, droppedData, "String Content");
}
} else if ("imageUrl" in droppedData) {
// Handle image URL (implementation not provided in original code)
} else {
const { content, title, source, type } = droppedData;
const processedURL = processURL(source);
// if (processedURL) {
// createEmbedsFromUrl({ editor, url: processedURL });
// } else {
// }
createTextCard(editor, position, title, type, content);
}
}
const { content, title, type, text } = droppedData;
if (text){
const { height, width } = formatTextToRatio(content);
editor.createShape({
type: 'text',
x: position.x - width / 2,
y: position.y - height / 2,
props: { text: formatContent(title, content) },
})
}else {
createTextCard(editor, position, title, type, content);
}
}}
function createTextCard(editor: Editor, position: { x: number; y: number }, content: string, type: string, extraInfo: string = "") {
const { height, width } = formatTextToRatio(content);
@ -209,62 +126,3 @@ function createTextCard(editor: Editor, position: { x: number; y: number }, cont
props: { content, type, extrainfo: extraInfo, h: 200, w: 400 },
});
}
function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds();
let selectionPageBounds = editor.getSelectionPageBounds();
if (selectionPageBounds) {
const offset = selectionPageBounds!.center.sub(position);
editor.updateShapes(
editor.getSelectedShapes().map((shape) => {
const localRotation = editor
.getShapeParentTransform(shape)
.decompose().rotation;
const localDelta = Vec.Rot(offset, -localRotation);
return {
id: shape.id,
type: shape.type,
x: shape.x! - localDelta.x,
y: shape.y! - localDelta.y,
};
})
);
}
// Zoom out to fit the shapes, if necessary
selectionPageBounds = editor.getSelectionPageBounds();
if (
selectionPageBounds &&
!viewportPageBounds.contains(selectionPageBounds)
) {
editor.zoomToSelection();
}
}
export function createEmptyBookmarkShape(
editor: Editor,
url: string,
position: VecLike
): TLBookmarkShape {
const partial: TLShapePartial = {
id: createShapeId(),
type: "bookmark",
x: position.x - 150,
y: position.y - 160,
opacity: 1,
props: {
assetId: null,
url,
},
};
editor.batch(() => {
editor.createShapes([partial]).select(partial.id);
centerSelectionAroundPoint(editor, position);
});
return editor.getShape(partial.id) as TLBookmarkShape;
}

View file

@ -0,0 +1,88 @@
import { AssetRecordType, Editor, TLAsset, TLAssetId, TLExternalContentSource, VecLike, getEmbedInfo, getHashForString } from "tldraw";
import { createEmptyBookmarkShape } from "./drophelpers";
import { unfirlSite } from "@/app/actions/fetchers";
export default async function createEmbedsFromUrl({
url,
point,
sources,
editor,
}: {
url: string;
point?: VecLike | undefined;
sources?: TLExternalContentSource[] | undefined;
editor: Editor;
}) {
const position =
point ??
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center);
const urlPattern = /https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/;
if (urlPattern.test(url)) {
return editor.createShape({
type: "Twittercard",
x: position.x - 250,
y: position.y - 150,
props: { url: url },
});
}
// try to paste as an embed first
const embedInfo = getEmbedInfo(url);
if (embedInfo) {
return editor.putExternalContent({
type: "embed",
url: embedInfo.url,
point,
embed: embedInfo.definition,
});
}
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url));
const shape = createEmptyBookmarkShape(editor, url, position);
let asset = editor.getAsset(assetId) as TLAsset;
let shouldAlsoCreateAsset = false;
if (!asset) {
shouldAlsoCreateAsset = true;
try {
const bookmarkAsset = await editor.getAssetForExternalContent({
type: "url",
url,
});
const value = await unfirlSite(url);
if (bookmarkAsset) {
if (bookmarkAsset.type === "bookmark" ){
if (value.title ) bookmarkAsset.props.title = value.title;
if (value.image) bookmarkAsset.props.image = value.image;
if (value.description)
bookmarkAsset.props.description = value.description;
}
}
if (!bookmarkAsset) throw Error("Could not create an asset");
asset = bookmarkAsset;
} catch (e) {
console.log(e);
return;
}
}
editor.batch(() => {
if (shouldAlsoCreateAsset) {
editor.createAssets([asset]);
}
editor.updateShapes([
{
id: shape.id,
type: shape.type,
props: {
assetId: asset.id,
},
},
]);
});
}

View file

@ -0,0 +1,60 @@
import { Editor, TLBookmarkShape, TLShapePartial, Vec, VecLike, createShapeId } from "tldraw";
export function createEmptyBookmarkShape(
editor: Editor,
url: string,
position: VecLike
): TLBookmarkShape {
const partial: TLShapePartial = {
id: createShapeId(),
type: "bookmark",
x: position.x - 150,
y: position.y - 160,
opacity: 1,
props: {
assetId: null,
url,
},
};
editor.batch(() => {
editor.createShapes([partial]).select(partial.id);
centerSelectionAroundPoint(editor, position);
});
return editor.getShape(partial.id) as TLBookmarkShape;
}
function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds();
let selectionPageBounds = editor.getSelectionPageBounds();
if (selectionPageBounds) {
const offset = selectionPageBounds!.center.sub(position);
editor.updateShapes(
editor.getSelectedShapes().map((shape) => {
const localRotation = editor
.getShapeParentTransform(shape)
.decompose().rotation;
const localDelta = Vec.Rot(offset, -localRotation);
return {
id: shape.id,
type: shape.type,
x: shape.x! - localDelta.x,
y: shape.y! - localDelta.y,
};
})
);
}
// Zoom out to fit the shapes, if necessary
selectionPageBounds = editor.getSelectionPageBounds();
if (
selectionPageBounds &&
!viewportPageBounds.contains(selectionPageBounds)
) {
editor.zoomToSelection();
}
}