Merge pull request #42 from StevanFreeborn/stevanfreeborn/feat/add-finishing-touches
feat: add missing pages
This commit is contained in:
+6
-1
@@ -3,6 +3,11 @@
|
|||||||
"extends": ["next/core-web-vitals", "plugin:prettier/recommended"],
|
"extends": ["next/core-web-vitals", "plugin:prettier/recommended"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "warn",
|
"no-console": "warn",
|
||||||
"prettier/prettier": "error"
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ An [X](https://twitter.com/home) clone built as a submission for the [WebDevCody
|
|||||||
- [Clerk](https://clerk.com/) - User authentication and management
|
- [Clerk](https://clerk.com/) - User authentication and management
|
||||||
- [Tailwind](https://tailwindcss.com/) - Styling
|
- [Tailwind](https://tailwindcss.com/) - Styling
|
||||||
- [Vercel](https://vercel.com/) - Hosting
|
- [Vercel](https://vercel.com/) - Hosting
|
||||||
|
- [Codemirror](https://codemirror.net/) - Editor
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | About',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center flex-1">
|
||||||
|
<article className="flex flex-col w-full max-w-4xl gap-8">
|
||||||
|
<section className="flex flex-col items-center gap-2">
|
||||||
|
<h1 className="text-4xl mt-4">
|
||||||
|
Welcome to{' '}
|
||||||
|
<span className="italic">
|
||||||
|
conve<span className="font-bold text-primary-accent">X</span>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-xl">
|
||||||
|
An X inspired social site that supports following other users,
|
||||||
|
liking posts, and sharing both long and short posts formatted with{' '}
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://commonmark.org/help/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Markdown
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">About</h2>
|
||||||
|
<p>
|
||||||
|
This site was built as a submission for the WebDevCody{' '}
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://hackathon.webdevcody.com/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
hackathon
|
||||||
|
</a>
|
||||||
|
. The following are some technologies that were used to build the
|
||||||
|
site:
|
||||||
|
</p>
|
||||||
|
<ul className=" flex flex-col gap-2 list-disc ml-4">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://www.typescriptlang.org/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
TypeScript
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://nextjs.org/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
NEXT.js
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://www.convex.dev/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Convex
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://clerk.com/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Clerk
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://tailwindcss.com/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Tailwind
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://codemirror.net/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Codemirror
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="text-primary-accent font-semibold dark:text-white dark:underline"
|
||||||
|
href="https://vercel.com/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Vercel
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
'use client';
|
import AccountForm from '@/components/AccountForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import { UserProfile } from '@clerk/nextjs';
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Account',
|
||||||
|
};
|
||||||
|
|
||||||
export default function AccountPage() {
|
export default function AccountPage() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-center">
|
<main className="flex flex-col items-center">
|
||||||
<UserProfile
|
<AccountForm />
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
card: 'sm:pb-4 shadow-md dark:bg-secondary-gray [&_*:not(button):not(a)]:dark:text-white [&_.cl-internal-b3fm6y]:hidden',
|
|
||||||
navbar: 'hidden',
|
|
||||||
navbarMobileMenuRow: 'hidden',
|
|
||||||
formFieldInput: 'dark:bg-primary-gray dark:text-white',
|
|
||||||
profileSectionTitle: 'dark:border-white dark:border-opacity-10',
|
|
||||||
breadcrumbsItem: 'dark:text-white',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
import { RiTwitterXLine } from 'react-icons/ri';
|
||||||
|
import { TiSocialLinkedin } from 'react-icons/ti';
|
||||||
|
import { VscGithub } from 'react-icons/vsc';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Contact',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContactPage() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center justify-center flex-1">
|
||||||
|
<article className="flex flex-col w-full max-w-4xl">
|
||||||
|
<section className="flex flex-col items-center gap-8">
|
||||||
|
<h1 className="text-4xl font-bold">Get in touch</h1>
|
||||||
|
<ul className="flex flex-col items-center gap-8 sm:flex-row">
|
||||||
|
<li className="bg-primary-accent rounded-full p-1 shadow-md hover:scale-125 transition duration-300 ease-in transform">
|
||||||
|
<a
|
||||||
|
href="https://github.com/StevanFreeborn"
|
||||||
|
target="_blank"
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
<VscGithub className="w-16 h-16" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="bg-primary-accent rounded-full p-1 shadow-md hover:scale-125 transition duration-300 ease-in transform">
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/stevan_freeborn"
|
||||||
|
target="_blank"
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
<RiTwitterXLine className="w-16 h-16" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="bg-primary-accent rounded-full p-1 shadow-md hover:scale-125 transition duration-300 ease-in transform">
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/stevan-freeborn-77917b75/"
|
||||||
|
target="_blank"
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
<TiSocialLinkedin className="w-16 h-16" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center flex-1">
|
||||||
|
<div className="flex flex-col items-center w-full max-w-4xl gap-4">
|
||||||
|
<h2 className="text-2xl">
|
||||||
|
Oops it looks like we've run into an issue.
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1 text-white bg-primary-accent rounded-md"
|
||||||
|
onClick={() => reset()}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,9 @@
|
|||||||
import FollowingPosts from '@/components/FollowingPosts';
|
import FollowingPosts from '@/components/FollowingPosts';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Following',
|
||||||
|
};
|
||||||
|
|
||||||
export default function FollowingPage() {
|
export default function FollowingPage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
|
export default function GlobalError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en" className="w-full h-full">
|
||||||
|
<body
|
||||||
|
className={`w-full h-full flex flex-col bg-white text-primary-gray dark:bg-primary-gray dark:text-white ${inter.className}`}
|
||||||
|
>
|
||||||
|
<main className="flex flex-col justify-center items-center flex-1">
|
||||||
|
<div className="flex flex-col items-center w-full max-w-4xl gap-4">
|
||||||
|
<h2 className="text-2xl">
|
||||||
|
Oops it looks like we've run into an issue.
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-white bg-primary-accent rounded-md"
|
||||||
|
onClick={() => reset()}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
+2
-1
@@ -10,7 +10,8 @@ import './globals.css';
|
|||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'conveX',
|
description:
|
||||||
|
'An X inspired social site that supports liking, following, and posting short and long form content',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
'use client';
|
import LoginForm from '@/components/LoginForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import { SignIn } from '@clerk/nextjs';
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Login',
|
||||||
|
};
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col flex-1 items-center">
|
<main className="flex flex-col flex-1 items-center">
|
||||||
<SignIn
|
<LoginForm />
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
card: 'shadow-md dark:bg-secondary-gray [&_*:not(button):not(a)]:dark:text-white [&_.cl-internal-b3fm6y]:hidden',
|
|
||||||
socialButtonsIconButton__github:
|
|
||||||
'hover:bg-gradient-to-br from-blue-600 via-purple-600 to-red-600 dark:border-white dark:border-opacity-10',
|
|
||||||
socialButtonsIconButton__google:
|
|
||||||
'hover:bg-gradient-to-r from-blue-500 via-green-500 to-yellow-500 dark:border-white dark:border-opacity-10',
|
|
||||||
socialButtonsIconButton__microsoft:
|
|
||||||
'hover:bg-gradient-to-r from-red-600 via-green-600 to-blue-600 dark:border-white dark:border-opacity-10',
|
|
||||||
dividerLine: 'dark:bg-white dark:bg-opacity-10',
|
|
||||||
formFieldInput: 'dark:bg-primary-gray dark:text-white',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function NotFound({
|
||||||
|
heading = 'Not Found',
|
||||||
|
message = "Hmmm it looks like we couldn't find what you were looking for.",
|
||||||
|
}: {
|
||||||
|
heading?: string;
|
||||||
|
message?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center flex-1">
|
||||||
|
<div className="flex flex-col items-center w-full max-w-4xl gap-4">
|
||||||
|
<h2 className="text-2xl">{heading}</h2>
|
||||||
|
<p>{message}</p>
|
||||||
|
<Link
|
||||||
|
className="px-3 py-1 text-white bg-primary-accent rounded-md"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Go to home page
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import AllPosts from '@/components/AllPosts';
|
import AllPosts from '@/components/AllPosts';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Home',
|
||||||
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,69 +1,14 @@
|
|||||||
'use client';
|
import EditPostForm from '@/components/EditPostForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import Editor, { SubmitActionParams } from '@/components/Editor';
|
export const metadata: Metadata = {
|
||||||
import SpinningLoader from '@/components/SpinningLoader';
|
title: 'conveX | Edit post',
|
||||||
import { useRouter } from '@/hooks';
|
};
|
||||||
import { useUser } from '@clerk/nextjs';
|
|
||||||
import { useMutation, useQuery } from 'convex/react';
|
|
||||||
import { api } from '../../../../../convex/_generated/api';
|
|
||||||
import { Id } from '../../../../../convex/_generated/dataModel';
|
|
||||||
|
|
||||||
export default function EditPostPage({ params }: { params: { id: string } }) {
|
export default function EditPostPage({ params }: { params: { id: string } }) {
|
||||||
const user = useUser();
|
|
||||||
const post = useQuery(api.posts.getPostById, {
|
|
||||||
id: params.id as Id<'posts'>,
|
|
||||||
});
|
|
||||||
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
if (user.isSignedIn === false) {
|
|
||||||
router.push('/login');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (post === 'POST_NOT_FOUND' || post === 'USER_FOR_POST_NOT_FOUND') {
|
|
||||||
// TODO: Return actual not found component
|
|
||||||
return <h1>Not Found</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitAction({
|
|
||||||
clerkUserId,
|
|
||||||
parentPostId,
|
|
||||||
post,
|
|
||||||
currentDoc,
|
|
||||||
}: SubmitActionParams) {
|
|
||||||
const result = await createOrUpdatePost({
|
|
||||||
parentPostId: parentPostId,
|
|
||||||
id: post?._id,
|
|
||||||
clerkUserId: clerkUserId,
|
|
||||||
content: currentDoc,
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case 'USER_NOT_FOUND':
|
|
||||||
case 'USER_NOT_AUTHORIZED':
|
|
||||||
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
|
|
||||||
throw Error('Unable to update post');
|
|
||||||
default:
|
|
||||||
router.push(`/posts/${post?._id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col flex-1 w-full items-center">
|
<main className="flex flex-col flex-1 w-full items-center">
|
||||||
{user.isLoaded && post !== undefined ? (
|
<EditPostForm postId={params.id} />
|
||||||
<Editor
|
|
||||||
clerkUserId={user.user.id}
|
|
||||||
post={post}
|
|
||||||
submitAction={submitAction}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<SpinningLoader className="animate-spin w-5 h-5" />
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
'use client';
|
import DisplayPost from '@/components/DisplayPost';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import Editor, { SubmitActionParams } from '@/components/Editor';
|
export const metadata: Metadata = {
|
||||||
import Post from '@/components/Post';
|
title: 'conveX | Post',
|
||||||
import PostReplies from '@/components/PostReplies';
|
};
|
||||||
import SpinningLoader from '@/components/SpinningLoader';
|
|
||||||
import { useRouter } from '@/hooks';
|
|
||||||
import { useUser } from '@clerk/nextjs';
|
|
||||||
import { useMutation, useQuery } from 'convex/react';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import { api } from '../../../../convex/_generated/api';
|
|
||||||
import { Id } from '../../../../convex/_generated/dataModel';
|
|
||||||
|
|
||||||
export default function PostPage({
|
export default function PostPage({
|
||||||
params,
|
params,
|
||||||
@@ -19,90 +12,13 @@ export default function PostPage({
|
|||||||
params: { id: string };
|
params: { id: string };
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
}) {
|
}) {
|
||||||
const replyRef = useRef<HTMLDivElement>(null);
|
|
||||||
const user = useUser();
|
|
||||||
const router = useRouter();
|
|
||||||
const post = useQuery(api.posts.getPostById, {
|
|
||||||
id: params.id as Id<'posts'>,
|
|
||||||
});
|
|
||||||
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
|
|
||||||
const isReply = searchParams.reply === 'true';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (replyRef.current !== null && isReply) {
|
|
||||||
replyRef.current.scrollIntoView();
|
|
||||||
}
|
|
||||||
}, [isReply, user]);
|
|
||||||
|
|
||||||
async function submitAction({
|
|
||||||
clerkUserId,
|
|
||||||
parentPostId,
|
|
||||||
post,
|
|
||||||
currentDoc,
|
|
||||||
}: SubmitActionParams) {
|
|
||||||
const result = await createOrUpdatePost({
|
|
||||||
parentPostId: parentPostId,
|
|
||||||
id: post?._id,
|
|
||||||
clerkUserId: clerkUserId,
|
|
||||||
content: currentDoc,
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case 'USER_NOT_FOUND':
|
|
||||||
case 'USER_NOT_AUTHORIZED':
|
|
||||||
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
|
|
||||||
throw Error('Unable to create post');
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isSignedIn === false) {
|
|
||||||
router.push('/login');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (post === 'POST_NOT_FOUND' || post === 'USER_FOR_POST_NOT_FOUND') {
|
|
||||||
// TODO: Return actual not found component
|
|
||||||
return <h1>Not Found</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col flex-1 w-full items-center">
|
<main className="flex flex-col flex-1 w-full items-center">
|
||||||
<div className="flex flex-col w-full max-w-4xl gap-4">
|
<div className="flex flex-col w-full max-w-4xl gap-4">
|
||||||
{post !== undefined && user.isLoaded ? (
|
<DisplayPost
|
||||||
<>
|
postId={params.id}
|
||||||
<div className="flex flex-col min-w-0 p-1 rounded-md border border-gray-600">
|
isReply={searchParams.reply === 'true'}
|
||||||
<Post post={post} limit={false} showReply={false} />
|
/>
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<PostReplies parentId={post._id} />
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full h-80 gap-4">
|
|
||||||
<div>
|
|
||||||
<Image
|
|
||||||
alt="user profile image"
|
|
||||||
src={user.user.imageUrl}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
className="rounded-full object-cover border-4 border-primary-accent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div ref={replyRef} className="flex w-full">
|
|
||||||
<Editor
|
|
||||||
clerkUserId={user.user.id}
|
|
||||||
parentPostId={post._id}
|
|
||||||
submitAction={submitAction}
|
|
||||||
autofocus={isReply}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex w-full justify-center gap-2">
|
|
||||||
<SpinningLoader className="animate-spin w-5 h-5" /> Loading post...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,57 +1,14 @@
|
|||||||
'use client';
|
import AddPostForm from '@/components/AddPostForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import Editor, { SubmitActionParams } from '@/components/Editor';
|
export const metadata: Metadata = {
|
||||||
import SpinningLoader from '@/components/SpinningLoader';
|
title: 'conveX | Add post',
|
||||||
import { useRouter } from '@/hooks';
|
};
|
||||||
import { useUser } from '@clerk/nextjs';
|
|
||||||
import { useMutation } from 'convex/react';
|
|
||||||
import { api } from '../../../../convex/_generated/api';
|
|
||||||
|
|
||||||
export default function AddPost() {
|
export default function AddPost() {
|
||||||
const user = useUser();
|
|
||||||
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
async function submitAction({
|
|
||||||
clerkUserId,
|
|
||||||
parentPostId,
|
|
||||||
post,
|
|
||||||
currentDoc,
|
|
||||||
}: SubmitActionParams) {
|
|
||||||
const result = await createOrUpdatePost({
|
|
||||||
parentPostId: parentPostId,
|
|
||||||
id: post?._id,
|
|
||||||
clerkUserId: clerkUserId,
|
|
||||||
content: currentDoc,
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case 'USER_NOT_FOUND':
|
|
||||||
case 'USER_NOT_AUTHORIZED':
|
|
||||||
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
|
|
||||||
throw Error('Unable to create post');
|
|
||||||
default:
|
|
||||||
'use server';
|
|
||||||
router.push('/');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isSignedIn === false) {
|
|
||||||
router.push('/login');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col flex-1 w-full items-center">
|
<main className="flex flex-col flex-1 w-full items-center">
|
||||||
{user.isLoaded ? (
|
<AddPostForm />
|
||||||
<Editor clerkUserId={user.user.id} submitAction={submitAction} />
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<SpinningLoader className="animate-spin w-5 h-5" />
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Privacy',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center flex-1">
|
||||||
|
<article className="flex flex-col w-full max-w-4xl gap-8">
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">Privacy Policy for conveX</h1>
|
||||||
|
<p>
|
||||||
|
At conveX, accessible from https://conve-x.stevanfreeborn.com, one
|
||||||
|
of our main priorities is the privacy of our visitors. This Privacy
|
||||||
|
Policy document contains types of information that is collected and
|
||||||
|
recorded by conveX and how we use it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you have additional questions or require more information about
|
||||||
|
our Privacy Policy, do not hesitate to contact us.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Log Files</h2>
|
||||||
|
<p>
|
||||||
|
conveX follows a standard procedure of using log files. These files
|
||||||
|
log visitors when they visit websites. All hosting companies do this
|
||||||
|
and a part of hosting services' analytics. The information
|
||||||
|
collected by log files include internet protocol (IP) addresses,
|
||||||
|
browser type, Internet Service Provider (ISP), date and time stamp,
|
||||||
|
referring/exit pages, and possibly the number of clicks. These are
|
||||||
|
not linked to any information that is personally identifiable. The
|
||||||
|
purpose of the information is for analyzing trends, administering
|
||||||
|
the site, tracking users' movement on the website, and
|
||||||
|
gathering demographic information. Our Privacy Policy was created
|
||||||
|
with the help of the{' '}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="https://www.privacypolicyonline.com/privacy-policy-generator/"
|
||||||
|
>
|
||||||
|
Privacy Policy Generator
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Cookies and Web Beacons</h2>
|
||||||
|
<p>
|
||||||
|
Like any other website, conveX uses "cookies". These
|
||||||
|
cookies are used to store information including visitors'
|
||||||
|
preferences, and the pages on the website that the visitor accessed
|
||||||
|
or visited. The information is used to optimize the users'
|
||||||
|
experience by customizing our web page content based on
|
||||||
|
visitors' browser type and/or other information.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For more general information on cookies, please read{' '}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="https://www.privacypolicyonline.com/what-are-cookies/"
|
||||||
|
>
|
||||||
|
the "Cookies" article from the Privacy Policy Generator
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Privacy Policies</h2>
|
||||||
|
<p>
|
||||||
|
You may consult this list to find the Privacy Policy for each of the
|
||||||
|
advertising partners of conveX.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Third-party ad servers or ad networks uses technologies like
|
||||||
|
cookies, JavaScript, or Web Beacons that are used in their
|
||||||
|
respective advertisements and links that appear on conveX, which are
|
||||||
|
sent directly to users' browser. They automatically receive
|
||||||
|
your IP address when this occurs. These technologies are used to
|
||||||
|
measure the effectiveness of their advertising campaigns and/or to
|
||||||
|
personalize the advertising content that you see on websites that
|
||||||
|
you visit.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Note that conveX has no access to or control over these cookies that
|
||||||
|
are used by third-party advertisers.
|
||||||
|
</p>
|
||||||
|
<h2 className="text-2xl font-bold">Third Party Privacy Policies</h2>
|
||||||
|
<p>
|
||||||
|
conveX's Privacy Policy does not apply to other advertisers or
|
||||||
|
websites. Thus, we are advising you to consult the respective
|
||||||
|
Privacy Policies of these third-party ad servers for more detailed
|
||||||
|
information. It may include their practices and instructions about
|
||||||
|
how to opt-out of certain options.{' '}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can choose to disable cookies through your individual browser
|
||||||
|
options. To know more detailed information about cookie management
|
||||||
|
with specific web browsers, it can be found at the browsers'
|
||||||
|
respective websites. What Are Cookies?
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Children's Information</h2>
|
||||||
|
<p>
|
||||||
|
Another part of our priority is adding protection for children while
|
||||||
|
using the internet. We encourage parents and guardians to observe,
|
||||||
|
participate in, and/or monitor and guide their online activity.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
conveX does not knowingly collect any Personal Identifiable
|
||||||
|
Information from children under the age of 13. If you think that
|
||||||
|
your child provided this kind of information on our website, we
|
||||||
|
strongly encourage you to contact us immediately and we will do our
|
||||||
|
best efforts to promptly remove such information from our records.
|
||||||
|
</p>
|
||||||
|
<h2 className="text-2xl font-bold">Online Privacy Policy Only</h2>
|
||||||
|
<p>
|
||||||
|
This Privacy Policy applies only to our online activities and is
|
||||||
|
valid for visitors to our website with regards to the information
|
||||||
|
that they shared and/or collect in conveX. This policy is not
|
||||||
|
applicable to any information collected offline or via channels
|
||||||
|
other than this website.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Consent</h2>
|
||||||
|
<p>
|
||||||
|
By using our website, you hereby consent to our Privacy Policy and
|
||||||
|
agree to its Terms and Conditions.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
|
import NotFound from '@/app/not-found';
|
||||||
import UserPosts from '@/components/UserPosts';
|
import UserPosts from '@/components/UserPosts';
|
||||||
import UserProfile from '@/components/UserProfile';
|
import UserProfile from '@/components/UserProfile';
|
||||||
import { getConvexClient } from '@/lib/convex';
|
import { getConvexClient } from '@/lib/convex';
|
||||||
|
import { Metadata } from 'next';
|
||||||
import { api } from '../../../../convex/_generated/api';
|
import { api } from '../../../../convex/_generated/api';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Profile',
|
||||||
|
};
|
||||||
|
|
||||||
export default async function UserProfilePage({
|
export default async function UserProfilePage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
@@ -14,8 +20,7 @@ export default async function UserProfilePage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (user === 'USER_NOT_FOUND') {
|
if (user === 'USER_NOT_FOUND') {
|
||||||
// TODO: Show real not found component
|
return <NotFound />;
|
||||||
return <div>Not Found</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import SearchResults from '@/components/SearchResults';
|
import SearchResults from '@/components/SearchResults';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Search',
|
||||||
|
};
|
||||||
|
|
||||||
export default function SearchPage({
|
export default function SearchPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
'use client';
|
import SignUpForm from '@/components/SignUpForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import { SignUp } from '@clerk/nextjs';
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Sign up',
|
||||||
|
};
|
||||||
|
|
||||||
export default function SignUpPage() {
|
export default function SignUpPage() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col flex-1 items-center">
|
<main className="flex flex-col flex-1 items-center">
|
||||||
<SignUp
|
<SignUpForm />
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
card: 'shadow-md dark:bg-secondary-gray [&_*:not(button):not(a):not(svg)]:dark:text-white [&_.cl-internal-b3fm6y]:hidden',
|
|
||||||
socialButtonsIconButton__github:
|
|
||||||
'hover:bg-gradient-to-br from-blue-600 via-purple-600 to-red-600 dark:border-white dark:border-opacity-10',
|
|
||||||
socialButtonsIconButton__google:
|
|
||||||
'hover:bg-gradient-to-r from-blue-500 via-green-500 to-yellow-500 dark:border-white dark:border-opacity-10',
|
|
||||||
socialButtonsIconButton__microsoft:
|
|
||||||
'hover:bg-gradient-to-r from-red-600 via-green-600 to-blue-600 dark:border-white dark:border-opacity-10',
|
|
||||||
dividerLine: 'dark:bg-white dark:bg-opacity-10',
|
|
||||||
formFieldInput: 'dark:bg-primary-gray dark:text-white',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'conveX | Terms',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TermsPage() {
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col items-center flex-1">
|
||||||
|
<article className="flex flex-col w-full max-w-4xl gap-8">
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">Terms and Conditions</h1>
|
||||||
|
<p>Welcome to conveX!</p>
|
||||||
|
<p>
|
||||||
|
These terms and conditions outline the rules and regulations for the
|
||||||
|
use of conveX's Website, located at
|
||||||
|
https://conve-x.stevanfreeborn.com.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By accessing this website we assume you accept these terms and
|
||||||
|
conditions. Do not continue to use conveX if you do not agree to
|
||||||
|
take all of the terms and conditions stated on this page.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The following terminology applies to these Terms and Conditions,
|
||||||
|
Privacy Statement and Disclaimer Notice and all Agreements:
|
||||||
|
"Client", "You" and "Your" refers to
|
||||||
|
you, the person log on this website and compliant to the Company’s
|
||||||
|
terms and conditions. "The Company",
|
||||||
|
"Ourselves", "We", "Our" and
|
||||||
|
"Us", refers to our Company. "Party",
|
||||||
|
"Parties", or "Us", refers to both the Client
|
||||||
|
and ourselves. All terms refer to the offer, acceptance and
|
||||||
|
consideration of payment necessary to undertake the process of our
|
||||||
|
assistance to the Client in the most appropriate manner for the
|
||||||
|
express purpose of meeting the Client’s needs in respect of
|
||||||
|
provision of the Company’s stated services, in accordance with and
|
||||||
|
subject to, prevailing law of Netherlands. Any use of the above
|
||||||
|
terminology or other words in the singular, plural, capitalization
|
||||||
|
and/or he/she or they, are taken as interchangeable and therefore as
|
||||||
|
referring to same. Our Terms and Conditions were created with the
|
||||||
|
help of the{' '}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="https://www.privacypolicyonline.com/terms-conditions-generator/"
|
||||||
|
>
|
||||||
|
Terms & Conditions Generator
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Cookies</h2>
|
||||||
|
<p>
|
||||||
|
We employ the use of cookies. By accessing conveX, you agreed to use
|
||||||
|
cookies in agreement with the conveX's Privacy Policy.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Most interactive websites use cookies to let us retrieve the
|
||||||
|
user's details for each visit. Cookies are used by our website
|
||||||
|
to enable the functionality of certain areas to make it easier for
|
||||||
|
people visiting our website. Some of our affiliate/advertising
|
||||||
|
partners may also use cookies.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">License</h2>
|
||||||
|
<p>
|
||||||
|
Unless otherwise stated, conveX and/or its licensors own the
|
||||||
|
intellectual property rights for all material on conveX. All
|
||||||
|
intellectual property rights are reserved. You may access this from
|
||||||
|
conveX for your own personal use subjected to restrictions set in
|
||||||
|
these terms and conditions.
|
||||||
|
</p>
|
||||||
|
<p>You must not:</p>
|
||||||
|
<ul className="list-disc ml-4">
|
||||||
|
<li>Republish material from conveX</li>
|
||||||
|
<li>Sell, rent or sub-license material from conveX</li>
|
||||||
|
<li>Reproduce, duplicate or copy material from conveX</li>
|
||||||
|
<li>Redistribute content from conveX</li>
|
||||||
|
</ul>
|
||||||
|
<p>This Agreement shall begin on the date hereof.</p>
|
||||||
|
<p>
|
||||||
|
Parts of this website offer an opportunity for users to post and
|
||||||
|
exchange opinions and information in certain areas of the website.
|
||||||
|
conveX does not filter, edit, publish or review Comments prior to
|
||||||
|
their presence on the website. Comments do not reflect the views and
|
||||||
|
opinions of conveX,its agents and/or affiliates. Comments reflect
|
||||||
|
the views and opinions of the person who post their views and
|
||||||
|
opinions. To the extent permitted by applicable laws, conveX shall
|
||||||
|
not be liable for the Comments or for any liability, damages or
|
||||||
|
expenses caused and/or suffered as a result of any use of and/or
|
||||||
|
posting of and/or appearance of the Comments on this website.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
conveX reserves the right to monitor all Comments and to remove any
|
||||||
|
Comments which can be considered inappropriate, offensive or causes
|
||||||
|
breach of these Terms and Conditions.
|
||||||
|
</p>
|
||||||
|
<p>You warrant and represent that:</p>
|
||||||
|
<ul className="list-disc ml-4">
|
||||||
|
<li>
|
||||||
|
You are entitled to post the Comments on our website and have all
|
||||||
|
necessary licenses and consents to do so;
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The Comments do not invade any intellectual property right,
|
||||||
|
including without limitation copyright, patent or trademark of any
|
||||||
|
third party;
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The Comments do not contain any defamatory, libelous, offensive,
|
||||||
|
indecent or otherwise unlawful material which is an invasion of
|
||||||
|
privacy
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The Comments will not be used to solicit or promote business or
|
||||||
|
custom or present commercial activities or unlawful activity.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
You hereby grant conveX a non-exclusive license to use, reproduce,
|
||||||
|
edit and authorize others to use, reproduce and edit any of your
|
||||||
|
Comments in any and all forms, formats or media.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Hyperlinking to our Content</h2>
|
||||||
|
<p>
|
||||||
|
The following organizations may link to our Website without prior
|
||||||
|
written approval:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc ml-4">
|
||||||
|
<li>Government agencies;</li>
|
||||||
|
<li>Search engines;</li>
|
||||||
|
<li>News organizations;</li>
|
||||||
|
<li>
|
||||||
|
Online directory distributors may link to our Website in the same
|
||||||
|
manner as they hyperlink to the Websites of other listed
|
||||||
|
businesses; and
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
System wide Accredited Businesses except soliciting non-profit
|
||||||
|
organizations, charity shopping malls, and charity fundraising
|
||||||
|
groups which may not hyperlink to our Web site.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
These organizations may link to our home page, to publications or to
|
||||||
|
other Website information so long as the link: (a) is not in any way
|
||||||
|
deceptive; (b) does not falsely imply sponsorship, endorsement or
|
||||||
|
approval of the linking party and its products and/or services; and
|
||||||
|
(c) fits within the context of the linking party's site.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We may consider and approve other link requests from the following
|
||||||
|
types of organizations:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc ml-4">
|
||||||
|
<li>
|
||||||
|
commonly-known consumer and/or business information sources;
|
||||||
|
</li>
|
||||||
|
<li>dot.com community sites;</li>
|
||||||
|
<li>associations or other groups representing charities;</li>
|
||||||
|
<li>online directory distributors;</li>
|
||||||
|
<li>internet portals;</li>
|
||||||
|
<li>accounting, law and consulting firms; and</li>
|
||||||
|
<li>educational institutions and trade associations.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
We will approve link requests from these organizations if we decide
|
||||||
|
that: (a) the link would not make us look unfavorably to ourselves
|
||||||
|
or to our accredited businesses; (b) the organization does not have
|
||||||
|
any negative records with us; (c) the benefit to us from the
|
||||||
|
visibility of the hyperlink compensates the absence of conveX; and
|
||||||
|
(d) the link is in the context of general resource information.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
These organizations may link to our home page so long as the link:
|
||||||
|
(a) is not in any way deceptive; (b) does not falsely imply
|
||||||
|
sponsorship, endorsement or approval of the linking party and its
|
||||||
|
products or services; and (c) fits within the context of the linking
|
||||||
|
party's site.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you are one of the organizations listed in paragraph 2 above and
|
||||||
|
are interested in linking to our website, you must inform us by
|
||||||
|
sending an e-mail to conveX. Please include your name, your
|
||||||
|
organization name, contact information as well as the URL of your
|
||||||
|
site, a list of any URLs from which you intend to link to our
|
||||||
|
Website, and a list of the URLs on our site to which you would like
|
||||||
|
to link. Wait 2-3 weeks for a response.
|
||||||
|
</p>
|
||||||
|
<p>Approved organizations may hyperlink to our Website as follows:</p>
|
||||||
|
<ul className="list-disc ml-4">
|
||||||
|
<li>By use of our corporate name; or</li>
|
||||||
|
<li>By use of the uniform resource locator being linked to; or</li>
|
||||||
|
<li>
|
||||||
|
By use of any other description of our Website being linked to
|
||||||
|
that makes sense within the context and format of content on the
|
||||||
|
linking party's site.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
No use of conveX's logo or other artwork will be allowed for
|
||||||
|
linking absent a trademark license agreement.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">iFrames</h2>
|
||||||
|
<p>
|
||||||
|
Without prior approval and written permission, you may not create
|
||||||
|
frames around our Webpages that alter in any way the visual
|
||||||
|
presentation or appearance of our Website.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold">Content Liability</h2>
|
||||||
|
<p>
|
||||||
|
We shall not be hold responsible for any content that appears on
|
||||||
|
your Website. You agree to protect and defend us against all claims
|
||||||
|
that is rising on your Website. No link(s) should appear on any
|
||||||
|
Website that may be interpreted as libelous, obscene or criminal, or
|
||||||
|
which infringes, otherwise violates, or advocates the infringement
|
||||||
|
or other violation of, any third party rights.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Reservation of Rights</h2>
|
||||||
|
<p>
|
||||||
|
We reserve the right to request that you remove all links or any
|
||||||
|
particular link to our Website. You approve to immediately remove
|
||||||
|
all links to our Website upon request. We also reserve the right to
|
||||||
|
amen these terms and conditions and it's linking policy at any
|
||||||
|
time. By continuously linking to our Website, you agree to be bound
|
||||||
|
to and follow these linking terms and conditions.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">
|
||||||
|
Removal of links from our website
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
If you find any link on our Website that is offensive for any
|
||||||
|
reason, you are free to contact and inform us any moment. We will
|
||||||
|
consider requests to remove links but we are not obligated to or so
|
||||||
|
or to respond to you directly.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We do not ensure that the information on this website is correct, we
|
||||||
|
do not warrant its completeness or accuracy; nor do we promise to
|
||||||
|
ensure that the website remains available or that the material on
|
||||||
|
the website is kept up to date.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-2xl font-bold">Disclaimer</h2>
|
||||||
|
<p>
|
||||||
|
To the maximum extent permitted by applicable law, we exclude all
|
||||||
|
representations, warranties and conditions relating to our website
|
||||||
|
and the use of this website. Nothing in this disclaimer will:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc ml-4">
|
||||||
|
<li>
|
||||||
|
limit or exclude our or your liability for death or personal
|
||||||
|
injury;
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
limit or exclude our or your liability for fraud or fraudulent
|
||||||
|
misrepresentation;
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
limit any of our or your liabilities in any way that is not
|
||||||
|
permitted under applicable law; or
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
exclude any of our or your liabilities that may not be excluded
|
||||||
|
under applicable law.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
The limitations and prohibitions of liability set in this Section
|
||||||
|
and elsewhere in this disclaimer: (a) are subject to the preceding
|
||||||
|
paragraph; and (b) govern all liabilities arising under the
|
||||||
|
disclaimer, including liabilities arising in contract, in tort and
|
||||||
|
for breach of statutory duty.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
As long as the website and the information and services on the
|
||||||
|
website are provided free of charge, we will not be liable for any
|
||||||
|
loss or damage of any nature.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ export type UserDto = {
|
|||||||
clerkUserId: string;
|
clerkUserId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PostWithUserDto = Omit<Doc<'posts'>, 'contentText'> & {
|
export type PostDto = Omit<Doc<'posts'>, 'contentText'>;
|
||||||
|
|
||||||
|
export type PostWithUserDto = PostDto & {
|
||||||
user: UserDto;
|
user: UserDto;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { UserProfile } from '@clerk/nextjs';
|
||||||
|
|
||||||
|
export default function AccountForm() {
|
||||||
|
return (
|
||||||
|
<UserProfile
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
card: 'sm:pb-4 shadow-md dark:bg-secondary-gray [&_*:not(button):not(a)]:dark:text-white [&_.cl-internal-b3fm6y]:hidden',
|
||||||
|
navbar: 'hidden',
|
||||||
|
navbarMobileMenuRow: 'hidden',
|
||||||
|
formFieldInput: 'dark:bg-primary-gray dark:text-white',
|
||||||
|
profileSectionTitle: 'dark:border-white dark:border-opacity-10',
|
||||||
|
breadcrumbsItem: 'dark:text-white',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useUser } from '@clerk/nextjs';
|
||||||
|
import { useMutation } from 'convex/react';
|
||||||
|
import { useRouter } from '@/hooks';
|
||||||
|
import { api } from '../../convex/_generated/api';
|
||||||
|
import Editor, { SubmitActionParams } from './Editor';
|
||||||
|
import SpinningLoader from './SpinningLoader';
|
||||||
|
|
||||||
|
export default function AddPostForm() {
|
||||||
|
const user = useUser();
|
||||||
|
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function submitAction({
|
||||||
|
clerkUserId,
|
||||||
|
parentPostId,
|
||||||
|
post,
|
||||||
|
currentDoc,
|
||||||
|
}: SubmitActionParams) {
|
||||||
|
const result = await createOrUpdatePost({
|
||||||
|
parentPostId: parentPostId,
|
||||||
|
id: post?._id,
|
||||||
|
clerkUserId: clerkUserId,
|
||||||
|
content: currentDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case 'USER_NOT_FOUND':
|
||||||
|
case 'USER_NOT_AUTHORIZED':
|
||||||
|
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
|
||||||
|
throw Error('Unable to create post');
|
||||||
|
default:
|
||||||
|
'use server';
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isSignedIn === false) {
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isLoaded) {
|
||||||
|
return <Editor clerkUserId={user.user.id} submitAction={submitAction} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<SpinningLoader className="animate-spin w-5 h-5" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import NotFound from '@/app/not-found';
|
||||||
|
import { useRouter } from '@/hooks';
|
||||||
|
import { useUser } from '@clerk/nextjs';
|
||||||
|
import { useMutation, useQuery } from 'convex/react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { api } from '../../convex/_generated/api';
|
||||||
|
import { Id } from '../../convex/_generated/dataModel';
|
||||||
|
import Editor, { SubmitActionParams } from './Editor';
|
||||||
|
import Post from './Post';
|
||||||
|
import PostReplies from './PostReplies';
|
||||||
|
import SpinningLoader from './SpinningLoader';
|
||||||
|
|
||||||
|
export default function DisplayPost({
|
||||||
|
postId,
|
||||||
|
isReply,
|
||||||
|
}: {
|
||||||
|
postId: string;
|
||||||
|
isReply: boolean;
|
||||||
|
}) {
|
||||||
|
const replyRef = useRef<HTMLDivElement>(null);
|
||||||
|
const user = useUser();
|
||||||
|
const router = useRouter();
|
||||||
|
const post = useQuery(api.posts.getPostById, {
|
||||||
|
id: postId as Id<'posts'>,
|
||||||
|
});
|
||||||
|
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (replyRef.current !== null && isReply) {
|
||||||
|
replyRef.current.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [isReply, user]);
|
||||||
|
|
||||||
|
async function submitAction({
|
||||||
|
clerkUserId,
|
||||||
|
parentPostId,
|
||||||
|
post,
|
||||||
|
currentDoc,
|
||||||
|
}: SubmitActionParams) {
|
||||||
|
const result = await createOrUpdatePost({
|
||||||
|
parentPostId: parentPostId,
|
||||||
|
id: post?._id,
|
||||||
|
clerkUserId: clerkUserId,
|
||||||
|
content: currentDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case 'USER_NOT_FOUND':
|
||||||
|
case 'USER_NOT_AUTHORIZED':
|
||||||
|
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
|
||||||
|
throw Error('Unable to create post');
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isSignedIn === false) {
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post === 'POST_NOT_FOUND' || post === 'USER_FOR_POST_NOT_FOUND') {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post !== undefined && user.isLoaded) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col min-w-0 p-1 rounded-md border border-gray-600">
|
||||||
|
<Post post={post} limit={false} showReply={false} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<PostReplies parentId={post._id} />
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full h-80 gap-4">
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
alt="user profile image"
|
||||||
|
src={user.user.imageUrl}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="rounded-full object-cover border-4 border-primary-accent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div ref={replyRef} className="flex w-full">
|
||||||
|
<Editor
|
||||||
|
clerkUserId={user.user.id}
|
||||||
|
parentPostId={post._id}
|
||||||
|
submitAction={submitAction}
|
||||||
|
autofocus={isReply}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full justify-center gap-2">
|
||||||
|
<SpinningLoader className="animate-spin w-5 h-5" /> Loading post...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import NotFound from '@/app/not-found';
|
||||||
|
import { useRouter } from '@/hooks';
|
||||||
|
import { useUser } from '@clerk/nextjs';
|
||||||
|
import { useMutation, useQuery } from 'convex/react';
|
||||||
|
import { api } from '../../convex/_generated/api';
|
||||||
|
import { Id } from '../../convex/_generated/dataModel';
|
||||||
|
import Editor, { SubmitActionParams } from './Editor';
|
||||||
|
import SpinningLoader from './SpinningLoader';
|
||||||
|
|
||||||
|
export default function EditPostForm({ postId }: { postId: string }) {
|
||||||
|
const user = useUser();
|
||||||
|
const post = useQuery(api.posts.getPostById, {
|
||||||
|
id: postId as Id<'posts'>,
|
||||||
|
});
|
||||||
|
const createOrUpdatePost = useMutation(api.posts.createOrUpdatePost);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (user.isSignedIn === false) {
|
||||||
|
router.push('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post === 'POST_NOT_FOUND' || post === 'USER_FOR_POST_NOT_FOUND') {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAction({
|
||||||
|
clerkUserId,
|
||||||
|
parentPostId,
|
||||||
|
post,
|
||||||
|
currentDoc,
|
||||||
|
}: SubmitActionParams) {
|
||||||
|
const result = await createOrUpdatePost({
|
||||||
|
parentPostId: parentPostId,
|
||||||
|
id: post?._id,
|
||||||
|
clerkUserId: clerkUserId,
|
||||||
|
content: currentDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case 'USER_NOT_FOUND':
|
||||||
|
case 'USER_NOT_AUTHORIZED':
|
||||||
|
case 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER':
|
||||||
|
throw Error('Unable to update post');
|
||||||
|
default:
|
||||||
|
router.push(`/posts/${post?._id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isLoaded && post !== undefined) {
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
clerkUserId={user.user.id}
|
||||||
|
post={post}
|
||||||
|
submitAction={submitAction}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<SpinningLoader className="animate-spin w-5 h-5" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { PostDto } from '@/app/types';
|
||||||
import { useCodeMirror, useRouter } from '@/hooks';
|
import { useCodeMirror, useRouter } from '@/hooks';
|
||||||
import { basicDark } from '@/lib/codemirror';
|
import { basicDark } from '@/lib/codemirror';
|
||||||
import { indentWithTab } from '@codemirror/commands';
|
import { indentWithTab } from '@codemirror/commands';
|
||||||
@@ -9,13 +10,13 @@ import { Compartment, EditorState, Text } from '@codemirror/state';
|
|||||||
import { EditorView, keymap } from '@codemirror/view';
|
import { EditorView, keymap } from '@codemirror/view';
|
||||||
import { basicSetup } from 'codemirror';
|
import { basicSetup } from 'codemirror';
|
||||||
import { FormEvent, useEffect, useState } from 'react';
|
import { FormEvent, useEffect, useState } from 'react';
|
||||||
import { Doc, Id } from '../../convex/_generated/dataModel';
|
import { Id } from '../../convex/_generated/dataModel';
|
||||||
import PostContent from './PostContent';
|
import PostContent from './PostContent';
|
||||||
|
|
||||||
export type SubmitActionParams = {
|
export type SubmitActionParams = {
|
||||||
clerkUserId: string;
|
clerkUserId: string;
|
||||||
parentPostId?: Id<'posts'>;
|
parentPostId?: Id<'posts'>;
|
||||||
post?: Doc<'posts'>;
|
post?: PostDto;
|
||||||
currentDoc: string[];
|
currentDoc: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export default function Editor({
|
|||||||
}: {
|
}: {
|
||||||
clerkUserId: string;
|
clerkUserId: string;
|
||||||
parentPostId?: Id<'posts'>;
|
parentPostId?: Id<'posts'>;
|
||||||
post?: Doc<'posts'>;
|
post?: PostDto;
|
||||||
submitAction: ({
|
submitAction: ({
|
||||||
clerkUserId,
|
clerkUserId,
|
||||||
parentPostId,
|
parentPostId,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function Footer() {
|
|||||||
<Link
|
<Link
|
||||||
href="https://github.com/StevanFreeborn/conve-x"
|
href="https://github.com/StevanFreeborn/conve-x"
|
||||||
className="hover:text-primary-orange"
|
className="hover:text-primary-orange"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
Code
|
Code
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SignIn } from '@clerk/nextjs';
|
||||||
|
|
||||||
|
export default function LoginForm() {
|
||||||
|
return (
|
||||||
|
<SignIn
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
card: 'shadow-md dark:bg-secondary-gray [&_*:not(button):not(a)]:dark:text-white [&_.cl-internal-b3fm6y]:hidden',
|
||||||
|
socialButtonsIconButton__github:
|
||||||
|
'hover:bg-gradient-to-br from-blue-600 via-purple-600 to-red-600 dark:border-white dark:border-opacity-10',
|
||||||
|
socialButtonsIconButton__google:
|
||||||
|
'hover:bg-gradient-to-r from-blue-500 via-green-500 to-yellow-500 dark:border-white dark:border-opacity-10',
|
||||||
|
socialButtonsIconButton__microsoft:
|
||||||
|
'hover:bg-gradient-to-r from-red-600 via-green-600 to-blue-600 dark:border-white dark:border-opacity-10',
|
||||||
|
dividerLine: 'dark:bg-white dark:bg-opacity-10',
|
||||||
|
formFieldInput: 'dark:bg-primary-gray dark:text-white',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SignUp } from '@clerk/nextjs';
|
||||||
|
|
||||||
|
export default function SignUpForm() {
|
||||||
|
return (
|
||||||
|
<SignUp
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
card: 'shadow-md dark:bg-secondary-gray [&_*:not(button):not(a):not(svg)]:dark:text-white [&_.cl-internal-b3fm6y]:hidden',
|
||||||
|
socialButtonsIconButton__github:
|
||||||
|
'hover:bg-gradient-to-br from-blue-600 via-purple-600 to-red-600 dark:border-white dark:border-opacity-10',
|
||||||
|
socialButtonsIconButton__google:
|
||||||
|
'hover:bg-gradient-to-r from-blue-500 via-green-500 to-yellow-500 dark:border-white dark:border-opacity-10',
|
||||||
|
socialButtonsIconButton__microsoft:
|
||||||
|
'hover:bg-gradient-to-r from-red-600 via-green-600 to-blue-600 dark:border-white dark:border-opacity-10',
|
||||||
|
dividerLine: 'dark:bg-white dark:bg-opacity-10',
|
||||||
|
formFieldInput: 'dark:bg-primary-gray dark:text-white',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { authMiddleware } from '@clerk/nextjs';
|
import { authMiddleware } from '@clerk/nextjs';
|
||||||
|
|
||||||
export default authMiddleware({
|
export default authMiddleware({
|
||||||
publicRoutes: [],
|
publicRoutes: ['/about', '/contact', '/terms', '/privacy'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user