diff --git a/.eslintrc.json b/.eslintrc.json index 1568c7f..a87c80b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,47 @@ { - "extends": [ - "next/core-web-vitals", - "standard", - "plugin:tailwindcss/recommended", - "prettier" - ] + "extends": [ + "next/core-web-vitals", + "next/typescript", + "standard", + "plugin:tailwindcss/recommended", + "prettier" + ], + "plugins": ["import"], + "rules": { + "import/order": [ + "error", + { + "groups": [ + "builtin", // Built-in types are first + "external", // External libraries + "internal", // Internal modules + ["parent", "sibling"], // Parent and sibling types can be mingled together + "index", // Then the index file + "object" // Object imports + ], + "newlines-between": "always", + "pathGroups": [ + { + "pattern": "@app/**", + "group": "external", + "position": "after" + } + ], + "pathGroupsExcludedImportTypes": ["builtin"], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ] + }, + "ignorePatterns": ["components/ui/**"], + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "rules": { + "no-undef": "off" + } + } + ] } diff --git a/.gitignore b/.gitignore index fd3dbb5..d32cc78 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,12 @@ # dependencies /node_modules /.pnp -.pnp.js -.yarn/install-state.gz +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions # testing /coverage @@ -25,8 +29,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# local env files -.env*.local +# env files (can opt-in for committing if needed) +.env* # vercel .vercel diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24eca33 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,40 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.addMissingImports": "explicit" + }, + "prettier.tabWidth": 2, + "prettier.useTabs": false, + "prettier.semi": true, + "prettier.singleQuote": false, + "prettier.jsxSingleQuote": false, + "prettier.trailingComma": "es5", + "prettier.arrowParens": "always", + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.vscode": true, + "**/.next": true, + "**/node_modules": true, + "next-env.d.ts": true + } +} diff --git a/README.md b/README.md index c403366..e215bc4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started @@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More @@ -27,10 +27,10 @@ To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index d0af8b5..2945174 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,7 +1,34 @@ -import React from 'react' +import Image from "next/image"; -const Layout = ({ children }: { children: React.ReactNode }) => { - return
{children}
-} +import SocialAuthForm from "@/components/forms/SocialAuthForm"; -export default Layout +const AuthLayout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( +
+
+
+
+

Join StackFlow

+

+ To get your questions answered +

+
+ stackflow logo +
+ {children} + +
+
+ ); +}; +export default AuthLayout; diff --git a/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/sign-in/[[...sign-in]]/page.tsx deleted file mode 100644 index d80106f..0000000 --- a/app/(auth)/sign-in/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignIn } from '@clerk/nextjs' - -export default function Page() { - return -} diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx new file mode 100644 index 0000000..fdb892b --- /dev/null +++ b/app/(auth)/sign-in/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { AuthForm } from "@/components/forms/AuthForm"; +import { SignInSchema } from "@/lib/validations"; + +const SignIn = () => { + return ( + Promise.resolve({ success: true, data })} + /> + ); +}; +export default SignIn; diff --git a/app/(auth)/sign-up/[[...sign-up]]/page.tsx b/app/(auth)/sign-up/[[...sign-up]]/page.tsx deleted file mode 100644 index 45a745f..0000000 --- a/app/(auth)/sign-up/[[...sign-up]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignUp } from '@clerk/nextjs' - -export default function Page() { - return -} diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx new file mode 100644 index 0000000..aa1f500 --- /dev/null +++ b/app/(auth)/sign-up/page.tsx @@ -0,0 +1,15 @@ +'use client' +import { AuthForm } from "@/components/forms/AuthForm"; +import { SignUpSchema } from "@/lib/validations"; + +const SignUp = () => { + return ( + Promise.resolve({ success: true, data })} + /> + ); +}; +export default SignUp; diff --git a/app/(root)/(home)/page.tsx b/app/(root)/(home)/page.tsx deleted file mode 100644 index 6ac35f4..0000000 --- a/app/(root)/(home)/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import QuestionCard from '@/components/cards/QuestionCard' -import HomeFilter from '@/components/home/HomeFilters' -import NoResult from '@/components/shared/NoResult' -import LocalSearch from '@/components/shared/search/LocalSearch' -import SearchFilter from '@/components/shared/search/SearchFilter' -import { Button } from '@/components/ui/button' -import { HomePageFilters } from '@/constants/filters' -import { getQuestions } from '@/lib/actions/question.action' -import Link from 'next/link' - -const Home = async () => { - const result = await getQuestions({}) - - return ( - <> -
-

All Questions

- - - -
-
- - -
- -
- {result?.questions?.length! > 0 ? ( - result?.questions.map((question) => ( - - )) - ) : ( - - )} -
- - ) -} - -export default Home diff --git a/app/(root)/ask-a-question/page.tsx b/app/(root)/ask-a-question/page.tsx deleted file mode 100644 index ce4c3ec..0000000 --- a/app/(root)/ask-a-question/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Question from '@/components/forms/Question' -import { getUserById } from '@/lib/actions/user.action' -import { auth } from '@clerk/nextjs' -import { redirect } from 'next/navigation' - -const AskAQuestion = async () => { - const { userId } = auth() - - if (!userId || userId === null || userId === undefined) redirect('/sign-in') - - const mongoUser = await getUserById({ userId }) - - return ( -
-

Ask a question

-
- -
-
- ) -} - -export default AskAQuestion diff --git a/app/(root)/ask-question/page.tsx b/app/(root)/ask-question/page.tsx new file mode 100644 index 0000000..22e45f1 --- /dev/null +++ b/app/(root)/ask-question/page.tsx @@ -0,0 +1,4 @@ +const AskAQuestion = () => { + return
AskAQuestion
; +}; +export default AskAQuestion; diff --git a/app/(root)/collection/page.tsx b/app/(root)/collection/page.tsx new file mode 100644 index 0000000..ce08b0d --- /dev/null +++ b/app/(root)/collection/page.tsx @@ -0,0 +1,6 @@ +const Collections = () => { + return ( +
Collections
+ ) +} +export default Collections \ No newline at end of file diff --git a/app/(root)/community/page.tsx b/app/(root)/community/page.tsx index cbf1083..9e49309 100644 --- a/app/(root)/community/page.tsx +++ b/app/(root)/community/page.tsx @@ -1,48 +1,6 @@ -import UserCard from '@/components/cards/UserCard' -import NoResult from '@/components/shared/NoResult' -import LocalSearch from '@/components/shared/search/LocalSearch' -import SearchFilter from '@/components/shared/search/SearchFilter' -import { UserFilters } from '@/constants/filters' -import { getUsers } from '@/lib/actions/user.action' - -const CommunityPage = async () => { - const result = await getUsers({}) - - return ( - <> -

All Users

-
- - -
-
- {result.users.length > 0 ? ( - result.users.map((user) => ( - - )) - ) : ( - - )} -
- - ) +const Community = () => { + return ( +
Community
+ ) } - -export default CommunityPage +export default Community \ No newline at end of file diff --git a/app/(root)/jobs/page.tsx b/app/(root)/jobs/page.tsx new file mode 100644 index 0000000..24965fc --- /dev/null +++ b/app/(root)/jobs/page.tsx @@ -0,0 +1,6 @@ +const FindJobs = () => { + return ( +
FindJobs
+ ) +} +export default FindJobs \ No newline at end of file diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx index 0251092..ed474d7 100644 --- a/app/(root)/layout.tsx +++ b/app/(root)/layout.tsx @@ -1,21 +1,14 @@ -import Navbar from '@/components/shared/navbar/Navbar' -import LeftSidebar from '@/components/shared/sidebar/LeftSidebar' -import RightSidebar from '@/components/shared/sidebar/RightSidebar' -import React from 'react' +import Navbar from "@/components/navigation/navbar"; -const Layout = ({ children }: { children: React.ReactNode }) => { - return ( -
- -
- -
-
{children}
-
- -
-
- ) -} - -export default Layout +const RootLayout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( +
+ {children} +
+ ); +}; +export default RootLayout; diff --git a/app/(root)/page.tsx b/app/(root)/page.tsx new file mode 100644 index 0000000..6adf510 --- /dev/null +++ b/app/(root)/page.tsx @@ -0,0 +1,75 @@ +import Image from "next/image"; + +import { Button } from "@/components/ui/button"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ +
+ +
+ ); +} diff --git a/app/(root)/profile/[id]/page.tsx b/app/(root)/profile/[id]/page.tsx new file mode 100644 index 0000000..87b5ba1 --- /dev/null +++ b/app/(root)/profile/[id]/page.tsx @@ -0,0 +1,6 @@ +const Profile = () => { + return ( +
Profile
+ ) +} +export default Profile \ No newline at end of file diff --git a/app/(root)/question/[id]/page.tsx b/app/(root)/question/[id]/page.tsx deleted file mode 100644 index 7a346ed..0000000 --- a/app/(root)/question/[id]/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import Answer from '@/components/forms/Answer' -import AllAnswers from '@/components/shared/AllAnswers' -import Metric from '@/components/shared/Metric' -import ParseHtml from '@/components/shared/ParseHtml' -import RenderTag from '@/components/shared/RenderTag' -import Votes from '@/components/shared/Votes' -import { getQuestion } from '@/lib/actions/question.action' -import { getUserById } from '@/lib/actions/user.action' -import { formatNumber, getTimestamp } from '@/lib/utils' -import { auth } from '@clerk/nextjs' -import Image from 'next/image' -import Link from 'next/link' - -const Page = async ({ params, searchParams }: any) => { - const { userId: clerkId } = auth() - - let mongoUser - - if (clerkId) { - mongoUser = await getUserById({ userId: clerkId }) - } - - const question = await getQuestion({ questionId: params.id }) - return ( - <> -
-
- - profile -

- {question.author.name} -

- -
- -
-
-

- {question.title} -

-
- -
- - - -
- - -
- {question.tags.map((tag: any) => ( - - ))} -
- - - - - ) -} - -export default Page diff --git a/app/(root)/tags/[id]/page.tsx b/app/(root)/tags/[id]/page.tsx deleted file mode 100644 index b5bc4dd..0000000 --- a/app/(root)/tags/[id]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Page = () => { - return
Page
-} - -export default Page diff --git a/app/(root)/tags/page.tsx b/app/(root)/tags/page.tsx index 3e5631f..80a5871 100644 --- a/app/(root)/tags/page.tsx +++ b/app/(root)/tags/page.tsx @@ -1,64 +1,4 @@ -import LocalSearch from '@/components/shared/search/LocalSearch' -import SearchFilter from '@/components/shared/search/SearchFilter' -import { TagFilters } from '@/constants/filters' -import { getAllTags } from '@/lib/actions/tag.action' -import Link from 'next/link' - -const Page = async () => { - const result = await getAllTags({}) - - return ( - <> -

Tags

-
- - -
-
- {result.tags.length > 0 ? ( - result.tags.map((tag) => ( - -
-
-

- {tag.name} -

-
- -

- - {tag.questions.length}+ - {' '} - Questions -

-
- - )) - ) : ( -
-

No tags yet

- - Create one to be the first! - -
- )} -
- - ) -} - -export default Page +const Tags = () => { + return
Tags
; +}; +export default Tags; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..00db783 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/auth"; // Referring to the auth.ts we just created +export const { GET, POST } = handlers; diff --git a/app/api/webhook/route.ts b/app/api/webhook/route.ts deleted file mode 100644 index fd741d3..0000000 --- a/app/api/webhook/route.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable camelcase */ -import { createUser, deleteUser, updateUser } from '@/lib/actions/user.action' -import { WebhookEvent } from '@clerk/nextjs/server' -import { headers } from 'next/headers' -import { NextResponse } from 'next/server' -import { Webhook } from 'svix' - -export async function POST(req: Request) { - // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook - const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET - - if (!WEBHOOK_SECRET) { - throw new Error( - 'Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local' - ) - } - - // Get the headers - const headerPayload = headers() - const svix_id = headerPayload.get('svix-id') - const svix_timestamp = headerPayload.get('svix-timestamp') - const svix_signature = headerPayload.get('svix-signature') - - // If there are no headers, error out - if (!svix_id || !svix_timestamp || !svix_signature) { - return new Response('Error occured -- no svix headers', { - status: 400, - }) - } - - // Get the body - const payload = await req.json() - const body = JSON.stringify(payload) - - // Create a new Svix instance with your secret. - const wh = new Webhook(WEBHOOK_SECRET) - - let evt: WebhookEvent - - // Verify the payload with the headers - try { - evt = wh.verify(body, { - 'svix-id': svix_id, - 'svix-timestamp': svix_timestamp, - 'svix-signature': svix_signature, - }) as WebhookEvent - } catch (err) { - console.error('Error verifying webhook:', err) - return new Response('Error occured', { - status: 400, - }) - } - - // Get the ID and type - const eventType = evt.type - - if (eventType === 'user.created') { - const { id, image_url, first_name, last_name, email_addresses, username } = - evt.data - - // Create a new user - const mongoUser = await createUser({ - clerkId: id, - email: email_addresses[0].email_address, - username: username ?? '', - name: `${first_name} ${last_name ?? ''}`, - picture: image_url, - }) - - console.log('mongo user', mongoUser) - - return NextResponse.json({ message: 'OK', user: mongoUser }) - } else if (eventType === 'user.updated') { - // Handle user updated - await updateUser({ - clerkId: evt.data.id, - updateData: { - email: evt.data.email_addresses[0].email_address, - username: evt.data.username ?? '', - name: `${evt.data.first_name} ${evt.data.last_name ?? ''}`, - picture: evt.data.image_url, - }, - path: `/profile/${evt.data.id}`, - }) - - return NextResponse.json({ status: 200 }) - } else if (eventType === 'user.deleted') { - const { id } = evt.data - // Handle user deleted - await deleteUser({ clerkId: id! }) - - return NextResponse.json({ status: 200 }) - } - - return new Response('', { status: 200 }) -} diff --git a/app/fonts/InterVF.ttf b/app/fonts/InterVF.ttf new file mode 100644 index 0000000..e724708 Binary files /dev/null and b/app/fonts/InterVF.ttf differ diff --git a/app/fonts/SpaceGroteskVF.ttf b/app/fonts/SpaceGroteskVF.ttf new file mode 100644 index 0000000..e1329aa Binary files /dev/null and b/app/fonts/SpaceGroteskVF.ttf differ diff --git a/app/globals.css b/app/globals.css index 578de50..1e68294 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,13 +2,247 @@ @tailwind components; @tailwind utilities; -@import url("../styles/theme.css"); - -body { - font-family: "Inter", sans-serif; +@layer base { + body { + font-family: "Inter", sans-serif; + } + :root { + --radius: 0.5rem; + } } @layer utilities { + .background-light850_dark100 { + @apply bg-light-850 dark:bg-dark-100; + } + + .background-light900_dark200 { + @apply bg-light-900 dark:bg-dark-200; + } + + .background-light900_dark300 { + @apply bg-light-900 dark:bg-dark-300; + } + + .background-light800_darkgradient { + @apply bg-light-800 dark:dark-gradient; + } + + .background-light800_dark400 { + @apply bg-light-800 dark:bg-dark-400 !important; + } + + .background-light700_dark400 { + @apply bg-light-700 dark:bg-dark-400; + } + + .background-light700_dark300 { + @apply bg-light-700 dark:bg-dark-300; + } + + .background-light800_dark400 { + @apply bg-light-800 dark:bg-dark-400; + } + + .background-light800_dark300 { + @apply bg-light-800 dark:bg-dark-300 !important; + } + + .background-light800_dark200 { + @apply bg-light-800 dark:bg-dark-200; + } + + .background-dark400_light900 { + @apply dark:bg-dark-400 bg-light-900 !important; + } + + .text-dark100_light900 { + @apply text-dark-100 dark:text-light-900 !important; + } + + .text-dark200_light900 { + @apply text-dark-200 dark:text-light-900; + } + + .text-dark200_light800 { + @apply text-dark-200 dark:text-light-800 !important; + } + + .text-dark300_light700 { + @apply text-dark-300 dark:text-light-700; + } + + .text-dark400_light700 { + @apply text-dark-400 dark:text-light-700; + } + + .text-dark500_light700 { + @apply text-dark-500 dark:text-light-700 !important; + } + + .text-dark500_light500 { + @apply text-dark-500 dark:text-light-500; + } + + .text-dark500_light400 { + @apply text-dark-500 dark:text-light-400; + } + + .text-dark300_light900 { + @apply text-dark-300 dark:text-light-900 !important; + } + + .text-dark400_light800 { + @apply text-dark-400 dark:text-light-800; + } + + .text-light400_light500 { + @apply text-light-400 dark:text-light-500 !important; + } + + .text-dark400_light500 { + @apply text-dark-400 dark:text-light-500; + } + + .text-dark400_light900 { + @apply text-dark-400 dark:text-light-900 !important; + } + + .text-light400_light500 { + @apply text-light-400 dark:text-light-500 !important; + } + + .light-border { + @apply border-light-800 dark:border-dark-300; + } + + .light-border-2 { + @apply border-light-700 dark:border-dark-400 !important; + } + + .h1-bold { + @apply text-[30px] font-bold leading-[42px] tracking-tighter; + } + + .h2-bold { + @apply text-[24px] font-bold leading-[31.2px]; + } + + .h2-semibold { + @apply text-[24px] font-semibold leading-[31.2px]; + } + + .h3-bold { + @apply text-[20px] font-bold leading-[26px]; + } + + .h3-semibold { + @apply text-[20px] font-semibold leading-[24.8px]; + } + + .base-medium { + @apply text-[18px] font-medium leading-[25.2px]; + } + + .base-semibold { + @apply text-[18px] font-semibold leading-[25.2px]; + } + + .base-bold { + @apply text-[18px] font-bold leading-[140%]; + } + + .paragraph-regular { + @apply text-[16px] font-normal leading-[22.4px]; + } + + .paragraph-medium { + @apply text-[16px] font-medium leading-[22.4px]; + } + + .paragraph-semibold { + @apply text-[16px] font-semibold leading-[20.8px]; + } + + .body-regular { + @apply text-[14px] font-normal leading-[19.6px]; + } + + .body-medium { + @apply text-[14px] font-medium leading-[18.2px]; + } + + .body-semibold { + @apply text-[14px] font-semibold leading-[18.2px]; + } + + .body-bold { + @apply text-[14px] font-bold leading-[18.2px]; + } + + .small-regular { + @apply text-[12px] font-normal leading-[15.6px]; + } + + .small-medium { + @apply text-[12px] font-medium leading-[15.6px]; + } + + .small-semibold { + @apply text-[12px] font-semibold leading-[15.6px]; + } + + .subtle-medium { + @apply text-[10px] font-medium leading-[13px] !important; + } + + .subtle-regular { + @apply text-[10px] font-normal leading-[13px]; + } + + .placeholder { + @apply placeholder:text-light-400 dark:placeholder:text-light-500; + } + + .invert-colors { + @apply invert dark:invert-0; + } + + .shadow-light100_dark100 { + @apply shadow-light-100 dark:shadow-dark-100; + } + + .shadow-light100_darknone { + @apply shadow-light-100 dark:shadow-none; + } + + .primary-gradient { + background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%); + } + + .dark-gradient { + background: linear-gradient( + 232deg, + rgba(23, 28, 35, 0.41) 0%, + rgba(19, 22, 28, 0.7) 100% + ); + } + + .light-gradient { + background: linear-gradient( + 132deg, + rgba(247, 249, 255, 0.5) 0%, + rgba(229, 237, 255, 0.25) 100% + ); + } + + .primary-text-gradient { + background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + .flex-center { @apply flex justify-center items-center; } @@ -37,12 +271,20 @@ body { @apply bg-light-700 dark:bg-dark-300 !important; } + .no-focus { + @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important; + } + .markdown { @apply max-w-full prose dark:prose-p:text-light-700 dark:prose-ol:text-light-700 dark:prose-ul:text-light-500 dark:prose-strong:text-white dark:prose-headings:text-white prose-headings:text-dark-400 prose-h1:text-dark-300 prose-h2:text-dark-300 prose-p:text-dark-500 prose-ul:text-dark-500 prose-ol:text-dark-500; } - .primary-gradient { - background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%); + .markdown-editor { + @apply prose max-w-full prose-p:m-0 dark:prose-headings:text-white prose-headings:text-dark-400 prose-p:text-dark-500 dark:prose-p:text-light-700 prose-ul:text-dark-500 dark:prose-ul:text-light-700 prose-ol:text-dark-500 dark:prose-ol:text-light-700 dark:prose-strong:text-white prose-blockquote:text-dark-500 dark:prose-blockquote:text-light-700; + } + + .tab { + @apply min-h-full dark:bg-dark-400 bg-light-800 text-light-500 dark:data-[state=active]:bg-dark-300 data-[state=active]:bg-primary-100 data-[state=active]:text-primary-500 !important; } .dark-gradient { @@ -52,14 +294,36 @@ body { rgba(19, 22, 28, 0.7) 100% ); } +} - .tab { - @apply min-h-full dark:bg-dark-400 bg-light-800 text-light-500 dark:data-[state=active]:bg-dark-300 data-[state=active]:bg-primary-100 data-[state=active]:text-primary-500 !important; - } +.custom-scrollbar::-webkit-scrollbar { + width: 3px; + height: 3px; + border-radius: 2px; } -.no-focus { - @apply focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 !important; +.custom-scrollbar::-webkit-scrollbar-track { + background: #ffffff; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: #888; + border-radius: 50px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ } .active-theme { @@ -67,41 +331,32 @@ body { brightness(104%) contrast(106%) !important; } -.light-gradient { - background: linear-gradient( - 132deg, - rgba(247, 249, 255, 0.5) 0%, - rgba(229, 237, 255, 0.25) 100% - ); +.hash-span { + margin-top: -140px; + padding-bottom: 140px; + display: block; } -.primary-text-gradient { - background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; +.mdxeditor-toolbar { + background: #ffffff !important; } -.custom-scrollbar::-webkit-scrollbar { - width: 3px; - height: 3px; - border-radius: 2px; +.dark .mdxeditor-toolbar { + background: #151821 !important; } -.custom-scrollbar::-webkit-scrollbar-track { - background: #ffffff; +.dark .mdxeditor-toolbar button svg { + color: #858ead !important; } -.custom-scrollbar::-webkit-scrollbar-thumb { - background: #888; - border-radius: 50px; +.dark .mdxeditor-toolbar button:hover svg { + color: #000 !important; } -.custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: #555; +.dark .mdxeditor-toolbar [role="separator"] { + border-color: #555 !important; } -/* Markdown Start */ .markdown a { color: #1da1f2; } @@ -140,26 +395,8 @@ code { color: inherit !important; } -/* Markdown End */ - -/* Clerk */ -.cl-internal-b3fm6y { - background: linear-gradient(129deg, #ff7000 0%, #e2995f 100%) !important; -} - -.hash-span { - margin-top: -140px; - padding-bottom: 140px; - display: block; -} -/* Hide scrollbar for Chrome, Safari and Opera */ -.no-scrollbar::-webkit-scrollbar { - display: none; +[data-lexical-editor="true"] { + height: 350px !important; + overflow-y: auto !important; } - -/* Hide scrollbar for IE, Edge and Firefox */ -.no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 8c8a285..ddbe799 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,51 +1,57 @@ -import { ThemeProvider } from '@/context/ThemeProvider' -import { ClerkProvider } from '@clerk/nextjs' -import type { Metadata } from 'next' -// eslint-disable-next-line camelcase -import { Inter, Space_Grotesk } from 'next/font/google' -import React from 'react' -import '../styles/prism.css' -import './globals.css' +import type { Metadata } from "next"; +import localFont from "next/font/local"; +import { SessionProvider } from "next-auth/react"; +import React from "react"; -const inter = Inter({ - subsets: ['latin'], - weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], - variable: '--font-inter', -}) +import { auth } from "@/auth"; +import { Toaster } from "@/components/ui/toaster"; +import ThemeProvider from "@/context/Theme"; -const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - weight: ['300', '400', '500', '600', '700'], - variable: '--font-spaceGrotesk', -}) +import "./globals.css"; + +const inter = localFont({ + src: "./fonts/InterVF.ttf", + variable: "--font-inter", + weight: "100 200 300 400 500 700 800 900", +}); + +const spaceGrotesk = localFont({ + src: "./fonts/SpaceGroteskVF.ttf", + variable: "--font-space-grotesk", + weight: "300 400 500 600 700", +}); export const metadata: Metadata = { - title: 'StackFlow', - description: - 'A community for developers to ask questions and share knowledge.', - icons: { - icon: '/assets/images/site-logo.svg', - }, -} + title: "StackFlow N15", + description: "Next15 version of StackFlow", + icons: { + icon: "/images/site-logo.svg", + }, +}; -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - - {children} - - - - ) +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const session = await auth(); + return ( + + + + + {children} + + + + + + ); } diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..7ff9307 --- /dev/null +++ b/auth.ts @@ -0,0 +1,7 @@ +import NextAuth from "next-auth"; +import GitHub from "next-auth/providers/github"; +import Google from "next-auth/providers/google"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [GitHub, Google], +}); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..d5078f8 Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json index 48c34e4..9f0424c 100644 --- a/components.json +++ b/components.json @@ -1,16 +1,21 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "new-york", "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "slate", - "cssVariables": true + "cssVariables": false, + "prefix": "" }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" - } + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" } \ No newline at end of file diff --git a/components/cards/QuestionCard.tsx b/components/cards/QuestionCard.tsx deleted file mode 100644 index c429c3e..0000000 --- a/components/cards/QuestionCard.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { formatNumber, getTimestamp } from '@/lib/utils' -import Link from 'next/link' -import Metric from '../shared/Metric' -import RenderTag from '../shared/RenderTag' - -interface QuestionProps { - _id: number - title: string - tags: { - _id: string - name: string - }[] - author: { - _id: string - name: string - picture: string - } - upvotes: number - views: number - answers: Array<{}> - createdAt: Date -} - -const QuestionCard = ({ - _id, - title, - tags, - author, - upvotes, - views, - answers, - createdAt, -}: QuestionProps) => { - return ( -
-
-
- - {getTimestamp(createdAt)} - - -

- {title} -

- -
-
- -
- {tags.map((tag) => ( - - ))} -
- -
- - - - -
-
- ) -} - -export default QuestionCard diff --git a/components/cards/UserCard.tsx b/components/cards/UserCard.tsx deleted file mode 100644 index 9733ce4..0000000 --- a/components/cards/UserCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { getTopInteractedTags } from '@/lib/actions/tag.action' -import Image from 'next/image' -import Link from 'next/link' -import RenderTag from '../shared/RenderTag' -import { Badge } from '../ui/badge' - -interface Props { - user: { - _id: string - clerkId: string - picture: string - name: string - username: string - } -} - -const UserCard = async ({ user }: Props) => { - const interactedTags = await getTopInteractedTags({ userId: user._id }) - - return ( - -
- user profile picture -
-

- {user.name} -

-

- @{user.username} -

-
-
- {interactedTags?.length! > 0 ? ( -
- {interactedTags?.map((tag) => ( - - ))} -
- ) : ( - No Tags yet - )} -
-
- - ) -} - -export default UserCard diff --git a/components/forms/Answer.tsx b/components/forms/Answer.tsx deleted file mode 100644 index bde664c..0000000 --- a/components/forms/Answer.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client"; - -import { useTheme } from "@/context/ThemeProvider"; -import { AnswerSchema } from "@/lib/validations"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Editor } from "@tinymce/tinymce-react"; -import Image from "next/image"; -import { useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { Button } from "../ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "../ui/form"; -import { createAnswer } from "@/lib/actions/answer.action"; -import { usePathname } from "next/navigation"; - -interface Props { - question: string; - questionId: string; - authorId: string; -} - -const Answer = ({ question, questionId, authorId }: Props) => { - const pathname = usePathname(); - const editorRef = useRef(null); - const [isSubmitting, setIsSubmitting] = useState(false); - const { mode } = useTheme(); - - const form = useForm>({ - resolver: zodResolver(AnswerSchema), - defaultValues: { - answer: "", - }, - }); - - async function onSubmit(values: z.infer) { - setIsSubmitting(true); - try { - await createAnswer({ - content: values.answer, - author: JSON.parse(authorId), - question: JSON.parse(questionId), - path: pathname, - }); - - form.reset(); - - if (editorRef.current) { - const editor = editorRef.current as any; - editor.setContent(""); - } - } catch (error) { - console.log("[CREATE_ANSWER_SUBMIT_ERROR]", error); - throw new Error("Failed to create answer"); - } finally { - setIsSubmitting(false); - } - } - - return ( -
-
-

- Write your answer here -

- -
-
- - ( - - - { - // @ts-ignore - editorRef.current = editor; - }} - onEditorChange={(content, editor) => { - field.onChange(content); - }} - onBlur={(content, editor) => field.onBlur()} - init={{ - height: 350, - menubar: false, - plugins: [ - "advlist", - "autolink", - "lists", - "link", - "image", - "charmap", - "preview", - "anchor", - "searchreplace", - "visualblocks", - "fullscreen", - "insertdatetime", - "media", - "table", - "codesample", - ], - toolbar: - "undo redo | blocks | " + - "codesample | bold italic forecolor | alignleft aligncenter " + - "alignright alignjustify | bullist numlist ", - content_style: - "body { font-family:Inter; font-size:16px }", - skin: mode === "dark" ? "oxide-dark" : "oxide", - content_css: mode === "dark" ? "dark" : "light", - }} - /> - - - - )} - /> -
- -
- - -
- ); -}; - -export default Answer; diff --git a/components/forms/AuthForm.tsx b/components/forms/AuthForm.tsx new file mode 100644 index 0000000..710bfc6 --- /dev/null +++ b/components/forms/AuthForm.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { + DefaultValues, + FieldValues, + Path, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import ROUTES from "@/constants/routes"; + +interface AuthFormProps { + formType: "SIGN_IN" | "SIGN_UP"; + schema: z.ZodType; + defaultValues: T; + onSubmit: (data: T) => Promise<{ success: boolean }>; +} + +export function AuthForm({ + formType, + schema, + defaultValues, + onSubmit, +}: AuthFormProps) { + const form = useForm>({ + resolver: zodResolver(schema), + defaultValues: defaultValues as DefaultValues, + }); + + const handleSubmit: SubmitHandler = async () => { + + }; + + const buttonText = formType === "SIGN_IN" ? "Sign In" : "Sign Up"; + + return ( +
+ + {Object.keys(defaultValues).map((field) => ( + } + render={({ field }) => ( + + + {field.name === "email" + ? "Email Address" + : field.name.charAt(0).toUpperCase() + field.name.slice(1)} + + + + + + + )} + /> + ))} + + {formType === "SIGN_IN" ? ( +

+ {" "} + Don't have an account?{" "} + + Sign Up + {" "} +

+ ) : ( +

+ Already have an account?{" "} + + Sign In + {" "} +

+ )} + + + ); +} diff --git a/components/forms/Question.tsx b/components/forms/Question.tsx deleted file mode 100644 index e0fe860..0000000 --- a/components/forms/Question.tsx +++ /dev/null @@ -1,248 +0,0 @@ -'use client' - -import { Button } from '@/components/ui/button' -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form' -import { Input } from '@/components/ui/input' -import { createQuestion } from '@/lib/actions/question.action' -import { QuestionsSchema } from '@/lib/validations' -import { zodResolver } from '@hookform/resolvers/zod' -import { Editor } from '@tinymce/tinymce-react' -import Image from 'next/image' -import { usePathname, useRouter } from 'next/navigation' -import React, { useRef, useState } from 'react' -import { useForm } from 'react-hook-form' -import * as z from 'zod' -import { Badge } from '../ui/badge' -import { useTheme } from '@/context/ThemeProvider' - -const type: any = 'create' - -interface Props { - mongoUserId: string -} - -const Question = ({ mongoUserId }: Props) => { - const editorRef = useRef(null) - const [isSubmitting, setIsSubmitting] = useState(false) - const {mode} = useTheme() - const router = useRouter() - const pathname = usePathname() - - const form = useForm>({ - resolver: zodResolver(QuestionsSchema), - defaultValues: { - title: '', - explanation: '', - tags: [], - }, - }) - - const handleTagRemove = (tag: string, field: any) => { - const tags = field.value.filter((t: string) => t !== tag) - form.setValue('tags', tags) - } - - const handleInputKeyDown = ( - e: React.KeyboardEvent, - field: any - ) => { - if (e.key === 'Enter' && field.name === 'tags') { - e.preventDefault() - - const tagInput = e.target as HTMLInputElement - const tagValue = tagInput.value.trim() - - if (tagValue !== '') { - if (tagValue.length > 15) { - return form.setError('tags', { - type: 'required', - message: 'Tag must be less than 15 characters', - }) - } - - if (!field.value.includes(tagValue as never)) { - form.setValue('tags', [...field.value, tagValue]) - tagInput.value = '' - form.clearErrors('tags') - } else { - form.trigger() - } - } - } - } - - async function onSubmit(values: z.infer) { - setIsSubmitting(true) - try { - // make async call to api - await createQuestion({ - title: values.title, - content: values.explanation, - tags: values.tags, - author: JSON.parse(mongoUserId), - path: pathname - }) - - router.push('/') - } catch (error) { - } finally { - setIsSubmitting(false) - } - } - - return ( -
- - {/** Question Title */} - ( - - - Question Title * - - - - - - Be descriptiive as possible - - - - )} - /> - {/** Question Explanation */} - ( - - - Detailed explanation of your problem? - * - - - { - // @ts-ignore - editorRef.current = editor - }} - initialValue="" - onEditorChange={(content, editor) => { - field.onChange(content) - }} - onBlur={(content, editor) => field.onBlur()} - init={{ - height: 350, - menubar: false, - plugins: [ - 'advlist', - 'autolink', - 'lists', - 'link', - 'image', - 'charmap', - 'preview', - 'anchor', - 'searchreplace', - 'visualblocks', - 'fullscreen', - 'insertdatetime', - 'media', - 'table', - 'codesample', - ], - toolbar: - 'undo redo | blocks | ' + - 'codesample | bold italic forecolor | alignleft aligncenter ' + - 'alignright alignjustify | bullist numlist ', - content_style: 'body { font-family:Inter; font-size:16px }', - skin: mode === 'dark' ? 'oxide-dark' : 'oxide', - content_css: mode === 'dark' ? 'dark' : 'light', - }} - /> - - - Introduce the problem and expand on what you put in the title. - Minimum 20 characters. - - - - )} - /> - {/** Question Tags */} - ( - - - Tags * - - - <> - handleInputKeyDown(e, field)} - /> - - {field.value.length > 0 && ( -
- {field.value.map((tag: any) => ( - handleTagRemove(tag, field)} - className="subtle-medium background-light800_dark300 text-light400_light500 flex items-center justify-center gap-2 rounded-md border-none px-4 py-2 capitalize"> - {tag}{' '} - close icon - - ))} -
- )} - -
- - Add up to 5 tags to describe what your question is about. Start - typing to see suggestions. - - -
- )} - /> - - - - ) -} - -export default Question diff --git a/components/forms/SocialAuthForm.tsx b/components/forms/SocialAuthForm.tsx new file mode 100644 index 0000000..c2552b4 --- /dev/null +++ b/components/forms/SocialAuthForm.tsx @@ -0,0 +1,64 @@ +"use client"; + +import Image from "next/image"; +import { signIn } from "next-auth/react"; + +import ROUTES from "@/constants/routes"; +import { useToast } from "@/hooks/use-toast"; + +import { Button } from "../ui/button"; + +const SocialAuthForm = () => { + const { toast } = useToast(); + const buttonClass = + "background-dark400_light900 body-medium text-dark200_light800 min-h-12 flex-1 rounded-2 px-4 py-3.5"; + + const handleSignIn = async (provider: "github" | "google") => { + try { + await signIn(provider, { + redirectTo: ROUTES.HOME, + redirect: false, + }); + } catch (error) { + console.log(error); + toast({ + title: "Sign in failed", + description: + error instanceof Error + ? error.message + : "An error occurred during sign in", + variant: "destructive", + }); + } + }; + + return ( +
+ + +
+ ); +}; +export default SocialAuthForm; diff --git a/components/home/HomeFilters.tsx b/components/home/HomeFilters.tsx deleted file mode 100644 index 6c27df2..0000000 --- a/components/home/HomeFilters.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client' - -import { HomePageFilters } from '@/constants/filters' -import { Button } from '../ui/button' - -const HomeFilters = () => { - const active = 'newest' - - return ( -
- {HomePageFilters.map((filter) => ( - - ))} -
- ) -} - -export default HomeFilters diff --git a/components/navigation/navbar/MobileNavigation.tsx b/components/navigation/navbar/MobileNavigation.tsx new file mode 100644 index 0000000..9c4785f --- /dev/null +++ b/components/navigation/navbar/MobileNavigation.tsx @@ -0,0 +1,71 @@ +import Image from "next/image"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetClose, + SheetContent, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import ROUTES from "@/constants/routes"; + +import NavLinks from "./NavLinks"; + +const MobileNavigation = () => { + return ( + + + hamburger menu + + + Navigation + + StackFlow Logo +

+ StackFlow +

+ +
+ +
+ +
+
+
+ + + + + + + + + + +
+
+
+
+ ); +}; +export default MobileNavigation; diff --git a/components/navigation/navbar/NavLinks.tsx b/components/navigation/navbar/NavLinks.tsx new file mode 100644 index 0000000..435604e --- /dev/null +++ b/components/navigation/navbar/NavLinks.tsx @@ -0,0 +1,69 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import React from "react"; + +import { SheetClose } from "@/components/ui/sheet"; +import { sidebarLinks } from "@/constants"; +import { cn } from "@/lib/utils"; + +const NavLinks = ({ isMobileNav = false }: { isMobileNav?: boolean }) => { + const pathname = usePathname(); + const userId = Math.floor(Math.random() * 100) + 1; + + return ( + <> + {sidebarLinks.map((link) => { + const isActive = + (pathname.includes(link.route) && link.route.length > 1) || + pathname === link.route; + + if (link.route === "/profile") { + if (userId) link.route = `${link.route}/${userId}`; + else return null; + } + const LinkComponent = ( + + {link.label} +

+ {link.label} +

+ + ); + + return isMobileNav ? ( + + {LinkComponent} + + ) : ( + {LinkComponent} + ); + })} + + ); +}; +export default NavLinks; diff --git a/components/navigation/navbar/Theme.tsx b/components/navigation/navbar/Theme.tsx new file mode 100644 index 0000000..9bcf289 --- /dev/null +++ b/components/navigation/navbar/Theme.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +const Theme = () => { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +}; +export default Theme; diff --git a/components/navigation/navbar/index.tsx b/components/navigation/navbar/index.tsx new file mode 100644 index 0000000..8935ce2 --- /dev/null +++ b/components/navigation/navbar/index.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; +import Link from "next/link"; + +import MobileNavigation from "./MobileNavigation"; +import Theme from "./Theme"; + +const Navbar = () => { + return ( + + ); +}; +export default Navbar; diff --git a/components/shared/AllAnswers.tsx b/components/shared/AllAnswers.tsx deleted file mode 100644 index 288e4b6..0000000 --- a/components/shared/AllAnswers.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import ParseHtml from '@/components/shared/ParseHtml' -import SearchFilter from '@/components/shared/search/SearchFilter' -import { AnswerFilters } from '@/constants/filters' -import { getAllQuestionAnswers } from '@/lib/actions/answer.action' -import { getTimestamp } from '@/lib/utils' -import Image from 'next/image' -import Link from 'next/link' -import Votes from './Votes' - -interface Props { - questionId: string - userId: string - totalAnswers: number - page?: number - filter?: number -} - -const AllAnswers = async ({ questionId, userId, totalAnswers }: Props) => { - const result = await getAllQuestionAnswers({ questionId }) - - return ( -
-
-

{totalAnswers} Answers

- -
-
- {result.answers.map((answer) => ( -
-
-
- - profile -
-

- {answer.author.name} -

-

- - answered {getTimestamp(answer.createdAt)} -

-
- -
- -
-
-
- -
- ))} -
-
- ) -} - -export default AllAnswers diff --git a/components/shared/Metric.tsx b/components/shared/Metric.tsx deleted file mode 100644 index 5191d61..0000000 --- a/components/shared/Metric.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' - -interface Props { - imgUrl: string - alt: string - title: string - value: string | number - textStyles?: string - isAuthor?: boolean - href?: string -} - -const Metric = ({ - imgUrl, - alt, - title, - value, - textStyles, - href, - isAuthor, -}: Props) => { - const metricContent = ( - <> - {alt} -

