Skip to content

Commit

Permalink
✨first commit: a notion clone
Browse files Browse the repository at this point in the history
  • Loading branch information
lewislovelock committed Mar 23, 2024
1 parent ccbe373 commit 3f1fb2c
Show file tree
Hide file tree
Showing 78 changed files with 12,559 additions and 2,190 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# trunk
.trunk

# vscode
.vscode
71 changes: 71 additions & 0 deletions app/(main)/(routes)/documents/[documentId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import { useMutation, useQuery } from "convex/react";
import dynamic from "next/dynamic";
import { useMemo } from "react";

import { api } from "@/convex/_generated/api";
import { Id } from "@/convex/_generated/dataModel";
import { Toolbar } from "@/components/toolbar";
import { Cover } from "@/components/cover";
import { Skeleton } from "@/components/ui/skeleton";
import { useParams } from "next/navigation";

// interface DocumentIdPageProps {
// params: {
// documentId: Id<"documents">;
// };
// };

const DocumentIdPage = () => {
const Editor = useMemo(() => dynamic(() => import("@/components/editor"), { ssr: false }) ,[]);
const params = useParams();

const document = useQuery(api.documents.getById, {
documentId: params.documentId as Id<"documents">
});

const update = useMutation(api.documents.update);

const onChange = (content: string) => {
update({
id: params.documentId as Id<"documents">,
content
});
};

if (document === undefined) {
return (
<div>
<Cover.Skeleton />
<div className="md:max-w-3xl lg:max-w-4xl mx-auto mt-10">
<div className="space-y-4 pl-8 pt-4">
<Skeleton className="h-14 w-[50%]" />
<Skeleton className="h-4 w-[80%]" />
<Skeleton className="h-4 w-[40%]" />
<Skeleton className="h-4 w-[60%]" />
</div>
</div>
</div>
);
}

if (document === null) {
return <div>Not found</div>
}

return (
<div className="pb-40">
<Cover url={document.coverImage} />
<div className="md:max-w-3xl lg:max-w-4xl mx-auto">
<Toolbar initialData={document} />
<Editor
onChange={onChange}
initialContent={document.content}
/>
</div>
</div>
);
}

export default DocumentIdPage;
60 changes: 60 additions & 0 deletions app/(main)/(routes)/documents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import Image from "next/image";
import { useUser } from "@clerk/clerk-react";
import { PlusCircle } from "lucide-react";
import { useMutation } from "convex/react";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

import { api } from "@/convex/_generated/api";
import { Button } from "@/components/ui/button";

const DocumentsPage = () => {
const router = useRouter();
const { user } = useUser();
const create = useMutation(api.documents.create);

const onCreate = () => {
const promise = create({ title: "Untitled" })
.then((result) => {
if (!result) {
throw new Error("Failed to create document");
}
router.push(`/documents/${result}`);
})
toast.promise(promise, {
loading: "Creating a new note...",
success: "New note created!",
error: "Failed to create a new note."
});
};

return (
<div className="h-full flex flex-col items-center justify-center space-y-4">
<Image
src="/empty.png"
height="300"
width="300"
alt="Empty"
className="dark:hidden"
/>
<Image
src="/empty-dark.png"
height="300"
width="300"
alt="Empty"
className="hidden dark:block"
/>
<h2 className="text-lg font-medium">
Welcome to {user?.firstName}&apos;s Jotion
</h2>
<Button onClick={onCreate}>
<PlusCircle className="h-4 w-4 mr-2" />
Create a note
</Button>
</div>
);
}

export default DocumentsPage;
70 changes: 70 additions & 0 deletions app/(main)/_components/banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { useRouter } from "next/navigation";
import { useMutation } from "convex/react";
import { toast } from "sonner";

import { Id } from "@/convex/_generated/dataModel";
import { api } from "@/convex/_generated/api";
import { Button } from "@/components/ui/button";
import { ConfirmModal } from "@/components/modals/confirm-modal";

interface BannerProps {
documentId: Id<"documents">;
};

export const Banner = ({
documentId
}: BannerProps) => {
const router = useRouter();

const remove = useMutation(api.documents.remove);
const restore = useMutation(api.documents.restore);

const onRemove = () => {
const promise = remove({ id: documentId });

toast.promise(promise, {
loading: "Deleting note...",
success: "Note deleted!",
error: "Failed to delete note."
});

router.push("/documents");
};

const onRestore = () => {
const promise = restore({ id: documentId });

toast.promise(promise, {
loading: "Restoring note...",
success: "Note restored!",
error: "Failed to restore note."
});
};

return (
<div className="w-full bg-rose-500 text-center text-sm p-2 text-white flex items-center gap-x-2 justify-center">
<p>
This page is in the Trash.
</p>
<Button
size="sm"
onClick={onRestore}
variant="outline"
className="border-white bg-transparent hover:bg-primary/5 text-white hover:text-white p-1 px-2 h-auto font-normal"
>
Restore page
</Button>
<ConfirmModal onConfirm={onRemove}>
<Button
size="sm"
variant="outline"
className="border-white bg-transparent hover:bg-primary/5 text-white hover:text-white p-1 px-2 h-auto font-normal"
>
Delete forever
</Button>
</ConfirmModal>
</div>
)
}
91 changes: 91 additions & 0 deletions app/(main)/_components/document-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";

import { api } from "@/convex/_generated/api";
import { Doc, Id } from "@/convex/_generated/dataModel";
import { useQuery } from "convex/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { Item } from "./item";
import { cn } from "@/lib/utils";
import { FileIcon } from "lucide-react";

interface DocumentListProps {
parentDocumentId?: Id<"documents">;
level?: number;
data?: Doc<"documents">[];
}

export const DocumentList = (
{ parentDocumentId, level = 0 }: DocumentListProps
) => {
const params = useParams();
const router = useRouter();
const [expanded, setExpanded] = useState<Record<string, boolean>>({});

const onExpand = (documentId: string) => {
setExpanded(prev => ({
...prev,
[documentId]: !prev[documentId]
}))
}

const documents = useQuery(api.documents.getSidebar, {
parentDocument: parentDocumentId
});

const onRedirect = (documentId: string) => {
router.push(`/documents/${documentId}`);
};

if (documents === undefined) {
return (
<>
<Item.Skeleton level={level} />
{level === 0 && (
<>
<Item.Skeleton level={level} />
<Item.Skeleton level={level} />
</>
)}
</>
)
}

return (
<>
<p
style={{
paddingLeft: level ? `${(level * 12) + 25}px` : "12px"
}}
className={cn(
"hidden text-sm text-muted-foreground/80 font-medium",
expanded && "last:block",
level === 0 && "hidden"
)}
>
No pages inside
</p>
{documents.map((document) => (
<div key={document._id}>
<Item
id={document._id}
onClick={() => onRedirect(document._id)}
label={document.title}
icon={FileIcon}
documentIcon={document.icon}
active={params.documentId === document._id}
level={level}
onExpand={() => onExpand(document._id)}
expanded={expanded[document._id]}
/>
{expanded[document._id] && (
<DocumentList
parentDocumentId={document._id}
level={level + 1}
/>
)}
</div>
))}
</>
)
}
Loading

0 comments on commit 3f1fb2c

Please sign in to comment.