feat: display user's posts on profile page
This commit is contained in:
@@ -1,8 +1,29 @@
|
||||
import UserPosts from '@/components/UserPosts';
|
||||
import UserProfile from '@/components/UserProfile';
|
||||
import { getConvexClient } from '@/lib/convex';
|
||||
import { api } from '../../../../convex/_generated/api';
|
||||
|
||||
export default async function UserProfilePage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) {
|
||||
// TODO: Profile page. Display profile and user's post
|
||||
return <h1>{params.id}</h1>;
|
||||
const client = await getConvexClient();
|
||||
const user = await client.query(api.users.getUserByClerkId, {
|
||||
clerkUserId: params.id,
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
// TODO: Show real not found component
|
||||
return <div>Not Found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className='flex flex-col w-full h-full items-center'>
|
||||
<div className='w-full max-w-4xl'>
|
||||
<UserProfile user={user} />
|
||||
<UserPosts user={user} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@ import { api } from '../../convex/_generated/api';
|
||||
import PostContent from './PostContent';
|
||||
|
||||
export default function Editor() {
|
||||
const user = useUser();
|
||||
const { user, isSignedIn } = useUser();
|
||||
const { theme } = useTheme();
|
||||
const editorTheme = new Compartment();
|
||||
const [currentDoc, setCurrentDoc] = useState(['']);
|
||||
const createPost = useMutation(api.posts.createPost);
|
||||
const router = useRouter();
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const doc = [''];
|
||||
const extensions = [
|
||||
@@ -55,13 +56,15 @@ export default function Editor() {
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (user.isSignedIn !== true) {
|
||||
if (isSignedIn !== true) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreating(true);
|
||||
|
||||
const result = await createPost({
|
||||
clerkUserId: user.user.id,
|
||||
clerkUserId: user.id,
|
||||
content: currentDoc,
|
||||
});
|
||||
|
||||
@@ -76,6 +79,8 @@ export default function Editor() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,8 +126,8 @@ export default function Editor() {
|
||||
<div className='flex items-center justify-end p-4 pt-0'>
|
||||
<button
|
||||
type='submit'
|
||||
disabled={!currentDoc.join() || !currentDoc.join().trim()}
|
||||
className='py-1 px-4 bg-primary-accent text-white rounded-md disabled:opacity-50'
|
||||
disabled={!currentDoc.join() || !currentDoc.join().trim() || creating}
|
||||
className='py-1 px-4 bg-primary-accent text-white rounded-md disabled:opacity-50 flex items-center justify-center gap-2'
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { SignedIn, SignedOut, UserButton, currentUser } from '@clerk/nextjs';
|
||||
import { SignedIn, SignedOut, currentUser } from '@clerk/nextjs';
|
||||
import Link from 'next/link';
|
||||
import { BiSolidMessageSquareAdd } from 'react-icons/bi';
|
||||
import ThemeButton from './ThemeButton';
|
||||
import UserButton from './UserButton';
|
||||
|
||||
export default async function Navbar() {
|
||||
const user = await currentUser();
|
||||
@@ -50,19 +51,7 @@ export default async function Navbar() {
|
||||
</li>
|
||||
<SignedIn>
|
||||
<li>
|
||||
<UserButton
|
||||
appearance={{
|
||||
elements: {
|
||||
card: 'dark:bg-secondary-gray [&_*]:dark:text-white',
|
||||
userButtonTrigger:
|
||||
'border-solid border-2 border-primary-accent focus:shadow-none',
|
||||
},
|
||||
}}
|
||||
userProfileMode='navigation'
|
||||
userProfileUrl='/account'
|
||||
signInUrl='/login'
|
||||
afterSignOutUrl='/'
|
||||
/>
|
||||
<UserButton />
|
||||
</li>
|
||||
</SignedIn>
|
||||
<SignedOut>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Text } from '@codemirror/state';
|
||||
import { useQuery } from 'convex/react';
|
||||
import Image from 'next/image';
|
||||
import { api } from '../../convex/_generated/api';
|
||||
import { Doc } from '../../convex/_generated/dataModel';
|
||||
import PostContent from './PostContent';
|
||||
import SpinningLoader from './SpinningLoader';
|
||||
|
||||
export default function Post({ post }: { post: Doc<'posts'> }) {
|
||||
const user = useQuery(api.users.getUserById, { id: post.userId });
|
||||
|
||||
if (user === undefined) {
|
||||
return (
|
||||
<div>
|
||||
<SpinningLoader className='animate-spin w-5 h-5' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (user === 'USER_NOT_FOUND') {
|
||||
return <div>User for post not found</div>;
|
||||
}
|
||||
|
||||
const username = user.clerkUser.username ?? user._id;
|
||||
const createdPostDate = new Date(post._creationTime);
|
||||
const postYear = createdPostDate.getFullYear();
|
||||
const postMonth = createdPostDate.toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
});
|
||||
const dayOfMonth = createdPostDate.getDate();
|
||||
|
||||
return (
|
||||
<div className='flex w-full h-full gap-4 p-8 bg-white dark:bg-secondary-gray border border-gray-600'>
|
||||
<div>
|
||||
<Image
|
||||
alt='user profile image'
|
||||
src={user.clerkUser.image_url}
|
||||
width={40}
|
||||
height={40}
|
||||
className='rounded-full object-cover border-4 border-primary-accent'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div>{username}</div>
|
||||
<div className='text-xs'>{`${postMonth} ${dayOfMonth}, ${postYear}`}</div>
|
||||
</div>
|
||||
<div>
|
||||
<PostContent content={Text.of(post.content).toString()} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ImSpinner2 } from 'react-icons/im';
|
||||
|
||||
export default function SpinningLoader({ className }: { className: string }) {
|
||||
return <ImSpinner2 className={className} />;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useMountedEffect } from '@/hooks';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsMoonStarsFill, BsSunFill } from 'react-icons/bs';
|
||||
import { ImSpinner2 } from 'react-icons/im';
|
||||
|
||||
export default function ThemeButton() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { mounted, Loader } = useMountedEffect();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
@@ -19,7 +18,7 @@ export default function ThemeButton() {
|
||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||
>
|
||||
{mounted === false ? (
|
||||
<ImSpinner2 className='w-5 h-5 animate-spin' />
|
||||
<Loader className='animate-spin w-5 h-5' />
|
||||
) : isDark ? (
|
||||
<BsMoonStarsFill className='w-5 h-5' />
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
import { useMountedEffect } from '@/hooks';
|
||||
import { UserButton as ClerkUserButton } from '@clerk/nextjs';
|
||||
|
||||
export default function UserButton() {
|
||||
const { mounted } = useMountedEffect();
|
||||
|
||||
if (mounted === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClerkUserButton
|
||||
appearance={{
|
||||
elements: {
|
||||
card: 'dark:bg-secondary-gray [&_*]:dark:text-white',
|
||||
userButtonTrigger:
|
||||
'border-solid border-2 border-primary-accent focus:shadow-none',
|
||||
},
|
||||
}}
|
||||
userProfileMode='navigation'
|
||||
userProfileUrl='/account'
|
||||
signInUrl='/login'
|
||||
afterSignOutUrl='/'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { usePaginatedQuery } from 'convex/react';
|
||||
import { api } from '../../convex/_generated/api';
|
||||
import { Doc } from '../../convex/_generated/dataModel';
|
||||
import Post from './Post';
|
||||
|
||||
export default function UserPosts({ user }: { user: Doc<'users'> }) {
|
||||
const PAGE_SIZE = 10;
|
||||
const pager = usePaginatedQuery(
|
||||
api.posts.getUsersPostById,
|
||||
{
|
||||
userId: user._id,
|
||||
},
|
||||
{ initialNumItems: 10 }
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col w-full h-full'>
|
||||
{pager.results.map(post => (
|
||||
<Post
|
||||
key={post._id}
|
||||
post={post}
|
||||
/>
|
||||
))}
|
||||
{pager.isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => pager.loadMore(10)}
|
||||
disabled={pager.status !== 'CanLoadMore'}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Image from 'next/image';
|
||||
import { AiFillCalendar } from 'react-icons/ai';
|
||||
import { Doc } from '../../convex/_generated/dataModel';
|
||||
|
||||
export default function UserProfile({ user }: { user: Doc<'users'> }) {
|
||||
const username = user.clerkUser.username ?? user._id;
|
||||
const userCreatedDate = new Date(user._creationTime);
|
||||
const monthJoined = userCreatedDate.toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
});
|
||||
const yearJoined = userCreatedDate.getFullYear();
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className='shadow-md bg-gradient-to-r from-violet-600 via-violet-600 to-indigo-600 rounded-t-md pt-20 pl-5 pr-40'>
|
||||
<div className='flex items-center gap-4 -mb-[25px]'>
|
||||
<div>
|
||||
<Image
|
||||
alt='user profile image'
|
||||
src={user.clerkUser.image_url}
|
||||
width={100}
|
||||
height={100}
|
||||
className='rounded-full object-cover border-4 border-primary-accent'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className='font-bold'>{username}</h1>
|
||||
<div className='flex gap-2 items-center text-sm'>
|
||||
<AiFillCalendar className='w-4 h-4' />
|
||||
{`Joined ${monthJoined} ${yearJoined}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[50px] bg-white dark:bg-primary-gray pl-5 pr-40 border border-gray-600'></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
-1
@@ -1,3 +1,4 @@
|
||||
import SpinningLoader from '@/components/SpinningLoader';
|
||||
import { EditorState, Extension, Text } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -30,7 +31,15 @@ export function useCodeMirror({
|
||||
setEditorView(view);
|
||||
return () => view.destroy();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editorRef]);
|
||||
}, [editorRef.current]);
|
||||
|
||||
return { editorRef, editorView };
|
||||
}
|
||||
|
||||
export function useMountedEffect() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
return { mounted, Loader: SpinningLoader };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { auth } from '@clerk/nextjs';
|
||||
import { ConvexHttpClient } from 'convex/browser';
|
||||
|
||||
export async function getConvexClient() {
|
||||
const { user, getToken } = auth();
|
||||
const token = await getToken({ template: 'convex' });
|
||||
|
||||
if (token === null || user === null) {
|
||||
throw new Error('NOT_AUTHORIZED');
|
||||
}
|
||||
|
||||
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||
|
||||
if (CONVEX_URL === undefined) {
|
||||
throw new Error('NEXT_PUBLIC_CONVEX_URL is undefined');
|
||||
}
|
||||
|
||||
const client = new ConvexHttpClient(CONVEX_URL);
|
||||
client.setAuth(token);
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getFullMonth() {
|
||||
const monthMap = {};
|
||||
}
|
||||
Reference in New Issue
Block a user