Skip to content

Commit

Permalink
add twitter-clone project (#384)
Browse files Browse the repository at this point in the history
  • Loading branch information
azizlimonu authored Jun 21, 2023
1 parent 24d6c56 commit a47a589
Show file tree
Hide file tree
Showing 66 changed files with 10,384 additions and 0 deletions.
3 changes: 3 additions & 0 deletions PROJECTS/twitter-clone/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
37 changes: 37 additions & 0 deletions PROJECTS/twitter-clone/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
51 changes: 51 additions & 0 deletions PROJECTS/twitter-clone/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import useUser from "@/hooks/useUser";
import { useCallback } from 'react'
import { useRouter } from "next/router";
import Image from "next/image";

interface AvatarProps {
userId: string;
isLarge?: boolean;
hasBorder?: boolean;
}

const Avatar: React.FC<AvatarProps> = ({ userId, isLarge, hasBorder }) => {
const router = useRouter();
const { data: fetchedUser } = useUser(userId);
// console.log("data :", fetchedUser);

const onClick = useCallback((event: any) => {
event.stopPropagation();

const url = `/users/${userId}`
router.push(url);
}, [router, userId]);

return (
<div
className={`
${hasBorder ? 'border-4 border-black' : ''}
${isLarge ? 'h-32' : 'h-12'}
${isLarge ? 'w-32' : 'w-12'}
rounded-full
hover:opacity-90
transition
cursor-pointer
relative
`}
>
<Image
fill
style={{
objectFit: 'cover',
borderRadius: '100%'
}}
alt="Avatar"
onClick={onClick}
src={fetchedUser?.profileImage || '/images/placeholder.png'}
/>
</div>
)
}

export default Avatar
45 changes: 45 additions & 0 deletions PROJECTS/twitter-clone/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'

interface ButtonProps {
label: string;
secondary?: boolean;
fullWidth?: boolean;
large?: boolean;
onClick: () => void;
disabled?: boolean;
outline?: boolean;
}

const Button: React.FC<ButtonProps> = ({
label,
secondary,
fullWidth,
onClick,
large,
disabled,
outline
}) => {
return (
<button
disabled={disabled}
onClick={onClick}
className={`
disabled:opacity-70 disabled:cursor-not-allowed rounded-full font-semibold hover:opacity-80 transition border-2
${fullWidth ? 'w-full' : 'w-fit'}
${secondary ? 'bg-white' : 'bg-sky-500'}
${secondary ? 'text-black' : 'text-white'}
${secondary ? 'border-black' : 'border-sky-500'}
${large ? 'text-xl' : 'text-md'}
${large ? 'px-5' : 'px-4'}
${large ? 'py-3' : 'py-2'}
${outline ? 'bg-transparent' : ''}
${outline ? 'border-white' : ''}
${outline ? 'text-white' : ''}
`}
>
{label }
</button >
)
}

export default Button
104 changes: 104 additions & 0 deletions PROJECTS/twitter-clone/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import axios from 'axios';
import { useCallback, useState } from 'react';
import { toast } from 'react-hot-toast';

import useLoginModal from '@/hooks/useLoginModal';
import useCurrentUser from '@/hooks/useCurrentUser';
import useRegisterModal from '@/hooks/useRegisterModal.';
import usePosts from '@/hooks/usePosts';
import usePost from '@/hooks/usePost';

import Avatar from './Avatar';
import Button from './Button';

interface FormProps {
placeholder: string;
isComment?: boolean;
postId?: string;
}

const Form: React.FC<FormProps> = ({ placeholder, isComment, postId }) => {
const registerModal = useRegisterModal();
const loginModal = useLoginModal();

const { data: currentUser } = useCurrentUser();
const { mutate: mutatePosts } = usePosts();
const { mutate: mutatePost } = usePost(postId as string);

const [body, setBody] = useState('');
const [isLoading, setIsLoading] = useState(false);

const onSubmit = useCallback(async () => {
try {
setIsLoading(true);

const url = isComment ? `/api/comments?postId=${postId}` : '/api/posts';

await axios.post(url, { body });

toast.success('Tweet created');
setBody('');
mutatePosts();
mutatePost();
} catch (error) {
toast.error('Something went wrong');
} finally {
setIsLoading(false);
}
}, [body, mutatePosts, isComment, postId, mutatePost]);

return (
<div className="border-b-[1px] border-neutral-800 px-5 py-2">
{currentUser ? (
<div className="flex flex-row gap-4">
<div>
<Avatar userId={currentUser?.id} />
</div>
<div className="w-full">
<textarea
disabled={isLoading}
onChange={(event) => setBody(event.target.value)}
value={body}
className="
disabled:opacity-80
peer
resize-none
mt-3
w-full
bg-black
ring-0
outline-none
text-[20px]
placeholder-neutral-500
text-white
"
placeholder={placeholder}>
</textarea>
<hr
className="
opacity-0
peer-focus:opacity-100
h-[1px]
w-full
border-neutral-800
transition"
/>
<div className="mt-4 flex flex-row justify-end">
<Button disabled={isLoading || !body} onClick={onSubmit} label="Tweet" />
</div>
</div>
</div>
) : (
<div className="py-8">
<h1 className="text-white text-2xl text-center mb-4 font-bold">Welcome to Twitter</h1>
<div className="flex flex-row items-center justify-center gap-4">
<Button label="Login" onClick={loginModal.onOpen} />
<Button label="Register" onClick={registerModal.onOpen} secondary />
</div>
</div>
)}
</div>
);
};

export default Form;
36 changes: 36 additions & 0 deletions PROJECTS/twitter-clone/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useRouter } from 'next/router'
import React, { useCallback } from 'react'
import { BiArrowBack } from 'react-icons/bi';

