Skip to content

Commit af0e85e

Browse files
author
JohnWeeks1
committed
first commit
1 parent 999d912 commit af0e85e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+4922
-157
lines changed

.env

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
NEXT_PUBLIC_APPWRITE_URL='https://cloud.appwrite.io/v1'
2+
NEXT_PUBLIC_ENDPOINT='64e08028dd2aa1998980'
3+
NEXT_PUBLIC_DATABASE_ID='64e0b3b7767b01f256bd'
4+
5+
NEXT_PUBLIC_COLLECTION_ID_PROFILE='64e0b3f965d3b8f229c7'
6+
NEXT_PUBLIC_COLLECTION_ID_POST='64e197dc80d9f8d6f3c5'
7+
NEXT_PUBLIC_COLLECTION_ID_LIKE='64e117c13f1139bad8da'
8+
NEXT_PUBLIC_COLLECTION_ID_COMMENT='64e2fc04e673d3987144'
9+
10+
NEXT_PUBLIC_BUCKET_NAME="tiktok-clone"
11+
NEXT_PUBLIC_BUCKET_ID='64e1195e05b45e82e612'
12+
NEXT_PUBLIC_PLACEHOLDER_DEAFULT_IMAGE_ID='64e11a7eb6dfbd875768'

.env.example

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
NEXT_PUBLIC_APPWRITE_URL='https://cloud.appwrite.io/v1'
2+
NEXT_PUBLIC_ENDPOINT=''
3+
NEXT_PUBLIC_DATABASE_ID=''
4+
5+
NEXT_PUBLIC_COLLECTION_ID_PROFILE=''
6+
NEXT_PUBLIC_COLLECTION_ID_POST=''
7+
NEXT_PUBLIC_COLLECTION_ID_LIKE=''
8+
NEXT_PUBLIC_COLLECTION_ID_COMMENT=''
9+
10+
NEXT_PUBLIC_BUCKET_NAME="tiktok-clone"
11+
NEXT_PUBLIC_BUCKET_ID=''
12+
NEXT_PUBLIC_PLACEHOLDER_DEAFULT_IMAGE_ID=''

app/components/AllOverlays.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client"
2+
3+
import { useGeneralStore } from "../stores/general";
4+
import AuthOverlay from "./AuthOverlay";
5+
import EditProfileOverlay from "./profile/EditProfileOverlay";
6+
import ClientOnly from "./ClientOnly";
7+
8+
export default function AllOverlays() {
9+
let { isLoginOpen, isEditProfileOpen } = useGeneralStore();
10+
return (
11+
<>
12+
<ClientOnly>
13+
{isLoginOpen ? <AuthOverlay /> : null}
14+
{isEditProfileOpen ? <EditProfileOverlay /> : null}
15+
</ClientOnly>
16+
</>
17+
)
18+
}

app/components/AuthOverlay.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { AiOutlineClose } from "react-icons/ai";
2+
import { useGeneralStore } from"@/app/stores/general"
3+
import Login from '@/app/components/auth/Login'
4+
import Register from '@/app/components/auth/Register'
5+
import { useState } from "react";
6+
7+
export default function AuthOverlay() {
8+
let { setIsLoginOpen } = useGeneralStore()
9+
10+
let [isRegister, setIsRegister] = useState<boolean>(false)
11+
12+
return (
13+
<>
14+
<div
15+
id="AuthOverlay"
16+
className="fixed flex items-center justify-center z-50 top-0 left-0 w-full h-full bg-black bg-opacity-50"
17+
>
18+
<div className="relative bg-white w-full max-w-[470px] h-[70%] p-4 rounded-lg">
19+
20+
<div className="w-full flex justify-end">
21+
<button onClick={() => setIsLoginOpen(false)} className="p-1.5 rounded-full bg-gray-100">
22+
<AiOutlineClose size="26"/>
23+
</button>
24+
</div>
25+
26+
{isRegister ? <Register /> : <Login />}
27+
28+
<div className="absolute flex items-center justify-center py-5 left-0 bottom-0 border-t w-full">
29+
<span className="text-[14px] text-gray-600">Don’t have an account?</span>
30+
31+
<button onClick={() => setIsRegister(isRegister = !isRegister)} className="text-[14px] text-[#F02C56] font-semibold pl-1" >
32+
<span>{!isRegister ? 'Register' : 'log in'}</span>
33+
</button>
34+
</div>
35+
36+
</div>
37+
</div>
38+
</>
39+
)
40+
}