- {value} - - {title} - -

- - ) - - if (href) { - return ( - - {metricContent} - - ) - } - - return
{metricContent}
-} - -export default Metric diff --git a/components/shared/NoResult.tsx b/components/shared/NoResult.tsx deleted file mode 100644 index e896c5c..0000000 --- a/components/shared/NoResult.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import { Button } from '../ui/button' - -interface Props { - title: string - description: string - linkUrl: string - linkTitle: string -} - -const NoResult = ({ title, description, linkUrl, linkTitle }: Props) => { - return ( -
- No result illustration - No result illustration -

{title}

-

- {description} -

- - - -
- ) -} - -export default NoResult diff --git a/components/shared/ParseHtml.tsx b/components/shared/ParseHtml.tsx deleted file mode 100644 index 596fcf5..0000000 --- a/components/shared/ParseHtml.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client' - -import { useEffect } from 'react' - -import parse from 'html-react-parser' -import * as Prism from 'prismjs' - -interface Props { - data: string -} - -const ParseHtml = ({ data }: Props) => { - useEffect(() => { - Prism.highlightAll() - }, []) - - return
{parse(data)}
-} - -export default ParseHtml diff --git a/components/shared/RenderTag.tsx b/components/shared/RenderTag.tsx deleted file mode 100644 index 42f909b..0000000 --- a/components/shared/RenderTag.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Link from 'next/link' -import { Badge } from '../ui/badge' - -interface Props { - _id: string - name: string - totalQuestions?: number - showCount?: boolean -} - -const RenderTag = ({ _id, name, totalQuestions, showCount }: Props) => { - return ( - - - {name} - - {showCount && ( -

{totalQuestions}

- )} - - ) -} - -export default RenderTag diff --git a/components/shared/Votes.tsx b/components/shared/Votes.tsx deleted file mode 100644 index 41ebab6..0000000 --- a/components/shared/Votes.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client' - -import { - downvoteAnswer, - downvoteQuestion, - upvoteAnswer, - upvoteQuestion, -} from '@/lib/actions/question.action' -import { formatNumber } from '@/lib/utils' -import Image from 'next/image' -import { usePathname } from 'next/navigation' - -interface Props { - type: string - itemId: string - userId: string - upvotes: number - hasupVoted: boolean - downvotes: number - hasdownVoted: boolean - hasSaved?: boolean -} - -const Votes = ({ - type, - itemId, - userId, - upvotes, - hasupVoted, - downvotes, - hasdownVoted, - hasSaved, -}: Props) => { - const pathname = usePathname() - // const router = useRouter() - - const handleVote = async (action: string) => { - if (!userId) return - - if (action === 'upvote') { - if (type === 'Question') - await upvoteQuestion({ - userId: JSON.parse(userId), - hasupVoted, - hasdownVoted, - questionId: JSON.parse(itemId), - path: pathname, - }) - else if (type === 'Answer') { - await upvoteAnswer({ - userId: JSON.parse(userId), - hasupVoted, - hasdownVoted, - answerId: JSON.parse(itemId), - path: pathname, - }) - } - - return - } - - if (action === 'downvote') { - if (type === 'Question') - await downvoteQuestion({ - userId: JSON.parse(userId), - hasupVoted, - hasdownVoted, - questionId: JSON.parse(itemId), - path: pathname, - }) - else if (type === 'Answer') { - await downvoteAnswer({ - userId: JSON.parse(userId), - hasupVoted, - hasdownVoted, - answerId: JSON.parse(itemId), - path: pathname, - }) - } - } - } - - const handleSave = () => {} - return ( -
-
-
- upvotes handleVote('upvote')} - /> -
-

