Merge pull request #42 from StevanFreeborn/stevanfreeborn/feat/add-finishing-touches

feat: add missing pages
This commit is contained in:
Stevan Freeborn
2023-09-16 17:29:14 -05:00
committed by GitHub
31 changed files with 1068 additions and 260 deletions
+6 -1
View File
@@ -3,6 +3,11 @@
"extends": ["next/core-web-vitals", "plugin:prettier/recommended"],
"rules": {
"no-console": "warn",
"prettier/prettier": "error"
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}
+1
View File
@@ -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
- [Tailwind](https://tailwindcss.com/) - Styling
- [Vercel](https://vercel.com/) - Hosting
- [Codemirror](https://codemirror.net/) - Editor
+114
View File
@@ -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>
);
}
+6 -14
View File
@@ -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() {
return (
<main className="flex flex-col items-center">
<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',
},
}}
/>
<AccountForm />
</main>
);
}
+49
View File
@@ -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>
);
}
+28
View File
@@ -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&apos;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

+5
View File
@@ -1,4 +1,9 @@
import FollowingPosts from '@/components/FollowingPosts';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'conveX | Following',
};
export default function FollowingPage() {
return (
+37
View File
@@ -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&apos;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
View File
@@ -10,7 +10,8 @@ import './globals.css';
const inter = Inter({ subsets: ['latin'] });
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({
+6 -17
View File
@@ -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() {
return (
<main className="flex flex-col flex-1 items-center">
<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',
},
}}
/>
<LoginForm />
</main>
);
}
+24
View File
@@ -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>
);
}
+5
View File
@@ -1,4 +1,9 @@
import AllPosts from '@/components/AllPosts';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'conveX | Home',
};
export default function Home() {
return (
+6 -61
View File
@@ -1,69 +1,14 @@
'use client';
import EditPostForm from '@/components/EditPostForm';
import { Metadata } from 'next';
import Editor, { SubmitActionParams } from '@/components/Editor';
import SpinningLoader from '@/components/SpinningLoader';
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 const metadata: Metadata = {
title: 'conveX | Edit post',
};
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 (
<main className="flex flex-col flex-1 w-full items-center">
{user.isLoaded && post !== undefined ? (
<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>
)}
<EditPostForm postId={params.id} />
</main>
);
}
+9 -93
View File
@@ -1,16 +1,9 @@
'use client';
import DisplayPost from '@/components/DisplayPost';
import { Metadata } from 'next';
import Editor, { SubmitActionParams } from '@/components/Editor';
import Post from '@/components/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 const metadata: Metadata = {
title: 'conveX | Post',
};
export default function PostPage({
params,
@@ -19,90 +12,13 @@ export default function PostPage({
params: { id: string };
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 (
<main className="flex flex-col flex-1 w-full items-center">
<div className="flex flex-col w-full max-w-4xl gap-4">
{post !== undefined && user.isLoaded ? (
<>
<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>
</>
) : (
<div className="flex w-full justify-center gap-2">
<SpinningLoader className="animate-spin w-5 h-5" /> Loading post...
</div>
)}
<DisplayPost
postId={params.id}
isReply={searchParams.reply === 'true'}
/>
</div>
</main>
);
+6 -49
View File
@@ -1,57 +1,14 @@
'use client';
import AddPostForm from '@/components/AddPostForm';
import { Metadata } from 'next';
import Editor, { SubmitActionParams } from '@/components/Editor';
import SpinningLoader from '@/components/SpinningLoader';
import { useRouter } from '@/hooks';
import { useUser } from '@clerk/nextjs';
import { useMutation } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
export const metadata: Metadata = {
title: 'conveX | Add post',
};
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 (
<main className="flex flex-col flex-1 w-full items-center">
{user.isLoaded ? (
<Editor clerkUserId={user.user.id} submitAction={submitAction} />
) : (
<div className="flex gap-2 items-center">
<SpinningLoader className="animate-spin w-5 h-5" />
Loading...
</div>
)}
<AddPostForm />
</main>
);
}
+136
View File
@@ -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&apos; 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&apos; 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 &quot;cookies&quot;. These
cookies are used to store information including visitors&apos;
preferences, and the pages on the website that the visitor accessed
or visited. The information is used to optimize the users&apos;
experience by customizing our web page content based on
visitors&apos; 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 &quot;Cookies&quot; 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&apos; 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&apos;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&apos;
respective websites. What Are Cookies?
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="text-2xl font-bold">Children&apos;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>
);
}
+7 -2
View File
@@ -1,8 +1,14 @@
import NotFound from '@/app/not-found';
import UserPosts from '@/components/UserPosts';
import UserProfile from '@/components/UserProfile';
import { getConvexClient } from '@/lib/convex';
import { Metadata } from 'next';
import { api } from '../../../../convex/_generated/api';
export const metadata: Metadata = {
title: 'conveX | Profile',
};
export default async function UserProfilePage({
params,
}: {
@@ -14,8 +20,7 @@ export default async function UserProfilePage({
});
if (user === 'USER_NOT_FOUND') {
// TODO: Show real not found component
return <div>Not Found</div>;
return <NotFound />;
}
return (
+5
View File
@@ -1,4 +1,9 @@
import SearchResults from '@/components/SearchResults';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'conveX | Search',
};
export default function SearchPage({
searchParams,
+6 -17
View File
@@ -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() {
return (
<main className="flex flex-col flex-1 items-center">
<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',
},
}}
/>
<SignUpForm />
</main>
);
}
+305
View File
@@ -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&apos;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:
&quot;Client&quot;, &quot;You&quot; and &quot;Your&quot; refers to
you, the person log on this website and compliant to the Companys
terms and conditions. &quot;The Company&quot;,
&quot;Ourselves&quot;, &quot;We&quot;, &quot;Our&quot; and
&quot;Us&quot;, refers to our Company. &quot;Party&quot;,
&quot;Parties&quot;, or &quot;Us&quot;, 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 Clients needs in respect of
provision of the Companys 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&apos;s Privacy Policy.
</p>
<p>
Most interactive websites use cookies to let us retrieve the
user&apos;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&apos;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&apos;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&apos;s site.
</li>
</ul>
<p>
No use of conveX&apos;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&apos;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>
);
}
+3 -1
View File
@@ -8,6 +8,8 @@ export type UserDto = {
clerkUserId: string;
};
export type PostWithUserDto = Omit<Doc<'posts'>, 'contentText'> & {
export type PostDto = Omit<Doc<'posts'>, 'contentText'>;
export type PostWithUserDto = PostDto & {
user: UserDto;
};
+20
View File
@@ -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',
},
}}
/>
);
}
+55
View File
@@ -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>
);
}
+106
View File
@@ -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>
);
}
+69
View File
@@ -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>
);
}
+4 -3
View File
@@ -1,5 +1,6 @@
'use client';
import { PostDto } from '@/app/types';
import { useCodeMirror, useRouter } from '@/hooks';
import { basicDark } from '@/lib/codemirror';
import { indentWithTab } from '@codemirror/commands';
@@ -9,13 +10,13 @@ import { Compartment, EditorState, Text } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { FormEvent, useEffect, useState } from 'react';
import { Doc, Id } from '../../convex/_generated/dataModel';
import { Id } from '../../convex/_generated/dataModel';
import PostContent from './PostContent';
export type SubmitActionParams = {
clerkUserId: string;
parentPostId?: Id<'posts'>;
post?: Doc<'posts'>;
post?: PostDto;
currentDoc: string[];
};
@@ -28,7 +29,7 @@ export default function Editor({
}: {
clerkUserId: string;
parentPostId?: Id<'posts'>;
post?: Doc<'posts'>;
post?: PostDto;
submitAction: ({
clerkUserId,
parentPostId,
+1
View File
@@ -20,6 +20,7 @@ export default function Footer() {
<Link
href="https://github.com/StevanFreeborn/conve-x"
className="hover:text-primary-orange"
target="_blank"
>
Code
</Link>
+23
View File
@@ -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',
},
}}
/>
);
}
+23
View File
@@ -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
View File
@@ -1,7 +1,7 @@
import { authMiddleware } from '@clerk/nextjs';
export default authMiddleware({
publicRoutes: [],
publicRoutes: ['/about', '/contact', '/terms', '/privacy'],
});
export const config = {