Merge pull request #90 from Dhravya/editor

canvas (3/3)
This commit is contained in:
CodeTorso 2024-07-01 06:10:13 +05:30 committed by GitHub
commit 38565f2ec4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 397 additions and 73 deletions

View file

@ -6,10 +6,9 @@ import ThinkPads from "./thinkPads";
async function page() {
const canvas = await getCanvas();
return (
<div className="h-screen w-full bg-[#171B1F] py-32 text-[#FFFFFF] ">
<div className="h-screen w-full py-32 text-[#FFFFFF] ">
<div className="flex w-full flex-col items-center gap-8">
<h1 className="text-4xl font-medium">Your thinkpads</h1>
<p>{JSON.stringify(canvas)}</p>
<SearchandCreate />
{
// @ts-ignore

View file

@ -1,9 +1,10 @@
"use client"
"use client";
import { useFormStatus } from "react-dom";
import Image from "next/image";
import { SearchIcon } from "@repo/ui/icons";
import { createCanvas } from "@/app/actions/doers";
import { toast } from "sonner";
export default function SearchandCreate() {
return (
@ -18,18 +19,27 @@ export default function SearchandCreate() {
</button>
</div>
<form action={createCanvas}>
<Button />
<form
action={async () => {
const res = await createCanvas();
if (!res.success){
toast.warning(res.message, {
style: {backgroundColor: "rgb(22 31 42 / 0.3)"}
});
}
}}
>
<Button />
</form>
</div>
);
}
function Button() {
const {pending} = useFormStatus()
const { pending } = useFormStatus();
return (
<button className="rounded-xl bg-[#1F2428] px-5 py-3 text-xl text-[#B8C4C6]">
{pending? "Creating.." : "Create New"}
{pending ? "Creating.." : "Create New"}
</button>
);
}
}

View file

@ -1,5 +1,13 @@
import {motion} from "framer-motion"
import { getCanvasData } from "@/app/actions/fetchers";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import {
EllipsisHorizontalCircleIcon,
TrashIcon,
PencilSquareIcon,
} from "@heroicons/react/24/outline";
import { toast } from "sonner";
import { Label } from "@repo/ui/shadcn/label";
const childVariants = {
hidden: { opacity: 0, y: 10, filter: "blur(2px)" },
@ -10,27 +18,259 @@ export default function ThinkPad({
title,
description,
image,
id
id,
}: {
title: string;
description: string;
image: string;
id: string;
}) {
const [deleted, setDeleted] = useState(false);
const [info, setInfo] = useState({ title, description });
return (
<motion.div
variants={childVariants}
className="flex h-48 gap-4 rounded-2xl bg-[#1F2428] p-2"
>
<Link className="h-full min-w-[40%] rounded-xl bg-[#363f46]" href={`/canvas/${id}`}>
<div></div>
</Link>
<div className="flex flex-col gap-2">
<div>{title}</div>
<div className="overflow-hidden text-ellipsis text-[#B8C4C6]">
{description}
</div>
</div>
</motion.div>
<AnimatePresence mode="sync">
{!deleted && (
<motion.div
layout
exit={{ opacity: 0, scaleY: 0 }}
variants={childVariants}
className="flex h-48 origin-top relative gap-4 rounded-2xl bg-[#1F2428] p-2"
>
<Link
className="h-full select-none min-w-[40%] bg-[#363f46] rounded-xl overflow-hidden"
href={`/canvas/${id}`}
>
<Suspense
fallback={
<div className=" h-full w-full flex justify-center items-center">
Loading...
</div>
}
>
<ImageComponent id={id} />
</Suspense>
</Link>
<div className="flex flex-col gap-2">
<motion.h2
initial={{ opacity: 0, filter: "blur(3px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
key={info.title}
>
{info.title}
</motion.h2>
<motion.h3
key={info.description}
initial={{ opacity: 0, filter: "blur(3px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
className="overflow-hidden text-ellipsis text-[#B8C4C6]"
>
{info.description}
</motion.h3>
</div>
<Menu
info={info}
id={id}
setDeleted={() => setDeleted(true)}
setInfo={(e) => setInfo(e)}
/>
</motion.div>
)}
</AnimatePresence>
);
}
}
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/ui/shadcn/popover";
function Menu({
info,
id,
setDeleted,
setInfo,
}: {
info: { title: string; description: string };
id: string;
setDeleted: () => void;
setInfo: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
}) {
return (
<Popover>
<PopoverTrigger className="absolute z-20 top-0 right-0" asChild>
<Button variant="secondary">
<EllipsisHorizontalCircleIcon className="size-5 stroke-2 stroke-[#B8C4C6]" />
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-32 px-2 py-2 bg-[#161f2a]/30 text-[#B8C4C6] border-border flex flex-col gap-3"
>
<EditToolbar info={info} id={id} setInfo={setInfo} />
<Button
onClick={async () => {
const res = await deleteCanvas(id);
if (res.success) {
toast.success("Thinkpad removed.", {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
setDeleted();
} else {
toast.warning("Something went wrong.", {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
}
}}
className="flex gap-2 border-border"
variant="outline"
>
<TrashIcon className="size-8 stroke-1" /> Delete
</Button>
</PopoverContent>
</Popover>
);
}
function EditToolbar({
id,
setInfo,
info
}: {
id: string;
setInfo: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
info: {
title: string;
description: string;
}
}) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="flex gap-2 border-border" variant="outline">
<PencilSquareIcon className="size-8 stroke-1" /> Edit
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] bg-[#161f2a]/30 border-0">
<form
action={async (FormData) => {
const data = {
title: FormData.get("title") as string,
description: FormData.get("description") as string,
};
const res = await AddCanvasInfo({ id, ...data });
if (res.success) {
setOpen(false);
setInfo(data);
} else {
setOpen(false);
toast.error("Something went wrong.", {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
}
}}
>
<DialogHeader>
<DialogTitle>Edit Canvas</DialogTitle>
<DialogDescription>
Add Description to your canvas. Pro tip: Let AI do the job, as you
add your content into canvas, we will autogenerate your
description.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
Title
</Label>
<Input
defaultValue={info.title}
name="title"
id="title"
placeholder="life planning..."
className="col-span-3 border-0"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea
defaultValue={info.description}
rows={6}
id="description"
name="description"
placeholder="contains information about..."
className="col-span-3 border-0 resize-none"
/>
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
import { Suspense, memo, use, useState } from "react";
import { Box, TldrawImage } from "tldraw";
import { Button } from "@repo/ui/shadcn/button";
import { AddCanvasInfo, deleteCanvas } from "@/app/actions/doers";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@repo/ui/shadcn/dialog";
import { Input } from "@repo/ui/shadcn/input";
import { Textarea } from "@repo/ui/shadcn/textarea";
import { textCardUtil } from "@/components/canvas/textCard";
import { twitterCardUtil } from "@/components/canvas/twitterCard";
const ImageComponent = memo(({ id }: { id: string }) => {
const snapshot = use(getCanvasData(id));
if (snapshot.bounds) {
const pageBounds = new Box(
snapshot.bounds.x,
snapshot.bounds.y,
snapshot.bounds.w,
snapshot.bounds.h
);
return (
<TldrawImage
shapeUtils={[twitterCardUtil, textCardUtil]}
snapshot={snapshot.snapshot}
background={false}
darkMode={true}
bounds={pageBounds}
padding={0}
scale={1}
format="png"
/>
);
}
return (
<div className=" h-full w-full flex justify-center items-center">
Drew things to seee here
</div>
);
});

View file

@ -1,5 +1,5 @@
.tl-background {
background: #1F2428 !important;
background: #181E23 !important;
}
.tlui-style-panel.tlui-style-panel__wrapper, .tlui-navigation-panel::before ,.tlui-menu-zone, .tlui-toolbar__tools, .tlui-popover__content, .tlui-menu, .tlui-button__help, .tlui-help-menu, .tlui-dialog__content {

View file

@ -1,19 +1,30 @@
import { auth } from "@/server/auth";
import "./canvasStyles.css";
import { redirect } from "next/navigation";
import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid";
import { Toaster } from "@repo/ui/shadcn/sonner";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const info = await auth();
if (!info) {
return redirect("/signin");
}
return (
<>
<div className="relative flex justify-center z-40 pointer-events-none">
<div
className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 md:bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
<BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" />
<div>{children}</div>
<Toaster />
</>
);
}

View file

@ -391,6 +391,12 @@ export const createCanvas = async () => {
return { error: "Not authenticated", success: false };
}
const canvases = await db.select().from(canvas).where(eq(canvas.userId, data.user.id))
if (canvases.length >= 5){
return {success: false, message: "A user currently can only have 5 canvases"}
}
const resp = await db
.insert(canvas)
.values({ userId: data.user.id }).returning({id: canvas.id});
@ -428,4 +434,33 @@ export const SaveCanvas = async ({id, data}: {id: string, data: string}) => {
} catch (error) {
return {success: false, error, message:"An error occured while saving your canvas"}
}
}
export const deleteCanvas = async (id: string) => {
try {
await process.env.CANVAS_SNAPS.delete(id)
await db.delete(canvas).where(eq(canvas.id,id))
return {
success: true,
message: "in-sync"
}
} catch (error) {
return {success: false, error, message:"An error occured while saving your canvas"}
}
}
export async function AddCanvasInfo({id, title, description}: {id: string, title: string, description: string}){
try {
await db.update(canvas).set({description, title}).where(eq(canvas.id, id))
return {
success: true,
message: "info updated successfully"
}
} catch (error) {
return {
success: false,
message: "something went wrong :/"
}
}
}

View file

@ -251,7 +251,7 @@ export const getCanvasData = async (canvasId: string) => {
if (canvas){
return JSON.parse(canvas);
} else {
return {}
return {snapshot: {}}
}
}

View file

@ -0,0 +1,27 @@
import type { NextRequest } from "next/server";
import { ensureAuth } from "../ensureAuth";
export const runtime = "edge";
export async function POST(request: NextRequest) {
const session = await ensureAuth(request);
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
const res : {query: string} = await request.json()
try {
const resp = await fetch(`${process.env.BACKEND_BASE_URL}/api/search?query=${res.query}&user=${session.user.id}`);
if (resp.status !== 200 || !resp.ok) {
const errorData = await resp.text();
console.log(errorData);
return new Response(
JSON.stringify({ message: "Error in CF function", error: errorData }),
{ status: resp.status },
);
}
return new Response(JSON.stringify({response:await resp.json(), status: 200 }));
} catch (error) {
return new Response(`Error, ${error}`)
}
}

View file

@ -1,25 +1,19 @@
import Image from "next/image";
import { useRef, useState } from "react";
interface DraggableComponentsProps {
content: string;
extraInfo?: string;
iconAlt: string;
}
import {motion} from "framer-motion"
export default function DraggableComponentsContainer({
content,
}: {
content: DraggableComponentsProps[];
content: {context:string}[] | undefined;
}) {
if (content === undefined) return null;
return (
<div className="flex flex-col gap-10">
{content.map((i) => {
return (
<DraggableComponents
content={i.content}
iconAlt={i.iconAlt}
extraInfo={i.extraInfo}
content={i.context}
/>
);
})}
@ -29,9 +23,7 @@ export default function DraggableComponentsContainer({
function DraggableComponents({
content,
extraInfo,
iconAlt,
}: DraggableComponentsProps) {
}: {content: string}) {
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
@ -49,19 +41,21 @@ function DraggableComponents({
};
return (
<div
<motion.div
initial={{opacity: 0, y: 5}}
animate={{opacity: 1, y: 0}}
ref={containerRef}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
draggable
className={`flex gap-4 px-1 rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
className={`flex gap-4 px-3 overflow-hidden rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
>
<div className="flex flex-col gap-2">
<div>
<h1 className="line-clamp-3">{content}</h1>
</div>
<p className="line-clamp-1 text-[#369DFD]">{extraInfo}</p>
{/* <p className="line-clamp-1 text-[#369DFD]">{extraInfo}</p> */}
</div>
</div>
</motion.div>
);
}

View file

@ -13,12 +13,18 @@ interface RectContextType {
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
visible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
id: string
id: string;
}
const RectContext = createContext<RectContextType | undefined>(undefined);
export const RectProvider = ({ id, children }: {id: string, children: React.ReactNode}) => {
export const RectProvider = ({
id,
children,
}: {
id: string;
children: React.ReactNode;
}) => {
const [fullScreen, setFullScreen] = useState(false);
const [visible, setVisible] = useState(true);
@ -36,12 +42,11 @@ export const RectProvider = ({ id, children }: {id: string, children: React.Reac
export const useRect = () => {
const context = useContext(RectContext);
if (context === undefined) {
throw new Error('useRect must be used within a RectProvider');
throw new Error("useRect must be used within a RectProvider");
}
return context;
};
export function ResizaleLayout() {
const { setVisible, fullScreen, setFullScreen } = useRect();
@ -82,7 +87,7 @@ export function ResizaleLayout() {
}
function DragIconContainer() {
const { fullScreen} = useRect();
const { fullScreen } = useRect();
return (
<div
className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`}
@ -93,7 +98,7 @@ function DragIconContainer() {
}
function CanvasContainer() {
const { fullScreen} = useRect();
const { fullScreen } = useRect();
return (
<div
className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`}
@ -104,7 +109,7 @@ function CanvasContainer() {
}
function SidePanelContainer() {
const { fullScreen, visible} = useRect();
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]`}
@ -123,35 +128,35 @@ function SidePanelContainer() {
}
function SidePanel() {
const [value, setValue] = useState("");
// const [dragAsText, setDragAsText] = useState(false);
const [content, setContent] = useState<{context: string}[]>()
return (
<>
<div className="px-3 py-5">
<input
placeholder="search..."
onChange={(e) => {
setValue(e.target.value);
<form
action={async (FormData) => {
const search = FormData.get("search");
console.log(search)
const res = await fetch("/api/canvasai", {
method: "POST",
body: JSON.stringify({ query: search }),
});
const t = await res.json()
console.log(t.response.response);
setContent(t.response.response)
}}
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>
>
<input
placeholder="search..."
name="search"
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"
/>
</form>
</div>
<DraggableComponentsContainer content={content} />
</>
);
}
const content = [
{
content:

View file

@ -10,7 +10,10 @@ export function SaveStatus({id}: {id:string}) {
const debouncedSave = useCallback(
debounce(async () => {
const snapshot = getSnapshot(editor.store)
SaveCanvas({id, data: JSON.stringify(snapshot)})
const bounds = editor.getViewportPageBounds()
console.log(bounds)
SaveCanvas({id, data: JSON.stringify({snapshot, bounds})})
setSave("saved!");
}, 3000),

View file

@ -9,6 +9,6 @@ export async function loadRemoteSnapshot(id:string) {
const newStore = createTLStore({
shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil],
});
loadSnapshot(newStore, snapshot);
loadSnapshot(newStore, snapshot.snapshot);
return newStore;
}