app/components/ClientOnly.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use client';
2+
3+
import React, { useEffect, useState } from "react";
4+
5+
export default function ClientOnly({ children }: { children: React.ReactNode }) {
6+
7+
const [isClient, setIsClient] = useState(false)
8+
useEffect(() => { setIsClient(true) }, [])
9+
10+
return (<> {isClient ? <div>{children}</div> : null} </>);
11+
};

app/components/PostMain.tsx

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use client"
2+
3+
import { AiFillHeart } from "react-icons/ai"
4+
import { ImMusic } from "react-icons/im"
5+
import Link from "next/link"
6+
import { useEffect } from "react"
7+
import PostMainLikes from "./PostMainLikes"
8+
import useCreateBucketUrl from "../hooks/useCreateBucketUrl"
9+
import { PostMainCompTypes } from "../types"
10+
11+
export default function PostMain({ post }: PostMainCompTypes) {
12+
13+
useEffect(() => {
14+
const video = document.getElementById(`video-${post?.id}`) as HTMLVideoElement
15+
const postMainElement = document.getElementById(`PostMain-${post.id}`);
16+
17+
if (postMainElement) {
18+
let observer = new IntersectionObserver((entries) => {
19+
entries[0].isIntersecting ? video.play() : video.pause()
20+
}, { threshold: [0.6] });
21+
22+
observer.observe(postMainElement);
23+
}
24+
}, [])
25+
26+
return (
27+
<>
28+
<div id={`PostMain-${post.id}`} className="flex border-b py-6">
29+
30+
<div className="cursor-pointer">
31+
<img className="rounded-full max-h-[60px]" width="60" src={useCreateBucketUrl(post?.profile?.image)} />
32+
</div>
33+
34+
<div className="pl-3 w-full px-4">
35+
<div className="flex items-center justify-between pb-0.5">
36+
<Link href={`/profile/${post.profile.user_id}`}>
37+
<span className="font-bold hover:underline cursor-pointer">
38+
{post.profile.name}
39+
</span>
40+
</Link>
41+
42+
<button className="border text-[15px] px-[21px] py-0.5 border-[#F02C56] text-[#F02C56] hover:bg-[#ffeef2] font-semibold rounded-md">
43+
Follow
44+
</button>
45+
</div>
46+
<p className="text-[15px] pb-0.5 break-words md:max-w-[400px] max-w-[300px]">{post.text}</p>
47+
<p className="text-[14px] text-gray-500 pb-0.5">#fun #cool #SuperAwesome</p>
48+
<p className="text-[14px] pb-0.5 flex items-center font-semibold">
49+
<ImMusic size="17"/>
50+
<span className="px-1">original sound - AWESOME</span>
51+
<AiFillHeart size="20"/>
52+
</p>
53+
54+
<div className="mt-2.5 flex">
55+
<div
56+
className="relative min-h-[480px] max-h-[580px] max-w-[260px] flex items-center bg-black rounded-xl cursor-pointer"
57+
>
58+
<video
59+
id={`video-${post.id}`}
60+
loop
61+
controls
62+
muted
63+
className="rounded-xl object-cover mx-auto h-full"
64+
src={useCreateBucketUrl(post?.video_url)}
65+
/>
66+
<img
67+
className="absolute right-2 bottom-10"
68+
width="90"
69+
src="/images/tiktok-logo-white.png"
70+
/>
71+
</div>
72+
73+
<PostMainLikes post={post} />
74+
</div>
75+
</div>
76+
</div>
77+
</>
78+
)
79+
}

