Skip to content

Commit

Permalink
feat: tweet create feature added
Browse files Browse the repository at this point in the history
  • Loading branch information
pratyushsingha committed Mar 4, 2024
1 parent d0b99da commit fba2ab4
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 6 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"axios": "^1.6.7",
"check-password-strength": "^2.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"emoji-picker-react": "^4.8.0",
"lucide-react": "^0.340.0",
"moment": "^2.30.1",
"react": "^18.2.0",
Expand Down
95 changes: 92 additions & 3 deletions src/components/CommentCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,101 @@ const CommentCard = ({
>
see replies
</Button>
{replies.map((reply) => (
<p key={reply._id}>{reply.content}</p>
))}
</div>
</div>
</div>
{replies.map(
({
_id,
ownerDetails,
updatedAt,
content,
images,
isLiked,
likeCount,
}) => (
<div
key={_id}
className="relative border-b border-slate ml-20 last:border-none"
>
<div className="flex p-4 text-white">
<div className="relative shrink-0 before:absolute before:left-1/2 before:top-[19px] before:z-[5] before:h-full before:w-[1px] before:bg-slate-800">
<div className="relative z-10 h-10 w-10 sm:h-12 sm:w-12">
<img
src={ownerDetails?.avatar}
alt={ownerDetails?.username}
className="h-full w-full rounded-full object-cover"
/>
</div>
</div>
<div className="pl-4 pt-1">
<div className="mb-2 flex items-center gap-x-2">
<div className="w-full">
<h2 className="inline-block font-bold">
{ownerDetails?.username}
</h2>
<span className="ml-2 inline-block text-sm text-gray-400">
{moment(updatedAt, "YYYYMMDD").fromNow()}
</span>
</div>
<button className="ml-auto shrink-0 hover:text-[#ae7aff]">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
aria-hidden="true"
className="h-5 w-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z"
></path>
</svg>
</button>
</div>
<p className="mb-4 text-sm sm:text-base">{content}</p>
{images && (
<>
<div className="mb-4 grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-4">
{images.map((image, index) => (
<img
key={index}
src={image}
alt={image}
className="rounded-md"
/>
))}
</div>
</>
)}
<div className="flex gap-x-4">
<button
onClick={(e) => toggleCommentLike(_id, e)}
className="group inline-flex items-center gap-x-1 outline-none after:content-[attr(data-like-count)] hover:text-[#ae7aff] focus:text-[#ae7aff] focus:after:content-[attr(data-like-count-alt)]"
>
{isLiked ? <Heart fill="#1A8CD8" /> : <Heart />}
<span>{likeCount}</span>
</button>
<button className="inline-flex items-center gap-x-1 outline-none hover:text-[#ae7aff]">
<MessageCircle />
<span>20</span>
</button>
<Button
className={disable ? "hidden" : "block"}
onClick={(e) => commentComments(_id, e)}
variant="ghost"
>
see replies
</Button>
</div>
</div>
</div>
</div>
)
)}
</div>
);
};
Expand Down
6 changes: 6 additions & 0 deletions src/components/Index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "./ui/separator";
import { Switch } from "./ui/switch";

import Login from "@/pages/auth/Login.jsx";
import App from "@/App";
Expand All @@ -27,6 +29,7 @@ import Spinner from "@/components/loader/Spinner";
import Container from "@/components/Container";
import Sidebar from "@/components/Sidebar";
import CommentCard from "@/components/CommentCard";
import TweetBox from "@/components/TweetBox";

export {
sidebarItems,
Expand Down Expand Up @@ -55,4 +58,7 @@ export {
DialogTitle,
DialogTrigger,
CommentCard,
TweetBox,
Separator,
Switch,
};
189 changes: 189 additions & 0 deletions src/components/TweetBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useContext, useEffect, useState } from "react";
import {
AppContext,
Button,
Label,
Separator,
useToast,
Switch,
} from "./Index";
import { Hash, Image, SmilePlus } from "lucide-react";
import EmojiPicker from "emoji-picker-react";
import axios from "axios";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const TweetBox = () => {
const { toast } = useToast();
const { userDetails, setLoading } = useContext(AppContext);
const [openImoji, setOpenImoji] = useState(false);
const [isAnonymous, setIsAnonymous] = useState(false);
const [tagClick, setTagClick] = useState(false);
const [previewImages, setPreviewImages] = useState([]);
const [multipleImages, setMultipleImages] = useState();

const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty, isTouched, isSubmitSuccessful },
reset,
} = useForm({
defaultValues: {
content: "",
tags: "",
images: [],
},
});