- {formatNumber(upvotes)} -

-
-
-
- downvotes handleVote('downvote')} - /> -
-

- {formatNumber(downvotes)} -

-
-
-
- {hasSaved && ( - save handleSave()} - /> - )} -
- ) -} - -export default Votes diff --git a/components/shared/navbar/MobileNav.tsx b/components/shared/navbar/MobileNav.tsx deleted file mode 100644 index e9a20d6..0000000 --- a/components/shared/navbar/MobileNav.tsx +++ /dev/null @@ -1,112 +0,0 @@ -'use client' - -import { Button } from '@/components/ui/button' -import { - Sheet, - SheetClose, - SheetContent, - SheetTrigger, -} from '@/components/ui/sheet' -import { sidebarLinks } from '@/constants' -import { SignedOut } from '@clerk/nextjs' -import Image from 'next/image' -import Link from 'next/link' -import { usePathname } from 'next/navigation' - -const NavContent = () => { - const pathname = usePathname() - - return ( -
- {sidebarLinks.map((item) => { - const isActive = - (pathname.includes(item.route) && item.route.length > 1) || - pathname === item.route - - return ( - - - {item.label} -

- {item.label} -

- -
- ) - })} -
- ) -} - -const MobileNav = () => { - return ( - - - Menu - - - - Stackflow -

- StackFlow -

- -
- - - - -
- - - - - - - - - - - -
-
-
-
-
- ) -} - -export default MobileNav diff --git a/components/shared/navbar/Navbar.tsx b/components/shared/navbar/Navbar.tsx deleted file mode 100644 index c3221f2..0000000 --- a/components/shared/navbar/Navbar.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { SignedIn, UserButton } from '@clerk/nextjs' -import Image from 'next/image' -import Link from 'next/link' -import GlobalSearch from '../search/GlobalSearch' -import MobileNav from './MobileNav' -import Theme from './Theme' - -const Navbar = () => { - return ( - - ) -} - -export default Navbar diff --git a/components/shared/navbar/Theme.tsx b/components/shared/navbar/Theme.tsx deleted file mode 100644 index 75d4608..0000000 --- a/components/shared/navbar/Theme.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client' - -import { - Menubar, - MenubarContent, - MenubarItem, - MenubarMenu, - MenubarTrigger, -} from '@/components/ui/menubar' -import { themes } from '@/constants' -import { useTheme } from '@/context/ThemeProvider' -import Image from 'next/image' - -const Theme = () => { - const { mode, setMode } = useTheme() - - return ( - - - - {mode === 'light' ? ( - sun - ) : ( - moon - )} - - - {themes.map((item) => ( - { - setMode(item.value) - - if (item.value !== 'system') { - localStorage.theme = item.value - } else { - localStorage.removeItem('theme') - } - }}> - {item.value} -