interface HeaderProps {
showBackArrow?: boolean;
label: string;
}

const Header: React.FC<HeaderProps> = ({ showBackArrow, label }) => {
const router = useRouter();

const handleBack = useCallback(() => {
router.back();
}, [router]);

return (
<div className='border-b-[1px] border-neutral-800 p-5'>
<div className='flex flex-row items-center gap-2'>
{showBackArrow && (
<BiArrowBack
onClick={handleBack}
color="white"
size={20}
className="cursor-pointer hover:opacity-70 transition"
/>
)}
<h1 className="text-white text-xl font-semibold">
{label}
</h1>
</div>
</div>
)
}

export default Header
65 changes: 65 additions & 0 deletions PROJECTS/twitter-clone/components/ImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Image from 'next/image';
import React, { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone';

interface ImageUploadProps {
onChange: (base64: string) => void;
label: string;
value?: string;
disabled?: boolean;
}

const ImageUpload: React.FC<ImageUploadProps> = ({
onChange, label, value, disabled
}) => {
const [base64, setBase64] = useState(value);

const handleChange = useCallback((base64: string) => {
onChange(base64)
}, [onChange]);

const handleDrop = useCallback((files: any) => {
const file = files[0];
const reader = new FileReader();

reader.onload = (event: any) => {
setBase64(event.target.result);
handleChange(event.target.result);
}

reader.readAsDataURL(file)
}, [handleChange]);

const { getRootProps, getInputProps } = useDropzone({
maxFiles: 1,
onDrop: handleDrop,
disabled,
accept: {
"image/jpeg": [],
"image/png": []
}
})

return (
<div {...getRootProps({
className: 'w-full p-4 text-white text-center border-2 border-dotted rounded-md border-neutral-700'
})}
>
<input {...getInputProps()} />
{base64 ? (
<div className="flex items-center justify-center">
<Image
src={base64}
height="100"
width="100"
alt="Uploaded image"
/>
</div>
) : (
<p className='text-white'>{label}</p>
)}
</div>
)
}

export default ImageUpload
27 changes: 27 additions & 0 deletions PROJECTS/twitter-clone/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react'

interface InputProps {
placeholder?: string;
value?: string;
type?: string;
disabled?: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label?: string;
}

const Input: React.FC<InputProps> = ({
placeholder, value, type = "text", onChange, disabled, label
}) => {
return (
<input
disabled={disabled}
onChange={onChange}
value={value == null ? '' : value}
placeholder={placeholder}
type={type}
className='w-full p-4 text-lg bg-black border-2 border-neutral-800 rounded-md outline-none text-white focus:border-sky-500 focus:border-2 transition disabled:bg-neutral-900 disabled:opacity-70 disabled:cursor-not-allowed'
/>
)
}

export default Input;
Loading

1 comment on commit a47a589

@vercel
Copy link

@vercel vercel bot commented on a47a589 Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ptw – ./

ptw-git-main-devvsakib.vercel.app
ptw-devvsakib.vercel.app
ptwa.vercel.app

Please sign in to comment.