const handleMultipleImages = (e) => {
if (e.target.files) {
setMultipleImages(e.target.files);
const imgArr = Array.from(e.target.files).map((file) =>
URL.createObjectURL(file)
);
setPreviewImages((prevImg) => prevImg.concat(imgArr));
}
};

const toggleAnonymous = (value) => {
setIsAnonymous(value);
if (value === true) {
toast({
title: "Tweet set to anonymous",
status: "success",
});
} else {
toast({
title: "Tweet set to public",
status: "success",
});
}
};

const createTweet = async (data) => {
setLoading(true);
try {
const formdata = new FormData();
formdata.append("content", data.content);
formdata.append("tags", data.tags);
formdata.append("isAnonymous", isAnonymous);
for (let i = 0; i < data.images.length; i++) {
formdata.append("images", data.images[i]);
}
const response = await axios.post(
`${import.meta.env.VITE_BACKEND_URL}/tweet`,
formdata,
{
withCredentials: true,
headers: {
"Content-Type": "multipart/form-data",
},
}
);
console.log(data);
toast({
title: response.data.message,
});
setLoading(false);
console.log(response);
} catch (error) {
console.log(error);
setLoading(false);
}
};

const render = (data) => {
return (
<div className="mb-4 grid grid-cols-[repeat(auto-fit,_minmax(200px,_1fr))] gap-4">
{data.map((image, index) => (
<img key={index} src={image} alt={image} className="rounded-md" />

Check warning

Code scanning / CodeQL

DOM text reinterpreted as HTML Medium

DOM text
is reinterpreted as HTML without escaping meta-characters.
))}
</div>
);
};

useEffect(() => {
if (isSubmitSuccessful)
reset({
content: "",
tags: "",
images: [],
});
}, [isSubmitSuccessful]);
return (
<form onSubmit={handleSubmit(createTweet)}>
<div className="relative flex-col border-slate border-2 rounded px-2 py-2 my-4">
<div className="flex space-x-5 w-full ">
<img
className="h-10 w-10 shrink-0 sm:h-12 self-center sm:w-12 rounded-full"
src={userDetails.avatar}
alt=""
/>
<div className="w-full relative">
<textarea
className="py-5 text-pretty text-xl border-none focus:border-none bg-black w-full focus:outline-none"
placeholder="what is happening?!"
{...register("content", {
required: true,
})}
/>
<input
placeholder="#trending..."
className={`text-[#1a8cd8] w-full rounded my-2 font-semibold bg-black outline-none ${
tagClick ? "block" : "hidden"
}`}
{...register("tags")}
/>
{render(previewImages)}
{openImoji && (
<div className="absolute top-52 left-0 z-50">
<EmojiPicker className="object-cover" theme="dark" />
</div>
)}
</div>
</div>
<Separator className="my-3" />
<div className="mx-4 flex justify-between">
<div>
<Label htmlFor="imageUpload">
<Button type="button" variant="ghost">
<Image color="#1a8cd8" />
</Button>
</Label>
<input
id="imageUpload"
type="file"
accept="image/png, image/jpg, image/jpeg"
multiple
{...register("images")}
onChange={(e) => handleMultipleImages(e)}
/>
<Button onClick={() => setOpenImoji(!openImoji)} variant="ghost">
<SmilePlus color="#1a8cd8" />
</Button>
<Button type="button" variant="ghost">
<Switch
defaultChecked={false}
onCheckedChange={(value) => toggleAnonymous(value)}
/>
</Button>

<Button
type="button"
onClick={() => setTagClick(!tagClick)}
variant="ghost"
>
<Hash color="#1a8cd8" />
</Button>
</div>
<Button disabled={!isDirty && !isTouched} type="submit">
Post
</Button>
</div>
</div>
</form>
);
};

export default TweetBox;
24 changes: 24 additions & 0 deletions src/components/ui/switch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client"

import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"

import { cn } from "@/lib/utils"

const Switch = React.forwardRef(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)} />
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName

export { Switch }
18 changes: 18 additions & 0 deletions src/components/ui/textarea.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from "react"

import { cn } from "@/lib/utils"

const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
(<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />)
);
})
Textarea.displayName = "Textarea"

export { Textarea }
Loading

0 comments on commit fba2ab4

Please sign in to comment.