Skip to content

Commit

Permalink
feat
Browse files Browse the repository at this point in the history
  • Loading branch information
[email protected] authored and [email protected] committed May 5, 2023
1 parent 42a4eda commit 16ba1e7
Show file tree
Hide file tree
Showing 11 changed files with 2,200 additions and 384 deletions.
2,181 changes: 1,860 additions & 321 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@
"@trpc/next": "^10.18.0",
"@trpc/react-query": "^10.18.0",
"@trpc/server": "^10.18.0",
"daisyui": "^2.51.6",
"next": "13.3.4",
"next-auth": "^4.21.0",
"react": "18.2.0",
"react-codemirror": "^1.0.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"superjson": "1.12.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/eslint": "^8.21.3",
"@types/node": "^18.15.5",
"@types/prettier": "^2.7.2",
Expand Down
21 changes: 21 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ model User {
image String?
accounts Account[]
sessions Session[]
topic Topic[]
}

model VerificationToken {
Expand All @@ -64,3 +65,23 @@ model VerificationToken {
@@unique([identifier, token])
}

model Topic {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
user User @relation(fields: [userId], references: [id])
userId String
notes Note[]
}

model Note {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String @db.Text
topic Topic @relation(fields: [topicId], references: [id])
topicId String
}
39 changes: 39 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable @next/next/no-img-element */
import { signIn, signOut, useSession } from "next-auth/react";

export const Header = () => {
const { data: sessionData } = useSession();

return (
<div className="navbar bg-primary text-primary-content">
<div className="flex-1 pl-5 text-3xl font-bold">
{sessionData?.user?.name ? `Notes for ${sessionData.user.name}` : ""}
</div>
<div className="flex-none gap-2">
<div className="dropdown-end dropdown">
{sessionData?.user ? (
<label
tabIndex={0}
className="btn-ghost btn-circle avatar btn"
onClick={() => void signOut()}
>
<div className="w-10 rounded-full">
<img
src={sessionData?.user?.image ?? ""}
alt={sessionData?.user?.name ?? ""}
/>
</div>
</label>
) : (
<button
className="btn-ghost rounded-btn btn"
onClick={() => void signIn()}
>
Sign in
</button>
)}
</div>
</div>
</div>
);
};
41 changes: 41 additions & 0 deletions src/components/NoteCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from "react";

import ReactMarkdown from "react-markdown";

import { type RouterOutputs } from "../utils/api";

type Note = RouterOutputs["note"]["getAll"][0];

export const NoteCard = ({
note,
onDelete,
}: {
note: Note;
onDelete: () => void;
}) => {
const [isExpanded, setIsExpanded] = useState<boolean>(true);

return (
<div className="card mt-5 border border-gray-200 bg-base-100 shadow-xl">
<div className="card-body m-0 p-3">
<div
className={`collapse-arrow ${isExpanded ? "collapse-open" : ""
} collapse`}
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="collapse-title text-xl font-bold">{note.title}</div>
<div className="collapse-content">
<article className="prose lg:prose-xl">
<ReactMarkdown>{note.content}</ReactMarkdown>
</article>
</div>
</div>
<div className="card-actions mx-2 flex justify-end">
<button className="btn-warning btn-xs btn px-5" onClick={onDelete}>
Delete
</button>
</div>
</div>
</div>
);
};
58 changes: 58 additions & 0 deletions src/components/NoteEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from "react";

import CodeMirror from "@uiw/react-codemirror";
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { languages } from "@codemirror/language-data";

export const NoteEditor = ({
onSave,
}: {
onSave: (note: { title: string; content: string }) => void;
}) => {
const [code, setCode] = useState<string>("");
const [title, setTitle] = useState<string>("");

return (
<div className="card mt-5 border border-gray-200 bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">
<input
type="text"
placeholder="Note title"
className="input-primary input input-lg w-full font-bold"
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
/>
</h2>
<CodeMirror
value={code}
width="500px"
height="30vh"
minWidth="100%"
minHeight="30vh"
extensions={[
markdown({ base: markdownLanguage, codeLanguages: languages }),
]}
onChange={(value) => setCode(value)}
className="border border-gray-300"
/>
</div>
<div className="card-actions justify-end">
<button
onClick={() => {
onSave({
title,
content: code,
});
setCode("");
setTitle("");
}}
className="btn-primary btn"
disabled={title.trim().length === 0 || code.trim().length === 0}
>
Save
</button>
</div>
</div>
);
};
161 changes: 104 additions & 57 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,130 @@
import { useState } from "react";
import { type NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { signIn, signOut, useSession } from "next-auth/react";
import { useSession } from "next-auth/react";

import { api } from "~/utils/api";
import { api, type RouterOutputs } from "../utils/api";
import { Header } from "../components/Header";
import { NoteEditor } from "../components/NoteEditor";
import { NoteCard } from "../components/NoteCard";

const Home: NextPage = () => {
const hello = api.example.hello.useQuery({ text: "from tRPC" });

return (
<>
<Head>
<title>Create T3 App</title>
<title>Notetaker</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c]">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
<h1 className="text-5xl font-extrabold tracking-tight text-white sm:text-[5rem]">
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
</h1>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
href="https://create.t3.gg/en/usage/first-steps"
target="_blank"
>
<h3 className="text-2xl font-bold">First Steps →</h3>
<div className="text-lg">
Just the basics - Everything you need to know to set up your
database and authentication.
</div>
</Link>
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
href="https://create.t3.gg/en/introduction"
target="_blank"
>
<h3 className="text-2xl font-bold">Documentation →</h3>
<div className="text-lg">
Learn more about Create T3 App, the libraries it uses, and how
to deploy it.
</div>
</Link>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-2xl text-white">
{hello.data ? hello.data.greeting : "Loading tRPC query..."}
</p>
<AuthShowcase />
</div>
</div>
<main>
<Header />
<Content />
</main>
</>
);
};

export default Home;

const AuthShowcase: React.FC = () => {
type Topic = RouterOutputs["topic"]["getAll"][0];

const Content: React.FC = () => {
const { data: sessionData } = useSession();

const { data: secretMessage } = api.example.getSecretMessage.useQuery(
const [selectedTopic, setSelectedTopic] = useState<Topic | null>(null);

const { data: topics, refetch: refetchTopics } = api.topic.getAll.useQuery(
undefined, // no input
{ enabled: sessionData?.user !== undefined },
{
enabled: sessionData?.user !== undefined,
onSuccess: (data) => {
setSelectedTopic(selectedTopic ?? data[0] ?? null);
},
}
);

const createTopic = api.topic.create.useMutation({
onSuccess: () => {
void refetchTopics();
},
});

const { data: notes, refetch: refetchNotes } = api.note.getAll.useQuery(
{
topicId: selectedTopic?.id ?? "",
},
{
enabled: sessionData?.user !== undefined && selectedTopic !== null,
}
);

const createNote = api.note.create.useMutation({
onSuccess: () => {
void refetchNotes();
},
});

const deleteNote = api.note.delete.useMutation({
onSuccess: () => {
void refetchNotes();
},
});

return (
<div className="flex flex-col items-center justify-center gap-4">
<p className="text-center text-2xl text-white">
{sessionData && <span>Logged in as {sessionData.user?.name}</span>}
{secretMessage && <span> - {secretMessage}</span>}
</p>
<button
className="rounded-full bg-white/10 px-10 py-3 font-semibold text-white no-underline transition hover:bg-white/20"
onClick={sessionData ? () => void signOut() : () => void signIn()}
>
{sessionData ? "Sign out" : "Sign in"}
</button>
<div className="mx-5 mt-5 grid grid-cols-4 gap-2">
<div className="px-2">
<ul className="menu rounded-box w-56 bg-base-100 p-2">
{topics?.map((topic) => (
<li key={topic.id}>
<a
href="#"
onClick={(evt) => {
evt.preventDefault();
setSelectedTopic(topic);
}}
>
{topic.title}
</a>
</li>
))}
</ul>
<div className="divider"></div>
<input
type="text"
placeholder="New Topic"
className="input-bordered input input-sm w-full"
onKeyDown={(e) => {
if (e.key === "Enter") {
createTopic.mutate({
title: e.currentTarget.value,
});
e.currentTarget.value = "";
}
}}
/>
</div>
<div className="col-span-3">
<div>
{notes?.map((note) => (
<div key={note.id} className="mt-5">
<NoteCard
note={note}
onDelete={() => void deleteNote.mutate({ id: note.id })}
/>
</div>
))}
</div>

<NoteEditor
onSave={({ title, content }) => {
void createNote.mutate({
title,
content,
topicId: selectedTopic?.id ?? "",
});
}}
/>
</div>
</div>
);
};
};
Loading

0 comments on commit 16ba1e7

Please sign in to comment.