diff --git a/backend/prisma/migrations/20240713163709_thought/migration.sql b/backend/prisma/migrations/20240713163709_thought/migration.sql new file mode 100644 index 0000000..c75d822 --- /dev/null +++ b/backend/prisma/migrations/20240713163709_thought/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "Thought" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "publishedDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "published" BOOLEAN NOT NULL DEFAULT false, + "authorId" TEXT NOT NULL, + + CONSTRAINT "Thought_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Thought" ADD CONSTRAINT "Thought_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 86d94cd..06abd24 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -28,6 +28,7 @@ model User { comments Comment[] subscribers Subscriber[] @relation("UserSubscribers") subscribedTo Subscriber[] @relation("UserSubscribedTo") + thoughts Thought[] } model Post { @@ -100,3 +101,12 @@ model Subscriber { user User @relation("UserSubscribers", fields: [userId], references: [id]) subscriber User @relation("UserSubscribedTo", fields: [subscriberId], references: [id]) } + + model Thought { + id String @id @default(uuid()) + content String + publishedDate DateTime @default(now()) + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index beec1bd..4a3d650 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -7,6 +7,7 @@ import { clapRouter } from "./routes/clap"; import { tagRouter } from "./routes/tag"; import { subscriberRouter } from "./routes/subscriber"; import { commentRouter } from "./routes/comments"; +import { thoughtRouter } from "./routes/thought"; const app = new Hono<{ Bindings: { @@ -22,5 +23,6 @@ app.route('/api/v1/clap', clapRouter) app.route('/api/v1/tag', tagRouter) app.route('/api/v1/subscriber', subscriberRouter) app.route("/api/v1/comments", commentRouter) +app.route("/api/v1/thought", thoughtRouter) export default app; diff --git a/backend/src/routes/thought.ts b/backend/src/routes/thought.ts new file mode 100644 index 0000000..f56d8d5 --- /dev/null +++ b/backend/src/routes/thought.ts @@ -0,0 +1,200 @@ +import { Hono } from "hono"; +import { verify } from "hono/jwt"; +import { getDBInstance } from "../db/util"; + +/** + * + * Introducing Thoughts. User can write a thought. Thoughts are short form. Think of 1 Thought like a Twitter post or Threads post. + * + * thoughtRouter.post +thoughtRouter.put +thoughtRouter.get: 1 and all +thoughtRouter.delete + */ +export const thoughtRouter = new Hono<{ + Bindings: { + DATABASE_URL: string; + JWT_SECRET: string; + }; + Variables: { + userId: string; + }; +}>(); + + +//get all thoughts of a user +thoughtRouter.get("/all/:userId", async (c) => { + try { + const prisma = getDBInstance(c) + const userId = await c.req.param("userId"); + const thoughts = await prisma.thought.findMany({ + where: { + authorId: userId, + }, + select: { + authorId: true, + content: true, + id: true, + published: true, + publishedDate: true, + author: { + select: { + name: true + } + } + + } + }); + + return c.json({ + thoughts: thoughts + }); + } catch (e) { + c.status(411); + return c.json({ + message: "Error while fetching thoughts", + error: e, + }); + } + +}) + +//get a thought of a user +thoughtRouter.get("/:thoughtId", async (c) => { + try { + const prisma = getDBInstance(c) + const thoughtId = c.req.param("thoughtId"); + const thought = await prisma.thought.findFirst({ + where: { + id: thoughtId, + }, + select: { + authorId: true, + content: true, + id: true, + published: true, + publishedDate: true, + author: { + select: { + name: true + } + } + + + } + }); + + return c.json({ + thought: thought + }); + } catch (e) { + c.status(411); + return c.json({ + message: "Error while fetching thought", + error: e, + }); + } + +}) + +//protect all the routes below this +thoughtRouter.use("/*", async (c, next) => { + try { + const header = c.req.header("authorization") || ""; + const token = header.split(" ")[1]; + const user = await verify(token, c.env.JWT_SECRET); + if (user && typeof user.id === "string") { + c.set("userId", user.id); + return next(); + } else { + c.status(403); + return c.json({ error: "Unauthorized " }); + } + } catch (e) { + c.status(403); + return c.json({ + error: "Credentials failed", + }); + } +}); + +//create thought +thoughtRouter.post("/create", async (c) => { + try { + const prisma = getDBInstance(c) + const userId = c.get("userId"); + const body = await c.req.json(); + const { content } = body; + if (!content) { + c.status(400); + return c.json({ + message: "Content is required", + }); + } + const thought = await prisma.thought.create({ + data: { + authorId: userId, + content: content, + }, + }); + + return c.json({ + id: thought.id + }); + } catch (e) { + c.status(403); + return c.json({ error: "Something went wrong ", stackTrace: e }); + } +}) + +//update thought +thoughtRouter.put("/update", async (c) => { + try { + const prisma = getDBInstance(c) + const body = await c.req.json(); + const { id, content } = body; + if (!content || !id) { + c.status(400); + return c.json({ + message: "Please provide all required values", + }); + } + const thought = await prisma.thought.update({ + where: { + id: id, + }, + data: { + content: content, + }, + }); + + return c.json({ + id: thought.id + }); + } catch (e) { + c.status(403); + return c.json({ error: "Something went wrong ", stackTrace: e }); + } +}) + +//to delete a thought +thoughtRouter.delete("/:thoughtId", async (c) => { + try { + const prisma = getDBInstance(c) + const thoughtId = c.req.param("thoughtId"); + await prisma.thought.delete({ + where: { + id: thoughtId, + }, + }); + + return c.json({ + message: "Thought deleted successfully", + }); + } catch (e) { + c.status(411); + return c.json({ + message: "Error while deleting post", + }); + } +}) \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0a671a0..c3a4520 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { lazy, Suspense } from 'react'; import Spinner from './components/Spinner'; import { ThemeProvider } from '@/components/theme-provider'; +import Thought from './pages/Thought'; // const Home = lazy(() => import('./pages/Home')); const Signup = lazy(() => import('./pages/Signup')); const Signin = lazy(() => import('./pages/Signin')); @@ -30,12 +31,13 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> } /> } /> - } /> + } /> {/* } /> */} } /> diff --git a/frontend/src/components/Appbar.tsx b/frontend/src/components/Appbar.tsx index 6eb7537..762bffe 100644 --- a/frontend/src/components/Appbar.tsx +++ b/frontend/src/components/Appbar.tsx @@ -72,6 +72,14 @@ const Appbar = ({ skipAuthCheck = false, pageActions, hideWriteAction = false }: )} + + +
diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 67321b7..26dcdbb 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -4,4 +4,4 @@ export const GITHUB_CONTRIBUTOR_URL = 'https://api.github.com/repos/aadeshkulkar export const FF_ENABLE_AI = true; // Feature flag (temp solution) export const FF_IMAGE_UPLOADS = true; // Set this to false, if Cloudfare R2 (Image upload) is not setup export const CHAT_LIMIT = 20; -export const MAX_TITLE_LENGTH = 100; +export const MAX_TITLE_LENGTH = 100; \ No newline at end of file diff --git a/frontend/src/pages/Thought.tsx b/frontend/src/pages/Thought.tsx new file mode 100644 index 0000000..51a23ee --- /dev/null +++ b/frontend/src/pages/Thought.tsx @@ -0,0 +1,185 @@ +import Appbar from '@/components/Appbar'; +import RemoveIcon from '@/components/icons/Remove'; +import WriteIcon from '@/components/icons/Write'; +import { BACKEND_URL } from '@/config'; +import axios from 'axios'; +import React, { FormEvent, useEffect, useState } from 'react'; +interface Thought { + id: string; + content: string; + publishedDate: Date; + published: boolean; + authorId: string; + author: { + name: string; + }; +} + +const Thought = () => { + const [content, setContent] = useState(''); + const [isEdit, setIsEdit] = useState(false); + const [editId, setEditId] = useState(''); + + const [thoughts, setthoughts] = useState(); + const userId = JSON.parse(localStorage.getItem('user') as string).id; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!isEdit) { + try { + const response = await axios.post( + `${BACKEND_URL}/api/v1/thought/create`, + { + content, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + } + ); + + console.log(response); + alert('posted your thought successfully'); + window.location.reload(); + } catch (error) { + alert('something went wrong'); + } + } else { + try { + const response = await axios.put( + `${BACKEND_URL}/api/v1/thought/update`, + { + id:editId, + content, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + } + ); + + console.log(response); + alert('updated your thought successfully'); + window.location.reload(); + } catch (error) { + alert('something went wrong'); + } + } + }; + const handleEnter = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + setContent(content + '\n'); + } + console.log(content); + }; + + const handleEdit = (itemId: string, content: string) => { + setIsEdit(true); + setContent(content); + setEditId(itemId); + }; + + const handleCancel = ()=>{ + setIsEdit(false) + setContent("") + setEditId("") + } + + const handleDelete = async(id:string)=>{ + try { + + await axios.delete( + `${BACKEND_URL}/api/v1/thought/${id}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }) + alert("deleted successfully") + window.location.reload() + } catch (error) { + // alert("something went wrong") + } + } + + useEffect(() => { + const getData = async () => { + const response = await axios.get(`${BACKEND_URL}/api/v1/thought/all/${userId}`); + const { thoughts } = response.data; + setthoughts(thoughts); + console.log(thoughts); + }; + + getData(); + }, []); + + return ( + <> + +
+
+
+
write your thought
+