app/components/PostMainLikes.tsx

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { AiFillHeart } from "react-icons/ai"
2+
import { FaShare, FaCommentDots } from "react-icons/fa"
3+
import { useEffect, useState } from "react"
4+
import { useUser } from "../context/user"
5+
import { BiLoaderCircle } from "react-icons/bi"
6+
import { useGeneralStore } from "../stores/general"
7+
import { useRouter } from "next/navigation"
8+
import { Comment, Like, PostMainLikesCompTypes } from "../types"
9+
import useGetCommentsByPostId from "../hooks/useGetCommentsByPostId"
10+
import useGetLikesByPostId from "../hooks/useGetLikesByPostId"
11+
import useIsLiked from "../hooks/useIsLiked"
12+
import useCreateLike from "../hooks/useCreateLike"
13+
import useDeleteLike from "../hooks/useDeleteLike"
14+
15+
export default function PostMainLikes({ post }: PostMainLikesCompTypes) {
16+
17+
let { setIsLoginOpen } = useGeneralStore();
18+
19+
const router = useRouter()
20+
const contextUser = useUser()
21+
const [hasClickedLike, setHasClickedLike] = useState<boolean>(false)
22+
const [userLiked, setUserLiked] = useState<boolean>(false)
23+
const [comments, setComments] = useState<Comment[]>([])
24+
const [likes, setLikes] = useState<Like[]>([])
25+
26+
useEffect(() => {
27+
getAllLikesByPost()
28+
getAllCommentsByPost()
29+
}, [post])
30+
31+
useEffect(() => { hasUserLikedPost() }, [likes, contextUser])
32+
33+
const getAllCommentsByPost = async () => {
34+
let result = await useGetCommentsByPostId(post?.id)
35+
setComments(result)
36+
}
37+
38+
const getAllLikesByPost = async () => {
39+
let result = await useGetLikesByPostId(post?.id)
40+
setLikes(result)
41+
}
42+
43+
const hasUserLikedPost = () => {
44+
if (!contextUser) return
45+
46+
if (likes?.length < 1 || !contextUser?.user?.id) {
47+
setUserLiked(false)
48+
return
49+
}
50+
let res = useIsLiked(contextUser?.user?.id, post?.id, likes)
51+
setUserLiked(res ? true : false)
52+
}
53+
54+
const like = async () => {
55+
setHasClickedLike(true)
56+
await useCreateLike(contextUser?.user?.id || '', post?.id)
57+
await getAllLikesByPost()
58+
hasUserLikedPost()
59+
setHasClickedLike(false)
60+
}
61+
62+
const unlike = async (id: string) => {
63+
setHasClickedLike(true)
64+
await useDeleteLike(id)
65+
await getAllLikesByPost()
66+
hasUserLikedPost()
67+
setHasClickedLike(false)
68+
}
69+
70+
const likeOrUnlike = () => {
71+
if (!contextUser?.user?.id) {
72+
setIsLoginOpen(true)
73+
return
74+
}
75+
76+
let res = useIsLiked(contextUser?.user?.id, post?.id, likes)
77+
78+
if (!res) {
79+
like()
80+
} else {
81+
likes.forEach((like: Like) => {
82+
if (contextUser?.user?.id == like?.user_id && like?.post_id == post?.id) {
83+
unlike(like?.id)
84+
}
85+
})
86+
}
87+
}
88+
89+
return (
90+
<>
91+
<div id={`PostMainLikes-${post?.id}`} className="relative mr-[75px]">
92+
<div className="absolute bottom-0 pl-2">
93+
<div className="pb-4 text-center">
94+
<button
95+
disabled={hasClickedLike}
96+
onClick={() => likeOrUnlike()}
97+
className="rounded-full bg-gray-200 p-2 cursor-pointer"
98+
>
99+
{!hasClickedLike ? (
100+
<AiFillHeart color={likes?.length > 0 && userLiked ? '#ff2626' : ''} size="25"/>
101+
) : (
102+
<BiLoaderCircle className="animate-spin" size="25"/>
103+
)}
104+
105+
</button>
106+
<span className="text-xs text-gray-800 font-semibold">
107+
{likes?.length}
108+
</span>
109+
</div>
110+
111+
<button
112+
onClick={() => router.push(`/post/${post?.id}/${post?.profile?.user_id}`)}
113+
className="pb-4 text-center"
114+
>
115+
<div className="rounded-full bg-gray-200 p-2 cursor-pointer">
116+
<FaCommentDots size="25"/>
117+
</div>
118+
<span className="text-xs text-gray-800 font-semibold">{comments?.length}</span>
119+
</button>
120+
121+
<button className="text-center">
122+
<div className="rounded-full bg-gray-200 p-2 cursor-pointer">
123+
<FaShare size="25"/>
124+
</div>
125+
<span className="text-xs text-gray-800 font-semibold">55</span>
126+
</button>
127+
</div>
128+
</div>
129+
</>
130+
)
131+
}

app/components/TextInput.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { TextInputCompTypes } from "../types"
2+
3+
export default function TextInput({ string, inputType, placeholder, error, onUpdate }: TextInputCompTypes) {
4+
5+
return (
6+
<>
7+
<input
8+
placeholder={placeholder}
9+
className="
10+
block
11+
w-full
12+
bg-[#F1F1F2]
13+
text-gray-800
14+
border
15+
border-gray-300
16+
rounded-md
17+
py-2.5
18+
px-3
19+
focus:outline-none
20+
"
21+
value={string || ''}
22+
onChange={(event) => onUpdate(event.target.value)}
23+
type={inputType}
24+
autoComplete="off"
25+
/>
26+
27+
<div className="text-red-500 text-[14px] font-semibold">
28+
{error ? (error) : null}
29+
</div>
30+
</>
31+
)
32+
}

0 commit comments

Comments
 (0)