- {item.label} -

-
- ))} -
-
-
- ) -} - -export default Theme diff --git a/components/shared/search/GlobalSearch.tsx b/components/shared/search/GlobalSearch.tsx deleted file mode 100644 index 9a17847..0000000 --- a/components/shared/search/GlobalSearch.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client' - -import { Input } from '@/components/ui/input' -import Image from 'next/image' - -const GlobalSearch = () => { - return ( -
-
- search - {}} - className="paragraph-regular no-focus placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none" - /> -
-
- ) -} - -export default GlobalSearch diff --git a/components/shared/search/LocalSearch.tsx b/components/shared/search/LocalSearch.tsx deleted file mode 100644 index 9e9f515..0000000 --- a/components/shared/search/LocalSearch.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client' - -import { Input } from '@/components/ui/input' -import Image from 'next/image' - -interface CustomLocalSearchProps { - route: string - iconPosition: 'left' | 'right' - placeholder: string - otherClassNames: string - imgSrc: string -} - -const LocalSearch = ({ - route, - iconPosition, - placeholder, - otherClassNames, - imgSrc, -}: CustomLocalSearchProps) => { - return ( -
- {iconPosition === 'left' && ( - search - )} - {}} - className="paragraph-regular no-focus placeholder text-dark400_light700 border-none bg-transparent shadow-none outline-none" - /> - {iconPosition === 'right' && ( - search - )} -
- ) -} - -export default LocalSearch diff --git a/components/shared/search/SearchFilter.tsx b/components/shared/search/SearchFilter.tsx deleted file mode 100644 index d4cbdf7..0000000 --- a/components/shared/search/SearchFilter.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client' - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { SelectGroup } from '@radix-ui/react-select' - -interface Props { - filters: { - name: string - value: string - }[] - otherClasses?: string - containerClasses?: string -} - -const SearchFilter = ({ filters, otherClasses, containerClasses }: Props) => { - return ( -
- -
- ) -} - -export default SearchFilter diff --git a/components/shared/sidebar/LeftSidebar.tsx b/components/shared/sidebar/LeftSidebar.tsx deleted file mode 100644 index ef8ab06..0000000 --- a/components/shared/sidebar/LeftSidebar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client' - -import { Button } from '@/components/ui/button' -import { sidebarLinks } from '@/constants' -import { SignedOut } from '@clerk/nextjs' -import Image from 'next/image' -import Link from 'next/link' -import { usePathname } from 'next/navigation' - -const LeftSidebar = () => { - const pathname = usePathname() - - return ( -
-
- {sidebarLinks.map((item) => { - const isActive = - (pathname.includes(item.route) && item.route.length > 1) || - pathname === item.route - - return ( - - {item.label} -

- {item.label} -

- - ) - })} -
-
- -
- - - - - - -
-
-
-
- ) -} - -export default LeftSidebar diff --git a/components/shared/sidebar/RightSidebar.tsx b/components/shared/sidebar/RightSidebar.tsx deleted file mode 100644 index 7b962a0..0000000 --- a/components/shared/sidebar/RightSidebar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import RenderTag from '../RenderTag' - -const popularTags = [ - { _id: '1', name: 'javascript', totalQuestions: 5 }, - { _id: '2', name: 'next', totalQuestions: 55 }, - { _id: '3', name: 'vue', totalQuestions: 25 }, - { _id: '4', name: 'react', totalQuestions: 15 }, -] - -const RightSidebar = () => { - return ( -
- {/** Top Questions */} -
-

Top Questions

-
- -

- Can I get the course for free? -

- chevron right - -
-
- {/** Popular Tags */} -
-

Popular Tags

-
- {popularTags.map((tag) => ( - - ))} -
-
-
- ) -} - -export default RightSidebar diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx deleted file mode 100644 index f000e3e..0000000 --- a/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 0ba4277..f1fbaa1 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-slate-300", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: + "bg-slate-900 text-slate-50 shadow hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90", destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", + "bg-red-500 text-slate-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90", outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + "border border-slate-200 bg-white shadow-sm hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + "bg-slate-100 text-slate-900 shadow-sm hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80", + ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50", + link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50", }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", }, }, defaultVariants: { diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index f69a0d6..e729b5f 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -27,14 +27,14 @@ const DropdownMenuSubTrigger = React.forwardRef< {children} - + )) DropdownMenuSubTrigger.displayName = @@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef< svg]:size-4 [&>svg]:shrink-0 dark:focus:bg-slate-800 dark:focus:text-slate-50", inset && "pl-8", className )} @@ -99,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< (({ className, ...props }, ref) => ( )) diff --git a/components/ui/form.tsx b/components/ui/form.tsx index 4603f8b..3f54feb 100644 --- a/components/ui/form.tsx +++ b/components/ui/form.tsx @@ -1,3 +1,5 @@ +"use client" + import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot" @@ -93,7 +95,7 @@ const FormLabel = React.forwardRef< return (