├── src
├── models
│ ├── index.ts
│ ├── name.ts
│ ├── client
│ │ └── config.ts
│ └── server
│ │ ├── config.ts
│ │ ├── answer.collection.ts
│ │ ├── dbSetup.ts
│ │ ├── comment.collection.ts
│ │ ├── vote.collection.ts
│ │ ├── storageSetup.ts
│ │ └── question.collection.ts
├── app
│ ├── globals.css
│ ├── favicon.ico
│ ├── env.ts
│ ├── users
│ │ └── [userId]
│ │ │ └── [userSlug]
│ │ │ ├── edit
│ │ │ └── page.tsx
│ │ │ ├── EditButton.tsx
│ │ │ ├── Navbar.tsx
│ │ │ ├── answers
│ │ │ └── page.tsx
│ │ │ ├── questions
│ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── votes
│ │ │ └── page.tsx
│ ├── questions
│ │ ├── [quesId]
│ │ │ └── [quesName]
│ │ │ │ ├── edit
│ │ │ │ ├── page.tsx
│ │ │ │ └── EditQues.tsx
│ │ │ │ ├── EditQuestion.tsx
│ │ │ │ ├── DeleteQuestion.tsx
│ │ │ │ └── page.tsx
│ │ ├── Search.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── (auth)
│ │ ├── layout.tsx
│ │ ├── login
│ │ │ └── page.tsx
│ │ └── register
│ │ │ └── page.tsx
│ ├── components
│ │ ├── HeroSection.tsx
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ ├── LatestQuestions.tsx
│ │ ├── TopContributers.tsx
│ │ └── HeroSectionHeader.tsx
│ ├── api
│ │ ├── answer
│ │ │ └── route.ts
│ │ └── vote
│ │ │ └── route.ts
│ └── page.tsx
├── lib
│ └── utils.ts
├── utils
│ ├── slugify.ts
│ └── relativeTime.ts
├── components
│ ├── RTE.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── input.tsx
│ │ ├── wobble-card.tsx
│ │ ├── hero-parallax.tsx
│ │ ├── tracing-beam.tsx
│ │ ├── floating-navbar.tsx
│ │ └── background-beams.tsx
│ ├── magicui
│ │ ├── retro-grid.tsx
│ │ ├── confetti.tsx
│ │ ├── meteors.tsx
│ │ ├── number-ticker.tsx
│ │ ├── border-beam.tsx
│ │ ├── animated-list.tsx
│ │ ├── shiny-button.tsx
│ │ ├── icon-cloud.tsx
│ │ ├── shine-border.tsx
│ │ ├── shimmer-button.tsx
│ │ ├── neon-gradient-card.tsx
│ │ ├── animated-grid-pattern.tsx
│ │ ├── magic-card.tsx
│ │ └── particles.tsx
│ ├── Pagination.tsx
│ ├── QuestionCard.tsx
│ ├── Comments.tsx
│ ├── VoteButtons.tsx
│ ├── Answers.tsx
│ └── QuestionForm.tsx
├── middleware.ts
└── store
│ └── Auth.ts
├── .eslintrc.json
├── next.config.mjs
├── .env.sample
├── postcss.config.mjs
├── components.json
├── .gitignore
├── public
├── vercel.svg
└── next.svg
├── tsconfig.json
├── package.json
├── README.md
└── tailwind.config.ts
/src/models/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiteshchoudhary/stackflow-appwrite/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_APPWRITE_HOST_URL=https://cloud.appwrite.io/v1
2 | NEXT_PUBLIC_APPWRITE_PROJECT_ID=6676
3 | APPWRITE_API_KEY=3d
4 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/env.ts:
--------------------------------------------------------------------------------
1 | const env = {
2 | appwrite: {
3 | endpoint: String(process.env.NEXT_PUBLIC_APPWRITE_HOST_URL),
4 | projectId: String(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID),
5 | apikey: String(process.env.APPWRITE_API_KEY)
6 | }
7 | }
8 |
9 | export default env
--------------------------------------------------------------------------------
/src/models/name.ts:
--------------------------------------------------------------------------------
1 | export const db = "main-stackflow"
2 | export const questionCollection = "questions"
3 | export const answerCollection = "answers"
4 | export const commentCollection = "comments"
5 | export const voteCollection = "votes"
6 | export const questionAttachmentBucket = "question-attachment"
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Page = () => {
4 | return (
5 |
6 |
Edit
7 | Homework
8 |
9 | );
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/src/utils/slugify.ts:
--------------------------------------------------------------------------------
1 | export default function slugify(text: string) {
2 | return text
3 | .toString()
4 | .toLowerCase()
5 | .trim() // Trim whitespace from both sides of the string
6 | .replace(/\s+/g, "-") // Replace spaces with a dash
7 | .replace(/[^\w\-]+/g, "") // Remove all non-word characters
8 | .replace(/\-\-+/g, "-"); // Replace multiple dashes with a single dash
9 | }
10 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/src/app/questions/[quesId]/[quesName]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { db, questionCollection } from "@/models/name";
2 | import { databases } from "@/models/server/config";
3 | import React from "react";
4 | import EditQues from "./EditQues";
5 |
6 | const Page = async ({ params }: { params: { quesId: string; quesName: string } }) => {
7 | const question = await databases.getDocument(db, questionCollection, params.quesId);
8 |
9 | return ;
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/src/models/client/config.ts:
--------------------------------------------------------------------------------
1 | import env from "@/app/env";
2 |
3 | import { Client, Account, Avatars, Databases, Storage } from "appwrite";
4 |
5 | const client = new Client()
6 | .setEndpoint(env.appwrite.endpoint) // Your API Endpoint
7 | .setProject(env.appwrite.projectId); // Your project ID
8 |
9 | const databases = new Databases(client)
10 | const account = new Account(client);
11 | const avatars = new Avatars(client);
12 | const storage = new Storage(client);
13 |
14 |
15 | export { client, databases, account, avatars, storage}
16 |
--------------------------------------------------------------------------------
/src/components/RTE.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from "next/dynamic";
4 |
5 | import Editor from "@uiw/react-md-editor";
6 |
7 | // for more information, see https://mdxeditor.dev/editor/docs/getting-started
8 |
9 | // This is the only place InitializedMDXEditor is imported directly.
10 | const RTE = dynamic(
11 | () =>
12 | import("@uiw/react-md-editor").then(mod => {
13 | return mod.default;
14 | }),
15 | { ssr: false }
16 | );
17 |
18 | export const MarkdownPreview = Editor.Markdown;
19 |
20 | export default RTE;
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | .env
38 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/models/server/config.ts:
--------------------------------------------------------------------------------
1 | import env from "@/app/env";
2 |
3 | import {Avatars, Client, Databases, Storage, Users} from "node-appwrite"
4 |
5 | let client = new Client();
6 |
7 | client
8 | .setEndpoint(env.appwrite.endpoint) // Your API Endpoint
9 | .setProject(env.appwrite.projectId) // Your project ID
10 | .setKey(env.appwrite.apikey) // Your secret API key
11 |
12 | ;
13 |
14 | const databases = new Databases(client)
15 | const avatars = new Avatars(client);
16 | const storage = new Storage(client);
17 | const users = new Users(client)
18 |
19 |
20 | export { client, databases, users, avatars, storage}
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 | import { cn } from "@/lib/utils";
7 | import Header from "./components/Header";
8 | export const metadata: Metadata = {
9 | title: "Create Next App",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { BackgroundBeams } from "@/components/ui/background-beams";
4 | import { useAuthStore } from "@/store/Auth"
5 | import { useRouter } from "next/navigation";
6 | import React from "react";
7 |
8 |
9 | const Layout = ({children}: {children: React.ReactNode}) => {
10 | const {session} = useAuthStore();
11 | const router = useRouter()
12 |
13 | React.useEffect(() => {
14 | if (session) {
15 | router.push("/")
16 | }
17 | }, [session, router])
18 |
19 | if (session) {
20 | return null
21 | }
22 |
23 | return (
24 |
25 |
26 |
{children}
27 |
28 | )
29 | }
30 |
31 |
32 | export default Layout
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import type { NextRequest } from 'next/server'
3 |
4 | import getOrCreateDB from './models/server/dbSetup'
5 | import getOrCreateStorage from './models/server/storageSetup'
6 |
7 | // This function can be marked `async` if using `await` inside
8 | export async function middleware(request: NextRequest) {
9 |
10 | await Promise.all([
11 | getOrCreateDB(),
12 | getOrCreateStorage()
13 | ])
14 | return NextResponse.next()
15 | }
16 |
17 | // See "Matching Paths" below to learn more
18 | export const config = {
19 | /* match all request paths except for the the ones that starts with:
20 | - api
21 | - _next/static
22 | - _next/image
23 | - favicon.com
24 |
25 | */
26 | matcher: [
27 | "/((?!api|_next/static|_next/image|favicon.ico).*)",
28 | ],
29 | }
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/EditButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useAuthStore } from "@/store/Auth";
4 | import Link from "next/link";
5 | import { useParams } from "next/navigation";
6 | import React from "react";
7 |
8 | const EditButton = () => {
9 | const { userId, userSlug } = useParams();
10 | const { user } = useAuthStore();
11 |
12 | if (user?.$id !== userId) return null;
13 |
14 | return (
15 |
19 | Edit
20 |
21 |
22 | );
23 | };
24 |
25 | export default EditButton;
26 |
--------------------------------------------------------------------------------
/src/app/questions/[quesId]/[quesName]/EditQuestion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useAuthStore } from "@/store/Auth";
4 | import slugify from "@/utils/slugify";
5 | import { IconEdit } from "@tabler/icons-react";
6 | import Link from "next/link";
7 | import React from "react";
8 |
9 | const EditQuestion = ({
10 | questionId,
11 | questionTitle,
12 | authorId,
13 | }: {
14 | questionId: string;
15 | questionTitle: string;
16 | authorId: string;
17 | }) => {
18 | const { user } = useAuthStore();
19 |
20 | return user?.$id === authorId ? (
21 |
25 |
26 |
27 | ) : null;
28 | };
29 |
30 | export default EditQuestion;
31 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/models/server/answer.collection.ts:
--------------------------------------------------------------------------------
1 | import { IndexType, Permission } from "node-appwrite";
2 | import { answerCollection, db } from "../name";
3 | import { databases } from "./config";
4 |
5 | export default async function createAnswerCollection() {
6 | // Creating Collection
7 | await databases.createCollection(db, answerCollection, answerCollection, [
8 | Permission.create("users"),
9 | Permission.read("any"),
10 | Permission.read("users"),
11 | Permission.update("users"),
12 | Permission.delete("users"),
13 | ]);
14 | console.log("Answer Collection Created");
15 |
16 | // Creating Attributes
17 | await Promise.all([
18 | databases.createStringAttribute(db, answerCollection, "content", 10000, true),
19 | databases.createStringAttribute(db, answerCollection, "questionId", 50, true),
20 | databases.createStringAttribute(db, answerCollection, "authorId", 50, true),
21 | ]);
22 | console.log("Answer Attributes Created");
23 | }
24 |
--------------------------------------------------------------------------------
/src/models/server/dbSetup.ts:
--------------------------------------------------------------------------------
1 | import { db } from "../name";
2 | import createAnswerCollection from "./answer.collection";
3 | import createCommentCollection from "./comment.collection";
4 | import createQuestionCollection from "./question.collection";
5 | import createVoteCollection from "./vote.collection";
6 |
7 | import { databases } from "./config";
8 |
9 | export default async function getOrCreateDB(){
10 | try {
11 | await databases.get(db)
12 | console.log("Database connection")
13 | } catch (error) {
14 | try {
15 | await databases.create(db, db)
16 | console.log("database created")
17 | //create collections
18 | await Promise.all([
19 | createQuestionCollection(),
20 | createAnswerCollection(),
21 | createCommentCollection(),
22 | createVoteCollection(),
23 |
24 | ])
25 | console.log("Collection created")
26 | console.log("Database connected")
27 | } catch (error) {
28 | console.log("Error creating databases or collection", error)
29 | }
30 | }
31 |
32 | return databases
33 | }
--------------------------------------------------------------------------------
/src/app/components/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HeroParallax } from "@/components/ui/hero-parallax";
3 | import { databases } from "@/models/server/config";
4 | import { db, questionAttachmentBucket, questionCollection } from "@/models/name";
5 | import { Query } from "node-appwrite";
6 | import slugify from "@/utils/slugify";
7 | import { storage } from "@/models/client/config";
8 | import HeroSectionHeader from "./HeroSectionHeader";
9 |
10 | export default async function HeroSection() {
11 | const questions = await databases.listDocuments(db, questionCollection, [
12 | Query.orderDesc("$createdAt"),
13 | Query.limit(15),
14 | ]);
15 |
16 | return (
17 | }
19 | products={questions.documents.map(q => ({
20 | title: q.title,
21 | link: `/questions/${q.$id}/${slugify(q.title)}`,
22 | thumbnail: storage.getFilePreview(questionAttachmentBucket, q.attachmentId).href,
23 | }))}
24 | />
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/models/server/comment.collection.ts:
--------------------------------------------------------------------------------
1 | import { Permission } from "node-appwrite";
2 | import { commentCollection, db } from "../name";
3 | import { databases } from "./config";
4 |
5 | export default async function createCommentCollection() {
6 | // Creating Collection
7 | await databases.createCollection(db, commentCollection, commentCollection, [
8 | Permission.create("users"),
9 | Permission.read("any"),
10 | Permission.read("users"),
11 | Permission.update("users"),
12 | Permission.delete("users"),
13 | ]);
14 | console.log("Comment Collection Created");
15 |
16 | // Creating Attributes
17 | await Promise.all([
18 | databases.createStringAttribute(db, commentCollection, "content", 10000, true),
19 | databases.createEnumAttribute(db, commentCollection, "type", ["answer", "question"], true),
20 | databases.createStringAttribute(db, commentCollection, "typeId", 50, true),
21 | databases.createStringAttribute(db, commentCollection, "authorId", 50, true),
22 | ]);
23 | console.log("Comment Attributes Created");
24 | }
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stackoverflow-appwrite",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-label": "^2.1.0",
13 | "@tabler/icons-react": "^3.6.0",
14 | "@uiw/react-md-editor": "^4.0.4",
15 | "appwrite": "^15.0.0",
16 | "class-variance-authority": "^0.7.0",
17 | "clsx": "^2.1.1",
18 | "framer-motion": "^11.2.12",
19 | "immer": "^10.1.1",
20 | "lucide-react": "^0.396.0",
21 | "mini-svg-data-uri": "^1.4.4",
22 | "next": "14.2.4",
23 | "node-appwrite": "^13.0.0",
24 | "react": "^18",
25 | "react-dom": "^18",
26 | "tailwind-merge": "^2.3.0",
27 | "tailwindcss-animate": "^1.0.7",
28 | "zustand": "^4.5.2"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^20",
32 | "@types/react": "^18",
33 | "@types/react-dom": "^18",
34 | "eslint": "^8",
35 | "eslint-config-next": "14.2.4",
36 | "postcss": "^8",
37 | "tailwindcss": "^3.4.1",
38 | "typescript": "^5"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/models/server/vote.collection.ts:
--------------------------------------------------------------------------------
1 | import { Permission } from "node-appwrite";
2 | import { db, voteCollection } from "../name";
3 | import { databases } from "./config";
4 |
5 | export default async function createVoteCollection() {
6 | // Creating Collection
7 | await databases.createCollection(db, voteCollection, voteCollection, [
8 | Permission.create("users"),
9 | Permission.read("any"),
10 | Permission.read("users"),
11 | Permission.update("users"),
12 | Permission.delete("users"),
13 | ]);
14 | console.log("Vote Collection Created");
15 |
16 | // Creating Attributes
17 | await Promise.all([
18 | databases.createEnumAttribute(db, voteCollection, "type", ["question", "answer"], true),
19 | databases.createStringAttribute(db, voteCollection, "typeId", 50, true),
20 | databases.createEnumAttribute(
21 | db,
22 | voteCollection,
23 | "voteStatus",
24 | ["upvoted", "downvoted"],
25 | true
26 | ),
27 | databases.createStringAttribute(db, voteCollection, "votedById", 50, true),
28 | ]);
29 | console.log("Vote Attributes Created");
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/components/Header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { FloatingNav } from "@/components/ui/floating-navbar";
4 | import { IconHome, IconMessage, IconWorldQuestion } from "@tabler/icons-react";
5 | import { useAuthStore } from "@/store/Auth";
6 | import slugify from "@/utils/slugify";
7 |
8 | export default function Header() {
9 | const { user } = useAuthStore();
10 |
11 | const navItems = [
12 | {
13 | name: "Home",
14 | link: "/",
15 | icon: ,
16 | },
17 | {
18 | name: "Questions",
19 | link: "/questions",
20 | icon: ,
21 | },
22 | ];
23 |
24 | if (user)
25 | navItems.push({
26 | name: "Profile",
27 | link: `/users/${user.$id}/${slugify(user.name)}`,
28 | icon: ,
29 | });
30 |
31 | return (
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/models/server/storageSetup.ts:
--------------------------------------------------------------------------------
1 | import { Permission } from "node-appwrite";
2 | import { questionAttachmentBucket } from "../name";
3 | import { storage } from "./config";
4 |
5 | export default async function getOrCreateStorage() {
6 | try {
7 | await storage.getBucket(questionAttachmentBucket);
8 | console.log("Storage Connected");
9 | } catch (error) {
10 | try {
11 | await storage.createBucket(
12 | questionAttachmentBucket,
13 | questionAttachmentBucket,
14 | [
15 | Permission.create("users"),
16 | Permission.read("any"),
17 | Permission.read("users"),
18 | Permission.update("users"),
19 | Permission.delete("users"),
20 | ],
21 | false,
22 | undefined,
23 | undefined,
24 | ["jpg", "png", "gif", "jpeg", "webp", "heic"]
25 | );
26 |
27 | console.log("Storage Created");
28 | console.log("Storage Connected");
29 | } catch (error) {
30 | console.error("Error creating storage:", error);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/relativeTime.ts:
--------------------------------------------------------------------------------
1 | export default function convertDateToRelativeTime(date: Date) {
2 | if (date.toString().toLowerCase() === "invalid date") return "";
3 |
4 | const now = new Date();
5 | const timeDifferenceInMiliSeconds = now.getTime() - date.getTime();
6 |
7 | const seconds = Math.floor(timeDifferenceInMiliSeconds / 1000);
8 | if (seconds < 10) {
9 | return `Just now`;
10 | }
11 | if (seconds < 60) {
12 | return `${seconds} second${seconds !== 1 ? "s" : ""} ago`;
13 | }
14 |
15 | const minutes = Math.floor(seconds / 60);
16 | if (minutes < 60) {
17 | return `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
18 | }
19 |
20 | const hours = Math.floor(minutes / 60);
21 | if (hours < 24) {
22 | return `${hours} hour${hours !== 1 ? "s" : ""} ago`;
23 | }
24 |
25 | const days = Math.floor(hours / 24);
26 | if (days < 30) {
27 | return `${days} day${days !== 1 ? "s" : ""} ago`;
28 | }
29 |
30 | const months = Math.floor(days / 30.44); // Average days in a month
31 | if (months < 12) {
32 | return `${months} month${months !== 1 ? "s" : ""} ago`;
33 | }
34 |
35 | const years = Math.floor(months / 12);
36 | return `${years} year${years !== 1 ? "s" : ""} ago`;
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A stackover flow inspired project using Next.js and Appwrite
2 |
3 |
4 | This is a stackoverflow clone built with Next.js and Appwrite. It uses the [Appwrite Node.js SDK](https://github.com/appwrite/sdk-for-node) to interact with the Appwrite API. The UI is built using Tailwind CSS and the magicui library and the database is stored in Appwrite.
5 |
6 | ## Features
7 |
8 | - User authentication with email and password
9 | - Questions and answers
10 | - Voting system
11 | - Comments
12 | - Markdown support
13 | - Search functionality
14 | - Themes
15 |
16 | ## Getting Started
17 |
18 | ### Prerequisites
19 |
20 | - [Node.js](https://nodejs.org/en/download/) (version 16.14.0 or higher)
21 | - [Appwrite](https://appwrite.io/docs/installation) (version 1.0.0 or higher)
22 |
23 | ## Build more features on top of this
24 |
25 | You can build more features on top of this by adding more collections and indexes to the database. You can also add more routes to the application to handle more functionalities. The code is well documented and easy to understand, so you can customize it to your needs.
26 |
27 | ## Youtube Tutorial
28 |
29 | You can find a youtube tutorial on how to build this project [here](https://www.youtube.com/@HiteshChoudharydotcom).
--------------------------------------------------------------------------------
/src/app/questions/[quesId]/[quesName]/DeleteQuestion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { databases } from "@/models/client/config";
4 | import { db, questionCollection } from "@/models/name";
5 | import { useAuthStore } from "@/store/Auth";
6 | import { IconTrash } from "@tabler/icons-react";
7 | import { useRouter } from "next/navigation";
8 | import React from "react";
9 |
10 | const DeleteQuestion = ({ questionId, authorId }: { questionId: string; authorId: string }) => {
11 | const router = useRouter();
12 | const { user } = useAuthStore();
13 |
14 | const deleteQuestion = async () => {
15 | try {
16 | await databases.deleteDocument(db, questionCollection, questionId);
17 |
18 | router.push("/questions");
19 | } catch (error: any) {
20 | window.alert(error?.message || "Something went wrong");
21 | }
22 | };
23 |
24 | return user?.$id === authorId ? (
25 |
29 |
30 |
31 | ) : null;
32 | };
33 |
34 | export default DeleteQuestion;
35 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/questions/[quesId]/[quesName]/edit/EditQues.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import QuestionForm from "@/components/QuestionForm";
4 | import { useAuthStore } from "@/store/Auth";
5 | import slugify from "@/utils/slugify";
6 | import { Models } from "appwrite";
7 | import { useRouter } from "next/navigation";
8 | import React from "react";
9 |
10 | const EditQues = ({ question }: { question: Models.Document }) => {
11 | const { user } = useAuthStore();
12 | const router = useRouter();
13 |
14 | React.useEffect(() => {
15 | if (question.authorId !== user?.$id) {
16 | router.push(`/questions/${question.$id}/${slugify(question.title)}`);
17 | }
18 | }, []);
19 |
20 | if (user?.$id !== question.authorId) return null;
21 |
22 | return (
23 |
24 |
25 |
Edit your public question
26 |
27 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default EditQues;
39 |
--------------------------------------------------------------------------------
/src/app/questions/Search.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
5 | import React from "react";
6 |
7 | const Search = () => {
8 | const pathname = usePathname();
9 | const searchParams = useSearchParams();
10 | const router = useRouter();
11 | const [search, setSearch] = React.useState(searchParams.get("search") || "");
12 |
13 | React.useEffect(() => {
14 | setSearch(() => searchParams.get("search") || "");
15 | }, [searchParams]);
16 |
17 | const handleSearch = (e: React.FormEvent) => {
18 | e.preventDefault();
19 | const newSearchParams = new URLSearchParams(searchParams);
20 | newSearchParams.set("search", search);
21 | router.push(`${pathname}?${newSearchParams}`);
22 | };
23 |
24 | return (
25 |
36 | );
37 | };
38 |
39 | export default Search;
40 |
--------------------------------------------------------------------------------
/src/components/magicui/retro-grid.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 |
3 | export default function RetroGrid({ className }: { className?: string }) {
4 | return (
5 |
11 | {/* Grid */}
12 |
27 |
28 | {/* Background Gradient */}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/magicui/confetti.tsx:
--------------------------------------------------------------------------------
1 | import confetti from "canvas-confetti";
2 |
3 | interface ConfettiOptions extends confetti.Options {
4 | particleCount?: number;
5 | angle?: number;
6 | spread?: number;
7 | startVelocity?: number;
8 | decay?: number;
9 | gravity?: number;
10 | drift?: number;
11 | flat?: boolean;
12 | ticks?: number;
13 | origin?: { x: number; y: number };
14 | colors?: string[];
15 | shapes?: confetti.Shape[];
16 | zIndex?: number;
17 | disableForReducedMotion?: boolean;
18 | useWorker?: boolean;
19 | resize?: boolean;
20 | canvas?: HTMLCanvasElement | null;
21 | scalar?: number;
22 | }
23 |
24 | const Confetti = (options: ConfettiOptions) => {
25 | if (options.disableForReducedMotion && window.matchMedia("(prefers-reduced-motion)").matches) {
26 | return;
27 | }
28 |
29 | const confettiInstance = options.canvas
30 | ? confetti.create(options.canvas, {
31 | resize: options.resize ?? true,
32 | useWorker: options.useWorker ?? true,
33 | })
34 | : confetti;
35 |
36 | confettiInstance({
37 | ...options,
38 | });
39 | };
40 |
41 | Confetti.shapeFromPath = (options: { path: string; [key: string]: any }) => {
42 | return confetti.shapeFromPath({ ...options });
43 | };
44 |
45 | Confetti.shapeFromText = (options: { text: string; [key: string]: any }) => {
46 | return confetti.shapeFromText({ ...options });
47 | };
48 |
49 | export { Confetti };
50 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/Navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { useParams, usePathname } from "next/navigation";
5 | import React from "react";
6 |
7 | const Navbar = () => {
8 | const { userId, userSlug } = useParams();
9 | const pathname = usePathname();
10 |
11 | const items = [
12 | {
13 | name: "Summary",
14 | href: `/users/${userId}/${userSlug}`,
15 | },
16 | {
17 | name: "Questions",
18 | href: `/users/${userId}/${userSlug}/questions`,
19 | },
20 | {
21 | name: "Answers",
22 | href: `/users/${userId}/${userSlug}/answers`,
23 | },
24 | {
25 | name: "Votes",
26 | href: `/users/${userId}/${userSlug}/votes`,
27 | },
28 | ];
29 |
30 | return (
31 |
32 | {items.map(item => (
33 |
34 |
40 | {item.name}
41 |
42 |
43 | ))}
44 |
45 | );
46 | };
47 |
48 | export default Navbar;
49 |
--------------------------------------------------------------------------------
/src/components/magicui/meteors.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import clsx from "clsx";
4 | import { useEffect, useState } from "react";
5 |
6 | interface MeteorsProps {
7 | number?: number;
8 | }
9 | export const Meteors = ({ number = 20 }: MeteorsProps) => {
10 | const [meteorStyles, setMeteorStyles] = useState>([]);
11 |
12 | useEffect(() => {
13 | const styles = [...new Array(number)].map(() => ({
14 | top: -5,
15 | left: Math.floor(Math.random() * window.innerWidth) + "px",
16 | animationDelay: Math.random() * 1 + 0.2 + "s",
17 | animationDuration: Math.floor(Math.random() * 8 + 2) + "s",
18 | }));
19 | setMeteorStyles(styles);
20 | }, [number]);
21 |
22 | return (
23 | <>
24 | {[...meteorStyles].map((style, idx) => (
25 | // Meteor Head
26 |
33 | {/* Meteor Tail */}
34 |
35 |
36 | ))}
37 | >
38 | );
39 | };
40 |
41 | export default Meteors;
42 |
--------------------------------------------------------------------------------
/src/components/magicui/number-ticker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/utils/cn";
4 | import { useInView, useMotionValue, useSpring } from "framer-motion";
5 | import { useEffect, useRef } from "react";
6 |
7 | export default function NumberTicker({
8 | value,
9 | direction = "up",
10 | delay = 0,
11 | className,
12 | }: {
13 | value: number;
14 | direction?: "up" | "down";
15 | className?: string;
16 | delay?: number; // delay in s
17 | }) {
18 | const ref = useRef(null);
19 | const motionValue = useMotionValue(direction === "down" ? value : 0);
20 | const springValue = useSpring(motionValue, {
21 | damping: 60,
22 | stiffness: 100,
23 | });
24 | const isInView = useInView(ref, { once: true, margin: "0px" });
25 |
26 | useEffect(() => {
27 | isInView &&
28 | setTimeout(() => {
29 | motionValue.set(direction === "down" ? 0 : value);
30 | }, delay * 1000);
31 | }, [motionValue, isInView, delay, value, direction]);
32 |
33 | useEffect(
34 | () =>
35 | springValue.on("change", latest => {
36 | if (ref.current) {
37 | ref.current.textContent = Intl.NumberFormat("en-US").format(latest.toFixed(0));
38 | }
39 | }),
40 | [springValue]
41 | );
42 |
43 | return (
44 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/models/server/question.collection.ts:
--------------------------------------------------------------------------------
1 | import {IndexType, Permission} from "node-appwrite"
2 |
3 | import {db, questionCollection} from "../name"
4 | import {databases} from "./config"
5 |
6 |
7 | export default async function createQuestionCollection(){
8 | // create collection
9 | await databases.createCollection(db, questionCollection, questionCollection, [
10 | Permission.read("any"),
11 | Permission.read("users"),
12 | Permission.create("users"),
13 | Permission.update("users"),
14 | Permission.delete("users"),
15 | ])
16 | console.log("Question collection is created")
17 |
18 | //creating attributes and Indexes
19 |
20 | await Promise.all([
21 | databases.createStringAttribute(db, questionCollection, "title", 100, true),
22 | databases.createStringAttribute(db, questionCollection, "content", 10000, true),
23 | databases.createStringAttribute(db, questionCollection, "authorId", 50, true),
24 | databases.createStringAttribute(db, questionCollection, "tags", 50, true, undefined, true),
25 | databases.createStringAttribute(db, questionCollection, "attachmentId", 50, false),
26 | ]);
27 | console.log("Question Attributes created")
28 |
29 | // create Indexes
30 |
31 | /*
32 | await Promise.all([
33 | databases.createIndex(
34 | db,
35 | questionCollection,
36 | "title",
37 | IndexType.Fulltext,
38 | ["title"],
39 | ['asc']
40 | ),
41 | databases.createIndex(
42 | db,
43 | questionCollection,
44 | "content",
45 | IndexType.Fulltext,
46 | ["content"],
47 | ['asc']
48 | )
49 | ])
50 | */
51 | }
--------------------------------------------------------------------------------
/src/components/magicui/border-beam.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 |
3 | interface BorderBeamProps {
4 | className?: string;
5 | size?: number;
6 | duration?: number;
7 | borderWidth?: number;
8 | anchor?: number;
9 | colorFrom?: string;
10 | colorTo?: string;
11 | delay?: number;
12 | }
13 |
14 | export const BorderBeam = ({
15 | className,
16 | size = 200,
17 | duration = 15,
18 | anchor = 90,
19 | borderWidth = 1.5,
20 | colorFrom = "#ffaa40",
21 | colorTo = "#9c40ff",
22 | delay = 0,
23 | }: BorderBeamProps) => {
24 | return (
25 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/app/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AnimatedGridPattern from "@/components/magicui/animated-grid-pattern";
3 | import { cn } from "@/utils/cn";
4 | import Link from "next/link";
5 |
6 | const Footer = () => {
7 | const items = [
8 | {
9 | title: "Home",
10 | href: "/",
11 | },
12 | {
13 | title: "About",
14 | href: "/about",
15 | },
16 | {
17 | title: "Privacy Policy",
18 | href: "/privacy-policy",
19 | },
20 | {
21 | title: "Terms of Service",
22 | href: "/terms-of-service",
23 | },
24 | {
25 | title: "Questions",
26 | href: "/questions",
27 | },
28 | ];
29 | return (
30 |
52 | );
53 | };
54 |
55 | export default Footer;
56 |
--------------------------------------------------------------------------------
/src/components/magicui/animated-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AnimatePresence, motion } from "framer-motion";
4 | import React, { ReactElement, useEffect, useMemo, useState } from "react";
5 |
6 | export const AnimatedList = React.memo(
7 | ({
8 | className,
9 | children,
10 | delay = 1000,
11 | }: {
12 | className?: string;
13 | children: React.ReactNode;
14 | delay?: number;
15 | }) => {
16 | const [index, setIndex] = useState(0);
17 | const childrenArray = React.Children.toArray(children);
18 |
19 | useEffect(() => {
20 | const interval = setInterval(() => {
21 | setIndex(prevIndex => (prevIndex + 1) % childrenArray.length);
22 | }, delay);
23 |
24 | return () => clearInterval(interval);
25 | }, [childrenArray.length, delay]);
26 |
27 | const itemsToShow = useMemo(
28 | () => childrenArray.slice(0, index + 1).reverse(),
29 | [index, childrenArray]
30 | );
31 |
32 | return (
33 |
34 |
35 | {itemsToShow.map(item => (
36 | {item}
37 | ))}
38 |
39 |
40 | );
41 | }
42 | );
43 |
44 | AnimatedList.displayName = "AnimatedList";
45 |
46 | export function AnimatedListItem({ children }: { children: React.ReactNode }) {
47 | const animations = {
48 | initial: { scale: 0, opacity: 0 },
49 | animate: { scale: 1, opacity: 1, originY: 0 },
50 | exit: { scale: 0, opacity: 0 },
51 | transition: { type: "spring", stiffness: 350, damping: 40 },
52 | };
53 |
54 | return (
55 |
56 | {children}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/api/answer/route.ts:
--------------------------------------------------------------------------------
1 | import { answerCollection, db } from "@/models/name";
2 | import { databases, users } from "@/models/server/config";
3 | import { NextRequest, NextResponse } from "next/server";
4 | import { ID } from "node-appwrite";
5 | import {UserPrefs} from "@/store/Auth"
6 |
7 | export async function POST(request: NextRequest){
8 | try {
9 | const {questionId, answer, authorId} = await request.json();
10 |
11 | const response = await databases.createDocument(db, answerCollection, ID.unique(), {
12 | content: answer,
13 | authorId: authorId,
14 | questionId: questionId
15 | })
16 |
17 | // Increase author reputation
18 | const prefs = await users.getPrefs(authorId)
19 | await users.updatePrefs(authorId, {
20 | reputation: Number(prefs.reputation) + 1
21 | })
22 |
23 | return NextResponse.json(response, {
24 | status: 201
25 | })
26 |
27 | } catch (error: any) {
28 | return NextResponse.json(
29 | {
30 | error: error?.message || "Error creating answer"
31 | },
32 | {
33 | status: error?.status || error?.code || 500
34 | }
35 | )
36 | }
37 | }
38 |
39 | export async function DELETE(request: NextRequest){
40 | try {
41 | const {answerId} = await request.json()
42 |
43 | const answer = await databases.getDocument(db, answerCollection, answerId)
44 |
45 | const response = await databases.deleteDocument(db, answerCollection, answerId)
46 |
47 | //decrese the reputation
48 | const prefs = await users.getPrefs(answer.authorId)
49 | await users.updatePrefs(answer.authorId, {
50 | reputation: Number(prefs.reputation) - 1
51 | })
52 |
53 | return NextResponse.json(
54 | {data: response},
55 | {status: 200}
56 | )
57 |
58 |
59 |
60 | } catch (error: any) {
61 | return NextResponse.json(
62 | {
63 | message: error?.message || "Error deleting the answer"
64 | },
65 | {
66 | status: error?.status || error?.code || 500
67 | }
68 | )
69 | }
70 | }
--------------------------------------------------------------------------------
/src/components/Pagination.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
4 | import React from "react";
5 |
6 | const Pagination = ({
7 | className,
8 | total,
9 | limit,
10 | }: {
11 | className?: string;
12 | limit: number;
13 | total: number;
14 | }) => {
15 | const searchParams = useSearchParams();
16 | const page = searchParams.get("page") || "1";
17 | const totalPages = Math.ceil(total / limit);
18 | const router = useRouter();
19 | const pathnanme = usePathname();
20 |
21 | const prev = () => {
22 | if (page <= "1") return;
23 | const pageNumber = parseInt(page);
24 | const newSearchParams = new URLSearchParams(searchParams);
25 | newSearchParams.set("page", `${pageNumber - 1}`);
26 | router.push(`${pathnanme}?${newSearchParams}`);
27 | };
28 |
29 | const next = () => {
30 | if (page >= `${totalPages}`) return;
31 | const pageNumber = parseInt(page);
32 | const newSearchParams = new URLSearchParams(searchParams);
33 | newSearchParams.set("page", `${pageNumber + 1}`);
34 | router.push(`${pathnanme}?${newSearchParams}`);
35 | };
36 |
37 | return (
38 |
39 |
44 | Previous
45 |
46 |
47 | {page} of {totalPages || "1"} {/* incase totalPage is 0 */}
48 |
49 | = `${totalPages}`}
53 | >
54 | Next
55 |
56 |
57 | );
58 | };
59 |
60 | export default Pagination;
61 |
--------------------------------------------------------------------------------
/src/app/components/LatestQuestions.tsx:
--------------------------------------------------------------------------------
1 | import QuestionCard from "@/components/QuestionCard";
2 | import { answerCollection, db, questionCollection, voteCollection } from "@/models/name";
3 | import { databases, users } from "@/models/server/config";
4 | import { UserPrefs } from "@/store/Auth";
5 | import { Query } from "node-appwrite";
6 | import React from "react";
7 |
8 | const LatestQuestions = async () => {
9 | const questions = await databases.listDocuments(db, questionCollection, [
10 | Query.limit(5),
11 | Query.orderDesc("$createdAt"),
12 | ]);
13 | console.log("Fetched Questions:", questions);
14 |
15 | questions.documents = await Promise.all(
16 | questions.documents.map(async ques => {
17 | const [author, answers, votes] = await Promise.all([
18 | users.get(ques.authorId),
19 | databases.listDocuments(db, answerCollection, [
20 | Query.equal("questionId", ques.$id),
21 | Query.limit(1), // for optimization
22 | ]),
23 | databases.listDocuments(db, voteCollection, [
24 | Query.equal("type", "question"),
25 | Query.equal("typeId", ques.$id),
26 | Query.limit(1), // for optimization
27 | ]),
28 | ]);
29 |
30 | return {
31 | ...ques,
32 | totalAnswers: answers.total,
33 | totalVotes: votes.total,
34 | author: {
35 | $id: author.$id,
36 | reputation: author.prefs.reputation,
37 | name: author.name,
38 | },
39 | };
40 | })
41 | );
42 |
43 | console.log("Latest question")
44 | console.log(questions)
45 | return (
46 |
47 | {questions.documents.map(question => (
48 |
49 | ))}
50 |
51 | );
52 | };
53 |
54 | export default LatestQuestions;
55 |
--------------------------------------------------------------------------------
/src/components/magicui/shiny-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { type AnimationProps, motion } from "framer-motion";
3 |
4 | const animationProps = {
5 | initial: { "--x": "100%", scale: 0.8 },
6 | animate: { "--x": "-100%", scale: 1 },
7 | whileTap: { scale: 0.95 },
8 | transition: {
9 | repeat: Infinity,
10 | repeatType: "loop",
11 | repeatDelay: 1,
12 | type: "spring",
13 | stiffness: 20,
14 | damping: 15,
15 | mass: 2,
16 | scale: {
17 | type: "spring",
18 | stiffness: 200,
19 | damping: 5,
20 | mass: 0.5,
21 | },
22 | },
23 | } as AnimationProps;
24 |
25 | const ShinyButton = ({ text = "shiny-button" }) => {
26 | return (
27 |
31 |
38 | {text}
39 |
40 |
47 |
48 | );
49 | };
50 |
51 | export default ShinyButton;
52 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/answers/page.tsx:
--------------------------------------------------------------------------------
1 | import Pagination from "@/components/Pagination";
2 | import { MarkdownPreview } from "@/components/RTE";
3 | import { answerCollection, db, questionCollection } from "@/models/name";
4 | import { databases } from "@/models/server/config";
5 | import slugify from "@/utils/slugify";
6 | import Link from "next/link";
7 | import { Query } from "node-appwrite";
8 | import React from "react";
9 |
10 | const Page = async ({
11 | params,
12 | searchParams,
13 | }: {
14 | params: { userId: string; userSlug: string };
15 | searchParams: { page?: string };
16 | }) => {
17 | searchParams.page ||= "1";
18 |
19 | const queries = [
20 | Query.equal("authorId", params.userId),
21 | Query.orderDesc("$createdAt"),
22 | Query.offset((+searchParams.page - 1) * 25),
23 | Query.limit(25),
24 | ];
25 |
26 | const answers = await databases.listDocuments(db, answerCollection, queries);
27 |
28 | answers.documents = await Promise.all(
29 | answers.documents.map(async ans => {
30 | const question = await databases.getDocument(db, questionCollection, ans.questionId, [
31 | Query.select(["title"]),
32 | ]);
33 | return { ...ans, question };
34 | })
35 | );
36 |
37 | return (
38 |
39 |
40 |
{answers.total} answers
41 |
42 |
43 | {answers.documents.map(ans => (
44 |
45 |
46 |
47 |
48 |
52 | Question
53 |
54 |
55 | ))}
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default Page;
63 |
--------------------------------------------------------------------------------
/src/components/magicui/icon-cloud.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // import { useTheme } from "next-themes";
4 | import { useEffect, useMemo, useState } from "react";
5 | import { Cloud, fetchSimpleIcons, ICloud, renderSimpleIcon, SimpleIcon } from "react-icon-cloud";
6 |
7 | export const cloudProps: Omit = {
8 | containerProps: {
9 | style: {
10 | display: "flex",
11 | justifyContent: "center",
12 | alignItems: "center",
13 | width: "100%",
14 | paddingTop: 40,
15 | },
16 | },
17 | options: {
18 | reverse: true,
19 | depth: 1,
20 | wheelZoom: false,
21 | imageScale: 2,
22 | activeCursor: "default",
23 | tooltip: "native",
24 | initial: [0.1, -0.1],
25 | clickToFront: 500,
26 | tooltipDelay: 0,
27 | outlineColour: "#0000",
28 | maxSpeed: 0.04,
29 | minSpeed: 0.02,
30 | // dragControl: false,
31 | },
32 | };
33 |
34 | export const renderCustomIcon = (icon: SimpleIcon, theme: string) => {
35 | const bgHex = theme === "light" ? "#f3f2ef" : "#080510";
36 | const fallbackHex = theme === "light" ? "#6e6e73" : "#ffffff";
37 | const minContrastRatio = theme === "dark" ? 2 : 1.2;
38 |
39 | return renderSimpleIcon({
40 | icon,
41 | bgHex,
42 | fallbackHex,
43 | minContrastRatio,
44 | size: 42,
45 | aProps: {
46 | href: undefined,
47 | target: undefined,
48 | rel: undefined,
49 | onClick: e => e.preventDefault(),
50 | },
51 | });
52 | };
53 |
54 | export type DynamicCloudProps = {
55 | iconSlugs: string[];
56 | };
57 |
58 | type IconData = Awaited>;
59 |
60 | export default function IconCloud({ iconSlugs }: DynamicCloudProps) {
61 | const [data, setData] = useState(null);
62 | const { theme } = { theme: "dark" };
63 |
64 | useEffect(() => {
65 | fetchSimpleIcons({ slugs: iconSlugs }).then(setData);
66 | }, [iconSlugs]);
67 |
68 | const renderedIcons = useMemo(() => {
69 | if (!data) return null;
70 |
71 | return Object.values(data.simpleIcons).map(icon =>
72 | renderCustomIcon(icon, theme || "light")
73 | );
74 | }, [data, theme]);
75 |
76 | return (
77 | // @ts-ignore
78 |
79 | <>{renderedIcons}>
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/questions/page.tsx:
--------------------------------------------------------------------------------
1 | import Pagination from "@/components/Pagination";
2 | import QuestionCard from "@/components/QuestionCard";
3 | import { answerCollection, db, questionCollection, voteCollection } from "@/models/name";
4 | import { databases, users } from "@/models/server/config";
5 | import { UserPrefs } from "@/store/Auth";
6 | import { Query } from "node-appwrite";
7 | import React from "react";
8 |
9 | const Page = async ({
10 | params,
11 | searchParams,
12 | }: {
13 | params: { userId: string; userSlug: string };
14 | searchParams: { page?: string };
15 | }) => {
16 | searchParams.page ||= "1";
17 |
18 | const queries = [
19 | Query.equal("authorId", params.userId),
20 | Query.orderDesc("$createdAt"),
21 | Query.offset((+searchParams.page - 1) * 25),
22 | Query.limit(25),
23 | ];
24 |
25 | const questions = await databases.listDocuments(db, questionCollection, queries);
26 |
27 | questions.documents = await Promise.all(
28 | questions.documents.map(async ques => {
29 | const [author, answers, votes] = await Promise.all([
30 | users.get(ques.authorId),
31 | databases.listDocuments(db, answerCollection, [
32 | Query.equal("questionId", ques.$id),
33 | Query.limit(1), // for optimization
34 | ]),
35 | databases.listDocuments(db, voteCollection, [
36 | Query.equal("type", "question"),
37 | Query.equal("typeId", ques.$id),
38 | Query.limit(1), // for optimization
39 | ]),
40 | ]);
41 |
42 | return {
43 | ...ques,
44 | totalAnswers: answers.total,
45 | totalVotes: votes.total,
46 | author: {
47 | $id: author.$id,
48 | reputation: author.prefs.reputation,
49 | name: author.name,
50 | },
51 | };
52 | })
53 | );
54 |
55 | return (
56 |
57 |
58 |
{questions.total} questions
59 |
60 |
61 | {questions.documents.map(ques => (
62 |
63 | ))}
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default Page;
71 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { avatars } from "@/models/client/config";
2 | import { users } from "@/models/server/config";
3 | import { UserPrefs } from "@/store/Auth";
4 | import convertDateToRelativeTime from "@/utils/relativeTime";
5 | import React from "react";
6 | import EditButton from "./EditButton";
7 | import Navbar from "./Navbar";
8 | import { IconClockFilled, IconUserFilled } from "@tabler/icons-react";
9 |
10 | const Layout = async ({
11 | children,
12 | params,
13 | }: {
14 | children: React.ReactNode;
15 | params: { userId: string; userSlug: string };
16 | }) => {
17 | const user = await users.get(params.userId);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
{user.name}
35 |
{user.email}
36 |
37 | Dropped{" "}
38 | {convertDateToRelativeTime(new Date(user.$createdAt))},
39 |
40 |
41 | Last activity
42 | {convertDateToRelativeTime(new Date(user.$updatedAt))}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
{children}
54 |
55 |
56 | );
57 | };
58 |
59 | export default Layout;
60 |
--------------------------------------------------------------------------------
/src/components/magicui/shine-border.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | type TColorProp = `#${string}` | `#${string}`[];
6 | interface ShineBorderProps {
7 | borderRadius?: number;
8 | borderWidth?: number;
9 | duration?: number;
10 | color?: TColorProp;
11 | className?: string;
12 | children: React.ReactNode;
13 | }
14 |
15 | /**
16 | * @name Shine Border
17 | * @description It is an animated background border effect component with easy to use and configurable props.
18 | * @param borderRadius defines the radius of the border.
19 | * @param borderWidth defines the width of the border.
20 | * @param duration defines the animation duration to be applied on the shining border
21 | * @param color a string or string array to define border color.
22 | * @param className defines the class name to be applied to the component
23 | * @param children contains react node elements.
24 | */
25 | export default function ShineBorder({
26 | borderRadius = 8,
27 | borderWidth = 1,
28 | duration = 14,
29 | color = "#fff",
30 | className,
31 | children,
32 | }: ShineBorderProps) {
33 | return (
34 |
45 |
58 |
{children}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/components/TopContributers.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { AnimatedList } from "@/components/magicui/animated-list";
4 | import { users } from "@/models/server/config";
5 | import { Models, Query } from "node-appwrite";
6 | import { UserPrefs } from "@/store/Auth";
7 | import convertDateToRelativeTime from "@/utils/relativeTime";
8 | import { avatars } from "@/models/client/config";
9 |
10 | const Notification = ({ user }: { user: Models.User }) => {
11 | return (
12 |
23 |
24 |
25 |
30 |
31 |
32 |
33 | {user.name}
34 | ·
35 |
36 | {convertDateToRelativeTime(new Date(user.$updatedAt))}
37 |
38 |
39 |
40 | Reputation
41 | ·
42 | {user.prefs.reputation}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default async function TopContributers() {
51 | const topUsers = await users.list([Query.limit(10)]);
52 |
53 | return (
54 |
55 |
56 | {topUsers.users.map(user => (
57 |
58 | ))}
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/QuestionCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { BorderBeam } from "./magicui/border-beam";
5 | import Link from "next/link";
6 | import { Models } from "appwrite";
7 | import slugify from "@/utils/slugify";
8 | import { avatars } from "@/models/client/config";
9 | import convertDateToRelativeTime from "@/utils/relativeTime";
10 |
11 | const QuestionCard = ({ ques }: { ques: Models.Document }) => {
12 | const [height, setHeight] = React.useState(0);
13 | const ref = React.useRef(null);
14 |
15 | React.useEffect(() => {
16 | if (ref.current) {
17 | setHeight(ref.current.clientHeight);
18 | }
19 | }, [ref]);
20 |
21 | return (
22 |
26 |
27 |
28 |
{ques.totalVotes} votes
29 |
{ques.totalAnswers} answers
30 |
31 |
32 |
36 |
{ques.title}
37 |
38 |
39 | {ques.tags.map((tag: string) => (
40 |
45 | #{tag}
46 |
47 | ))}
48 |
49 |
50 |
55 |
56 |
60 | {ques.author.name}
61 |
62 |
"{ques.author.reputation}"
63 |
64 |
asked {convertDateToRelativeTime(new Date(ques.$createdAt))}
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default QuestionCard;
72 |
--------------------------------------------------------------------------------
/src/components/ui/wobble-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from "react";
3 | import { motion } from "framer-motion";
4 | import { cn } from "@/utils/cn";
5 |
6 | export const WobbleCard = ({
7 | children,
8 | containerClassName,
9 | className,
10 | }: {
11 | children: React.ReactNode;
12 | containerClassName?: string;
13 | className?: string;
14 | }) => {
15 | const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
16 | const [isHovering, setIsHovering] = useState(false);
17 |
18 | const handleMouseMove = (event: React.MouseEvent) => {
19 | const { clientX, clientY } = event;
20 | const rect = event.currentTarget.getBoundingClientRect();
21 | const x = (clientX - (rect.left + rect.width / 2)) / 20;
22 | const y = (clientY - (rect.top + rect.height / 2)) / 20;
23 | setMousePosition({ x, y });
24 | };
25 | return (
26 | setIsHovering(true)}
29 | onMouseLeave={() => {
30 | setIsHovering(false);
31 | setMousePosition({ x: 0, y: 0 });
32 | }}
33 | style={{
34 | transform: isHovering
35 | ? `translate3d(${mousePosition.x}px, ${mousePosition.y}px, 0) scale3d(1, 1, 1)`
36 | : "translate3d(0px, 0px, 0) scale3d(1, 1, 1)",
37 | transition: "transform 0.1s ease-out",
38 | }}
39 | className={cn(
40 | "relative mx-auto w-full overflow-hidden rounded-2xl bg-indigo-800",
41 | containerClassName
42 | )}
43 | >
44 |
51 |
60 |
61 | {children}
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | const Noise = () => {
69 | return (
70 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { databases, users } from "@/models/server/config";
2 | import { UserPrefs } from "@/store/Auth";
3 | import React from "react";
4 | import { MagicCard, MagicContainer } from "@/components/magicui/magic-card";
5 | import NumberTicker from "@/components/magicui/number-ticker";
6 | import { answerCollection, db, questionCollection } from "@/models/name";
7 | import { Query } from "node-appwrite";
8 |
9 | const Page = async ({ params }: { params: { userId: string; userSlug: string } }) => {
10 | const [user, questions, answers] = await Promise.all([
11 | users.get(params.userId),
12 | databases.listDocuments(db, questionCollection, [
13 | Query.equal("authorId", params.userId),
14 | Query.limit(1), // for optimization
15 | ]),
16 | databases.listDocuments(db, answerCollection, [
17 | Query.equal("authorId", params.userId),
18 | Query.limit(1), // for optimization
19 | ]),
20 | ]);
21 |
22 | return (
23 |
24 |
25 |
26 |
Reputation
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Questions asked
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
Answers given
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Page;
56 |
--------------------------------------------------------------------------------
/src/store/Auth.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { immer } from "zustand/middleware/immer";
3 | import { persist } from "zustand/middleware";
4 |
5 | import {AppwriteException, ID, Models} from "appwrite"
6 | import { account } from "@/models/client/config";
7 |
8 |
9 | export interface UserPrefs {
10 | reputation: number
11 | }
12 |
13 | interface IAuthStore {
14 | session: Models.Session | null;
15 | jwt: string | null
16 | user: Models.User | null
17 | hydrated: boolean
18 |
19 | setHydrated(): void;
20 | verfiySession(): Promise;
21 | login(
22 | email: string,
23 | password: string
24 | ): Promise<
25 | {
26 | success: boolean;
27 | error?: AppwriteException| null
28 | }>
29 | createAccount(
30 | name: string,
31 | email: string,
32 | password: string
33 | ): Promise<
34 | {
35 | success: boolean;
36 | error?: AppwriteException| null
37 | }>
38 | logout(): Promise
39 | }
40 |
41 |
42 | export const useAuthStore = create()(
43 | persist(
44 | immer((set) => ({
45 | session: null,
46 | jwt: null,
47 | user: null,
48 | hydrated: false,
49 |
50 | setHydrated() {
51 | set({hydrated: true})
52 | },
53 |
54 | async verfiySession() {
55 | try {
56 | const session = await account.getSession("current")
57 | set({session})
58 |
59 | } catch (error) {
60 | console.log(error)
61 | }
62 | },
63 |
64 | async login(email: string, password: string) {
65 | try {
66 | const session = await account.createEmailPasswordSession(email, password)
67 | const [user, {jwt}] = await Promise.all([
68 | account.get(),
69 | account.createJWT()
70 |
71 | ])
72 | if (!user.prefs?.reputation) await account.updatePrefs({
73 | reputation: 0
74 | })
75 |
76 | set({session, user, jwt})
77 |
78 | return { success: true}
79 |
80 | } catch (error) {
81 |
82 | console.log(error)
83 | return {
84 | success: false,
85 | error: error instanceof AppwriteException ? error: null,
86 |
87 | }
88 | }
89 | },
90 |
91 | async createAccount(name:string, email: string, password: string) {
92 | try {
93 | await account.create(ID.unique(), email, password, name)
94 | return {success: true}
95 | } catch (error) {
96 | console.log(error)
97 | return {
98 | success: false,
99 | error: error instanceof AppwriteException ? error: null,
100 |
101 | }
102 | }
103 | },
104 |
105 | async logout() {
106 | try {
107 | await account.deleteSessions()
108 | set({session: null, jwt: null, user: null})
109 |
110 | } catch (error) {
111 | console.log(error)
112 | }
113 | },
114 | })),
115 | {
116 | name: "auth",
117 | onRehydrateStorage(){
118 | return (state, error) => {
119 | if (!error) state?.setHydrated()
120 | }
121 | }
122 | }
123 | )
124 | )
--------------------------------------------------------------------------------
/src/app/questions/page.tsx:
--------------------------------------------------------------------------------
1 | import { databases, users } from "@/models/server/config";
2 | import { answerCollection, db, voteCollection, questionCollection } from "@/models/name";
3 | import { Query } from "node-appwrite";
4 | import React from "react";
5 | import Link from "next/link";
6 | import ShimmerButton from "@/components/magicui/shimmer-button";
7 | import QuestionCard from "@/components/QuestionCard";
8 | import { UserPrefs } from "@/store/Auth";
9 | import Pagination from "@/components/Pagination";
10 | import Search from "./Search";
11 |
12 | const Page = async ({
13 | searchParams,
14 | }: {
15 | searchParams: { page?: string; tag?: string; search?: string };
16 | }) => {
17 | searchParams.page ||= "1";
18 |
19 | const queries = [
20 | Query.orderDesc("$createdAt"),
21 | Query.offset((+searchParams.page - 1) * 25),
22 | Query.limit(25),
23 | ];
24 |
25 | if (searchParams.tag) queries.push(Query.equal("tags", searchParams.tag));
26 | if (searchParams.search)
27 | queries.push(
28 | Query.or([
29 | Query.search("title", searchParams.search),
30 | Query.search("content", searchParams.search),
31 | ])
32 | );
33 |
34 | const questions = await databases.listDocuments(db, questionCollection, queries);
35 | console.log("Questions", questions)
36 |
37 | questions.documents = await Promise.all(
38 | questions.documents.map(async ques => {
39 | const [author, answers, votes] = await Promise.all([
40 | users.get(ques.authorId),
41 | databases.listDocuments(db, answerCollection, [
42 | Query.equal("questionId", ques.$id),
43 | Query.limit(1), // for optimization
44 | ]),
45 | databases.listDocuments(db, voteCollection, [
46 | Query.equal("type", "question"),
47 | Query.equal("typeId", ques.$id),
48 | Query.limit(1), // for optimization
49 | ]),
50 | ]);
51 |
52 | return {
53 | ...ques,
54 | totalAnswers: answers.total,
55 | totalVotes: votes.total,
56 | author: {
57 | $id: author.$id,
58 | reputation: author.prefs.reputation,
59 | name: author.name,
60 | },
61 | };
62 | })
63 | );
64 |
65 | return (
66 |
67 |
68 |
All Questions
69 |
70 |
71 |
72 | Ask a question
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
{questions.total} questions
82 |
83 |
84 | {questions.documents.map(ques => (
85 |
86 | ))}
87 |
88 |
89 |
90 | );
91 | };
92 |
93 | export default Page;
94 |
--------------------------------------------------------------------------------
/src/components/magicui/shimmer-button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 | import React, { CSSProperties } from "react";
3 |
4 | export interface ShimmerButtonProps extends React.ButtonHTMLAttributes {
5 | shimmerColor?: string;
6 | shimmerSize?: string;
7 | borderRadius?: string;
8 | shimmerDuration?: string;
9 | background?: string;
10 | className?: string;
11 | children?: React.ReactNode;
12 | }
13 |
14 | const ShimmerButton = React.forwardRef(
15 | (
16 | {
17 | shimmerColor = "#ffffff",
18 | shimmerSize = "0.05em",
19 | shimmerDuration = "3s",
20 | borderRadius = "100px",
21 | background = "rgba(0, 0, 0, 1)",
22 | className,
23 | children,
24 | ...props
25 | },
26 | ref
27 | ) => {
28 | return (
29 |
48 | {/* spark container */}
49 |
55 | {/* spark */}
56 |
57 | {/* spark before */}
58 |
59 |
60 |
61 | {children}
62 |
63 | {/* Highlight */}
64 |
80 |
81 | {/* backdrop */}
82 |
87 |
88 | );
89 | }
90 | );
91 |
92 | ShimmerButton.displayName = "ShimmerButton";
93 |
94 | export default ShimmerButton;
95 |
--------------------------------------------------------------------------------
/src/components/ui/hero-parallax.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { motion, useScroll, useTransform, useSpring, MotionValue } from "framer-motion";
4 | import Image from "next/image";
5 | import Link from "next/link";
6 |
7 | export const HeroParallax = ({
8 | products,
9 | header,
10 | }: {
11 | header: React.ReactNode;
12 | products: {
13 | title: string;
14 | link: string;
15 | thumbnail: string;
16 | }[];
17 | }) => {
18 | const firstRow = products.slice(0, 5);
19 | const secondRow = products.slice(5, 10);
20 | const thirdRow = products.slice(10, 15);
21 | const ref = React.useRef(null);
22 | const { scrollYProgress } = useScroll({
23 | target: ref,
24 | offset: ["start start", "end start"],
25 | });
26 |
27 | const springConfig = { stiffness: 300, damping: 30, bounce: 100 };
28 |
29 | const translateX = useSpring(useTransform(scrollYProgress, [0, 1], [0, 1000]), springConfig);
30 | const translateXReverse = useSpring(
31 | useTransform(scrollYProgress, [0, 1], [0, -1000]),
32 | springConfig
33 | );
34 | const rotateX = useSpring(useTransform(scrollYProgress, [0, 0.2], [15, 0]), springConfig);
35 | const opacity = useSpring(useTransform(scrollYProgress, [0, 0.2], [0.2, 1]), springConfig);
36 | const rotateZ = useSpring(useTransform(scrollYProgress, [0, 0.2], [20, 0]), springConfig);
37 | const translateY = useSpring(
38 | useTransform(scrollYProgress, [0, 0.2], [-700, 500]),
39 | springConfig
40 | );
41 | return (
42 |
46 | {header}
47 |
56 |
57 | {firstRow.map(product => (
58 |
59 | ))}
60 |
61 |
62 | {secondRow.map(product => (
63 |
68 | ))}
69 |
70 |
71 | {thirdRow.map(product => (
72 |
73 | ))}
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | export const ProductCard = ({
81 | product,
82 | translate,
83 | }: {
84 | product: {
85 | title: string;
86 | link: string;
87 | thumbnail: string;
88 | };
89 | translate: MotionValue;
90 | }) => {
91 | return (
92 |
102 |
103 |
110 |
111 |
112 |
113 | {product.title}
114 |
115 |
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/src/components/Comments.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { databases } from "@/models/client/config";
4 | import { commentCollection, db } from "@/models/name";
5 | import { useAuthStore } from "@/store/Auth";
6 | import { cn } from "@/lib/utils"
7 | import convertDateToRelativeTime from "@/utils/relativeTime";
8 | import slugify from "@/utils/slugify";
9 | import { IconTrash } from "@tabler/icons-react";
10 | import { ID, Models } from "appwrite";
11 | import Link from "next/link";
12 | import React from "react";
13 |
14 | const Comments = ({
15 | comments: _comments,
16 | type,
17 | typeId,
18 | className,
19 | }: {
20 | comments: Models.DocumentList;
21 | type: "question" | "answer";
22 | typeId: string;
23 | className?: string;
24 | }) => {
25 | const [comments, setComments] = React.useState(_comments);
26 | const [newComment, setNewComment] = React.useState("");
27 | const { user } = useAuthStore();
28 |
29 | const handleSubmit = async (e: React.FormEvent) => {
30 | e.preventDefault();
31 | if (!newComment || !user) return;
32 |
33 | try {
34 | const response = await databases.createDocument(db, commentCollection, ID.unique(), {
35 | content: newComment,
36 | authorId: user.$id,
37 | type: type,
38 | typeId: typeId,
39 | });
40 |
41 | setNewComment(() => "");
42 | setComments(prev => ({
43 | total: prev.total + 1,
44 | documents: [{ ...response, author: user }, ...prev.documents],
45 | }));
46 | } catch (error: any) {
47 | window.alert(error?.message || "Error creating comment");
48 | }
49 | };
50 |
51 | const deleteComment = async (commentId: string) => {
52 | try {
53 | await databases.deleteDocument(db, commentCollection, commentId);
54 |
55 | setComments(prev => ({
56 | total: prev.total - 1,
57 | documents: prev.documents.filter(comment => comment.$id !== commentId),
58 | }));
59 | } catch (error: any) {
60 | window.alert(error?.message || "Error deleting comment");
61 | }
62 | };
63 |
64 | return (
65 |
66 | {comments.documents.map(comment => (
67 |
68 |
69 |
70 |
71 | {comment.content} -{" "}
72 |
76 | {comment.author.name}
77 | {" "}
78 |
79 | {convertDateToRelativeTime(new Date(comment.$createdAt))}
80 |
81 |
82 | {user?.$id === comment.authorId ? (
83 |
deleteComment(comment.$id)}
85 | className="shrink-0 text-red-500 hover:text-red-600"
86 | >
87 |
88 |
89 | ) : null}
90 |
91 |
92 | ))}
93 |
94 |
106 |
107 | );
108 | };
109 |
110 | export default Comments;
111 |
--------------------------------------------------------------------------------
/src/app/components/HeroSectionHeader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import IconCloud from "@/components/magicui/icon-cloud";
4 | import Particles from "@/components/magicui/particles";
5 | import ShimmerButton from "@/components/magicui/shimmer-button";
6 | import { useAuthStore } from "@/store/Auth";
7 | import Link from "next/link";
8 | import React from "react";
9 |
10 | const slugs = [
11 | "typescript",
12 | "javascript",
13 | "dart",
14 | "java",
15 | "react",
16 | "flutter",
17 | "android",
18 | "html5",
19 | "css3",
20 | "nodedotjs",
21 | "express",
22 | "nextdotjs",
23 | "prisma",
24 | "amazonaws",
25 | "postgresql",
26 | "firebase",
27 | "nginx",
28 | "vercel",
29 | "testinglibrary",
30 | "jest",
31 | "cypress",
32 | "docker",
33 | "git",
34 | "jira",
35 | "github",
36 | "gitlab",
37 | "visualstudiocode",
38 | "androidstudio",
39 | "sonarqube",
40 | "figma",
41 | ];
42 |
43 | const HeroSectionHeader = () => {
44 | const { session } = useAuthStore();
45 |
46 | return (
47 |
48 |
55 |
56 |
57 |
58 |
59 | RiverFlow
60 |
61 |
62 | Ask questions, share knowledge, and collaborate with developers
63 | worldwide. Join our community and enhance your coding skills!
64 |
65 |
66 | {session ? (
67 |
68 |
69 |
70 | Ask a question
71 |
72 |
73 |
74 | ) : (
75 | <>
76 |
77 |
78 |
79 | Sign up
80 |
81 |
82 |
83 |
87 | Login
88 |
89 |
90 | >
91 | )}
92 |
93 |
94 |
95 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default HeroSectionHeader;
106 |
--------------------------------------------------------------------------------
/src/components/VoteButtons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { databases } from "@/models/client/config";
4 | import { db, voteCollection } from "@/models/name";
5 | import { useAuthStore } from "@/store/Auth";
6 | import { cn } from "@/lib/utils";
7 | import { IconCaretUpFilled, IconCaretDownFilled } from "@tabler/icons-react";
8 | import { ID, Models, Query } from "appwrite";
9 | import { useRouter } from "next/navigation";
10 | import React from "react";
11 |
12 | const VoteButtons = ({
13 | type,
14 | id,
15 | upvotes,
16 | downvotes,
17 | className,
18 | }: {
19 | type: "question" | "answer";
20 | id: string;
21 | upvotes: Models.DocumentList;
22 | downvotes: Models.DocumentList;
23 | className?: string;
24 | }) => {
25 | const [votedDocument, setVotedDocument] = React.useState(); // undefined means not fetched yet
26 | const [voteResult, setVoteResult] = React.useState(upvotes.total - downvotes.total);
27 |
28 | const { user } = useAuthStore();
29 | const router = useRouter();
30 |
31 | React.useEffect(() => {
32 | (async () => {
33 | if (user) {
34 | const response = await databases.listDocuments(db, voteCollection, [
35 | Query.equal("type", type),
36 | Query.equal("typeId", id),
37 | Query.equal("votedById", user.$id),
38 | ]);
39 | setVotedDocument(() => response.documents[0] || null);
40 | }
41 | })();
42 | }, [user, id, type]);
43 |
44 | const toggleUpvote = async () => {
45 | if (!user) return router.push("/login");
46 |
47 | if (votedDocument === undefined) return;
48 |
49 | try {
50 | const response = await fetch(`/api/vote`, {
51 | method: "POST",
52 | body: JSON.stringify({
53 | votedById: user.$id,
54 | voteStatus: "upvoted",
55 | type,
56 | typeId: id,
57 | }),
58 | });
59 |
60 | const data = await response.json();
61 |
62 | if (!response.ok) throw data;
63 |
64 | setVoteResult(() => data.data.voteResult);
65 | setVotedDocument(() => data.data.document);
66 | } catch (error: any) {
67 | window.alert(error?.message || "Something went wrong");
68 | }
69 | };
70 |
71 | const toggleDownvote = async () => {
72 | if (!user) return router.push("/login");
73 |
74 | if (votedDocument === undefined) return;
75 |
76 | try {
77 | const response = await fetch(`/api/vote`, {
78 | method: "POST",
79 | body: JSON.stringify({
80 | votedById: user.$id,
81 | voteStatus: "downvoted",
82 | type,
83 | typeId: id,
84 | }),
85 | });
86 |
87 | const data = await response.json();
88 |
89 | if (!response.ok) throw data;
90 |
91 | setVoteResult(() => data.data.voteResult);
92 | setVotedDocument(() => data.data.document);
93 | } catch (error: any) {
94 | window.alert(error?.message || "Something went wrong");
95 | }
96 | };
97 |
98 | return (
99 |
100 |
109 |
110 |
111 | {voteResult}
112 |
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | export default VoteButtons;
128 |
--------------------------------------------------------------------------------
/src/components/ui/tracing-beam.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect, useRef, useState } from "react";
3 | import { motion, useTransform, useScroll, useSpring } from "framer-motion";
4 | import { cn } from "@/lib/utils";
5 |
6 | export const TracingBeam = ({
7 | children,
8 | className,
9 | }: {
10 | children: React.ReactNode;
11 | className?: string;
12 | }) => {
13 | const ref = useRef(null);
14 | const { scrollYProgress } = useScroll({
15 | target: ref,
16 | offset: ["start start", "end start"],
17 | });
18 |
19 | const contentRef = useRef(null);
20 | const [svgHeight, setSvgHeight] = useState(0);
21 |
22 | useEffect(() => {
23 | if (contentRef.current) {
24 | setSvgHeight(contentRef.current.offsetHeight);
25 | }
26 | }, []);
27 |
28 | const y1 = useSpring(useTransform(scrollYProgress, [0, 0.8], [50, svgHeight]), {
29 | stiffness: 500,
30 | damping: 90,
31 | });
32 | const y2 = useSpring(useTransform(scrollYProgress, [0, 1], [50, svgHeight - 200]), {
33 | stiffness: 500,
34 | damping: 90,
35 | });
36 |
37 | return (
38 |
39 |
40 | 0 ? "none" : "rgba(0, 0, 0, 0.24) 0px 3px 8px",
48 | }}
49 | className="border-netural-200 ml-[27px] flex h-4 w-4 items-center justify-center rounded-full border shadow-sm"
50 | >
51 | 0 ? "white" : "var(--emerald-500)",
59 | borderColor: scrollYProgress.get() > 0 ? "white" : "var(--emerald-600)",
60 | }}
61 | className="h-2 w-2 rounded-full border border-neutral-300 bg-white"
62 | />
63 |
64 |
71 |
80 |
90 |
91 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | {children}
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/src/components/ui/floating-navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from "react";
3 | import { motion, AnimatePresence, useScroll, useMotionValueEvent } from "framer-motion";
4 | import { cn } from "@/lib/utils";
5 | import Link from "next/link";
6 | import { useAuthStore } from "@/store/Auth";
7 |
8 | export const FloatingNav = ({
9 | navItems,
10 | className,
11 | }: {
12 | navItems: {
13 | name: string;
14 | link: string;
15 | icon?: JSX.Element;
16 | }[];
17 | className?: string;
18 | }) => {
19 | const { scrollYProgress, scrollY } = useScroll();
20 |
21 | const { session, logout } = useAuthStore();
22 |
23 | const [visible, setVisible] = useState(true);
24 |
25 | useMotionValueEvent(scrollYProgress, "change", current => {
26 | // Check if current is not undefined and is a number
27 | if (scrollY.get()! === 0) {
28 | setVisible(true);
29 | return;
30 | }
31 | if (typeof current === "number") {
32 | let direction = current! - scrollYProgress.getPrevious()!;
33 |
34 | if (scrollYProgress.get() < 0.05) {
35 | setVisible(false);
36 | } else {
37 | if (direction < 0) {
38 | setVisible(true);
39 | } else {
40 | setVisible(false);
41 | }
42 | }
43 | }
44 | });
45 |
46 | return (
47 |
48 |
65 | {navItems.map((navItem: any, idx: number) => (
66 |
73 | {navItem.icon}
74 | {navItem.name}
75 |
76 | ))}
77 | {session ? (
78 |
82 | Logout
83 |
84 |
85 | ) : (
86 | <>
87 |
91 | Login
92 |
93 |
94 |
98 | Signup
99 |
100 |
101 | >
102 | )}
103 |
104 |
105 | );
106 | };
107 |
--------------------------------------------------------------------------------
/src/components/magicui/neon-gradient-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/utils/cn";
4 | import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from "react";
5 |
6 | interface NeonColorsProps {
7 | firstColor: string;
8 | secondColor: string;
9 | }
10 |
11 | interface NeonGradientCardProps {
12 | /**
13 | * @default
14 | * @type ReactElement
15 | * @description
16 | * The component to be rendered as the card
17 | * */
18 | as?: ReactElement;
19 | /**
20 | * @default ""
21 | * @type string
22 | * @description
23 | * The className of the card
24 | */
25 | className?: string;
26 |
27 | /**
28 | * @default ""
29 | * @type ReactNode
30 | * @description
31 | * The children of the card
32 | * */
33 | children?: ReactNode;
34 |
35 | /**
36 | * @default 5
37 | * @type number
38 | * @description
39 | * The size of the border in pixels
40 | * */
41 | borderSize?: number;
42 |
43 | /**
44 | * @default 20
45 | * @type number
46 | * @description
47 | * The size of the radius in pixels
48 | * */
49 | borderRadius?: number;
50 |
51 | /**
52 | * @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
53 | * @type string
54 | * @description
55 | * The colors of the neon gradient
56 | * */
57 | neonColors?: NeonColorsProps;
58 |
59 | [key: string]: any;
60 | }
61 |
62 | const NeonGradientCard: React.FC = ({
63 | className,
64 | children,
65 | borderSize = 2,
66 | borderRadius = 20,
67 | neonColors = {
68 | firstColor: "#ff00aa",
69 | secondColor: "#00FFF1",
70 | },
71 | ...props
72 | }) => {
73 | const containerRef = useRef(null);
74 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
75 |
76 | useEffect(() => {
77 | const updateDimensions = () => {
78 | if (containerRef.current) {
79 | const { offsetWidth, offsetHeight } = containerRef.current;
80 | setDimensions({ width: offsetWidth, height: offsetHeight });
81 | }
82 | };
83 |
84 | updateDimensions();
85 | window.addEventListener("resize", updateDimensions);
86 |
87 | return () => {
88 | window.removeEventListener("resize", updateDimensions);
89 | };
90 | }, []);
91 |
92 | return (
93 |
113 |
127 | {children}
128 |
129 |
130 | );
131 | };
132 |
133 | export { NeonGradientCard };
134 |
--------------------------------------------------------------------------------
/src/components/magicui/animated-grid-pattern.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/utils/cn";
4 | import { motion } from "framer-motion";
5 | import { useEffect, useId, useRef, useState } from "react";
6 |
7 | interface GridPatternProps {
8 | width?: number;
9 | height?: number;
10 | x?: number;
11 | y?: number;
12 | strokeDasharray?: any;
13 | numSquares?: number;
14 | className?: string;
15 | maxOpacity?: number;
16 | duration?: number;
17 | repeatDelay?: number;
18 | }
19 |
20 | export function GridPattern({
21 | width = 40,
22 | height = 40,
23 | x = -1,
24 | y = -1,
25 | strokeDasharray = 0,
26 | numSquares = 50,
27 | className,
28 | maxOpacity = 0.5,
29 | duration = 4,
30 | repeatDelay = 0.5,
31 | ...props
32 | }: GridPatternProps) {
33 | const id = useId();
34 | const containerRef = useRef(null);
35 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
36 | const [squares, setSquares] = useState(() => generateSquares(numSquares));
37 |
38 | function getPos() {
39 | return [
40 | Math.floor((Math.random() * dimensions.width) / width),
41 | Math.floor((Math.random() * dimensions.height) / height),
42 | ];
43 | }
44 |
45 | // Adjust the generateSquares function to return objects with an id, x, and y
46 | function generateSquares(count: number) {
47 | return Array.from({ length: count }, (_, i) => ({
48 | id: i,
49 | pos: getPos(),
50 | }));
51 | }
52 |
53 | // Function to update a single square's position
54 | const updateSquarePosition = (id: number) => {
55 | setSquares(currentSquares =>
56 | currentSquares.map(sq =>
57 | sq.id === id
58 | ? {
59 | ...sq,
60 | pos: getPos(),
61 | }
62 | : sq
63 | )
64 | );
65 | };
66 |
67 | // Update squares to animate in
68 | useEffect(() => {
69 | if (dimensions.width && dimensions.height) {
70 | setSquares(generateSquares(numSquares));
71 | }
72 | }, [dimensions, numSquares]);
73 |
74 | // Resize observer to update container dimensions
75 | useEffect(() => {
76 | const resizeObserver = new ResizeObserver(entries => {
77 | for (let entry of entries) {
78 | setDimensions({
79 | width: entry.contentRect.width,
80 | height: entry.contentRect.height,
81 | });
82 | }
83 | });
84 |
85 | if (containerRef.current) {
86 | resizeObserver.observe(containerRef.current);
87 | }
88 |
89 | return () => {
90 | if (containerRef.current) {
91 | resizeObserver.unobserve(containerRef.current);
92 | }
93 | };
94 | }, [containerRef]);
95 |
96 | return (
97 |
106 |
107 |
115 |
120 |
121 |
122 |
123 |
124 | {squares.map(({ pos: [x, y], id }, index) => (
125 | updateSquarePosition(id)}
135 | key={`${x}-${y}-${index}`}
136 | width={width - 1}
137 | height={height - 1}
138 | x={x * width + 1}
139 | y={y * height + 1}
140 | fill="currentColor"
141 | strokeWidth="0"
142 | />
143 | ))}
144 |
145 |
146 | );
147 | }
148 |
149 | export default GridPattern;
150 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | const flattenColorPalette = require("tailwindcss/lib/util/flattenColorPalette");
3 | const svgToDataUri = require("mini-svg-data-uri");
4 |
5 | // This plugin adds each Tailwind color as a global CSS variable, e.g. var(--gray-200).
6 | function addVariablesForColors({ addBase, theme }: any) {
7 | let allColors = flattenColorPalette(theme("colors"));
8 | let newVars = Object.fromEntries(
9 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
10 | );
11 |
12 | addBase({
13 | ":root": newVars,
14 | });
15 | }
16 |
17 | const config: Config = {
18 | darkMode: "class",
19 | content: [
20 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
21 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
22 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
23 | ],
24 | theme: {
25 | extend: {
26 | boxShadow: {
27 | input: `0px 2px 3px -1px rgba(0,0,0,0.1), 0px 1px 0px 0px rgba(25,28,33,0.02), 0px 0px 0px 1px rgba(25,28,33,0.08)`,
28 | },
29 | backgroundImage: {
30 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
31 | "gradient-conic":
32 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
33 | },
34 | animation: {
35 | grid: "grid 15s linear infinite",
36 | "spin-around": "spin-around calc(var(--speed) * 2) infinite linear",
37 | slide: "slide var(--speed) ease-in-out infinite alternate",
38 | meteor: "meteor 5s linear infinite",
39 | shimmer: "shimmer 2s linear infinite",
40 | "border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
41 | backgroundPositionSpin: "background-position-spin 3000ms infinite alternate",
42 | },
43 | keyframes: {
44 | "background-position-spin": {
45 | "0%": { backgroundPosition: "top center" },
46 | "100%": { backgroundPosition: "bottom center" },
47 | },
48 | "border-beam": {
49 | "100%": {
50 | "offset-distance": "100%",
51 | },
52 | },
53 | grid: {
54 | "0%": { transform: "translateY(-50%)" },
55 | "100%": { transform: "translateY(0)" },
56 | },
57 | "spin-around": {
58 | "0%": {
59 | transform: "translateZ(0) rotate(0)",
60 | },
61 | "15%, 35%": {
62 | transform: "translateZ(0) rotate(90deg)",
63 | },
64 | "65%, 85%": {
65 | transform: "translateZ(0) rotate(270deg)",
66 | },
67 | "100%": {
68 | transform: "translateZ(0) rotate(360deg)",
69 | },
70 | },
71 | slide: {
72 | to: {
73 | transform: "translate(calc(100cqw - 100%), 0)",
74 | },
75 | },
76 | meteor: {
77 | "0%": { transform: "rotate(215deg) translateX(0)", opacity: "1" },
78 | "70%": { opacity: "1" },
79 | "100%": {
80 | transform: "rotate(215deg) translateX(-500px)",
81 | opacity: "0",
82 | },
83 | },
84 | shimmer: {
85 | from: {
86 | backgroundPosition: "0 0",
87 | },
88 | to: {
89 | backgroundPosition: "-200% 0",
90 | },
91 | },
92 | "shine-pulse": {
93 | "0%": {
94 | "background-position": "0% 0%",
95 | },
96 | "50%": {
97 | "background-position": "100% 100%",
98 | },
99 | to: {
100 | "background-position": "0% 0%",
101 | },
102 | },
103 | },
104 | },
105 | },
106 | plugins: [
107 | addVariablesForColors,
108 | function ({ matchUtilities, theme }: any) {
109 | matchUtilities(
110 | {
111 | "bg-dot-thick": (value: any) => ({
112 | backgroundImage: `url("${svgToDataUri(
113 | ` `
114 | )}")`,
115 | }),
116 | },
117 | { values: flattenColorPalette(theme("backgroundColor")), type: "color" }
118 | );
119 | },
120 | ],
121 | };
122 |
123 | export default config;
124 |
--------------------------------------------------------------------------------
/src/app/users/[userId]/[userSlug]/votes/page.tsx:
--------------------------------------------------------------------------------
1 | import Pagination from "@/components/Pagination";
2 | import { answerCollection, db, questionCollection, voteCollection } from "@/models/name";
3 | import { databases } from "@/models/server/config";
4 | import convertDateToRelativeTime from "@/utils/relativeTime";
5 | import slugify from "@/utils/slugify";
6 | import Link from "next/link";
7 | import { Query } from "node-appwrite";
8 | import React from "react";
9 |
10 | const Page = async ({
11 | params,
12 | searchParams,
13 | }: {
14 | params: { userId: string; userSlug: string };
15 | searchParams: { page?: string; voteStatus?: "upvoted" | "downvoted" };
16 | }) => {
17 | searchParams.page ||= "1";
18 |
19 | const query = [
20 | Query.equal("votedById", params.userId),
21 | Query.orderDesc("$createdAt"),
22 | Query.offset((+searchParams.page - 1) * 25),
23 | Query.limit(25),
24 | ];
25 |
26 | if (searchParams.voteStatus) query.push(Query.equal("voteStatus", searchParams.voteStatus));
27 |
28 | const votes = await databases.listDocuments(db, voteCollection, query);
29 |
30 | votes.documents = await Promise.all(
31 | votes.documents.map(async vote => {
32 | const questionOfTypeQuestion =
33 | vote.type === "question"
34 | ? await databases.getDocument(db, questionCollection, vote.typeId, [
35 | Query.select(["title"]),
36 | ])
37 | : null;
38 |
39 | if (questionOfTypeQuestion) {
40 | return {
41 | ...vote,
42 | question: questionOfTypeQuestion,
43 | };
44 | }
45 |
46 | const answer = await databases.getDocument(db, answerCollection, vote.typeId);
47 | const questionOfTypeAnswer = await databases.getDocument(
48 | db,
49 | questionCollection,
50 | answer.questionId,
51 | [Query.select(["title"])]
52 | );
53 |
54 | return {
55 | ...vote,
56 | question: questionOfTypeAnswer,
57 | };
58 | })
59 | );
60 |
61 | return (
62 |
63 |
64 |
{votes.total} votes
65 |
66 |
67 |
73 | All
74 |
75 |
76 |
77 |
85 | Upvotes
86 |
87 |
88 |
89 |
97 | Downvotes
98 |
99 |
100 |
101 |
102 |
103 | {votes.documents.map(vote => (
104 |
108 |
109 |
{vote.voteStatus}
110 |
111 |
115 | {vote.question.title}
116 |
117 |
118 |
119 |
120 | {convertDateToRelativeTime(new Date(vote.$createdAt))}
121 |
122 |
123 | ))}
124 |
125 |
126 |
127 | );
128 | };
129 |
130 | export default Page;
131 |
--------------------------------------------------------------------------------
/src/app/api/vote/route.ts:
--------------------------------------------------------------------------------
1 | import { answerCollection, db, questionCollection, voteCollection } from "@/models/name";
2 | import { databases, users } from "@/models/server/config";
3 | import { UserPrefs } from "@/store/Auth";
4 | import { NextRequest, NextResponse } from "next/server";
5 | import { ID, Query } from "node-appwrite";
6 |
7 | export async function POST(request: NextRequest) {
8 | try {
9 | const { votedById, voteStatus, type, typeId } = await request.json();
10 |
11 | const response = await databases.listDocuments(db, voteCollection, [
12 | Query.equal("type", type),
13 | Query.equal("typeId", typeId),
14 | Query.equal("votedById", votedById),
15 | ]);
16 |
17 | if (response.documents.length > 0) {
18 | await databases.deleteDocument(db, voteCollection, response.documents[0].$id);
19 |
20 | // Decrease the reputation of the question/answer author
21 | const questionOrAnswer = await databases.getDocument(
22 | db,
23 | type === "question" ? questionCollection : answerCollection,
24 | typeId
25 | );
26 |
27 | const authorPrefs = await users.getPrefs(questionOrAnswer.authorId);
28 |
29 | await users.updatePrefs(questionOrAnswer.authorId, {
30 | reputation:
31 | response.documents[0].voteStatus === "upvoted"
32 | ? Number(authorPrefs.reputation) - 1
33 | : Number(authorPrefs.reputation) + 1,
34 | });
35 | }
36 |
37 | // that means prev vote does not exists or voteStatus changed
38 | if (response.documents[0]?.voteStatus !== voteStatus) {
39 | const doc = await databases.createDocument(db, voteCollection, ID.unique(), {
40 | type,
41 | typeId,
42 | voteStatus,
43 | votedById,
44 | });
45 |
46 | // Increate/Decrease the reputation of the question/answer author accordingly
47 | const questionOrAnswer = await databases.getDocument(
48 | db,
49 | type === "question" ? questionCollection : answerCollection,
50 | typeId
51 | );
52 |
53 | const authorPrefs = await users.getPrefs(questionOrAnswer.authorId);
54 |
55 | // if vote was present
56 | if (response.documents[0]) {
57 | await users.updatePrefs(questionOrAnswer.authorId, {
58 | reputation:
59 | // that means prev vote was "upvoted" and new value is "downvoted" so we have to decrease the reputation
60 | response.documents[0].voteStatus === "upvoted"
61 | ? Number(authorPrefs.reputation) - 1
62 | : Number(authorPrefs.reputation) + 1,
63 | });
64 | } else {
65 | await users.updatePrefs(questionOrAnswer.authorId, {
66 | reputation:
67 | // that means prev vote was "upvoted" and new value is "downvoted" so we have to decrease the reputation
68 | voteStatus === "upvoted"
69 | ? Number(authorPrefs.reputation) + 1
70 | : Number(authorPrefs.reputation) - 1,
71 | });
72 | }
73 |
74 | const [upvotes, downvotes] = await Promise.all([
75 | databases.listDocuments(db, voteCollection, [
76 | Query.equal("type", type),
77 | Query.equal("typeId", typeId),
78 | Query.equal("voteStatus", "upvoted"),
79 | Query.equal("votedById", votedById),
80 | Query.limit(1), // for optimization as we only need total
81 | ]),
82 | databases.listDocuments(db, voteCollection, [
83 | Query.equal("type", type),
84 | Query.equal("typeId", typeId),
85 | Query.equal("voteStatus", "downvoted"),
86 | Query.equal("votedById", votedById),
87 | Query.limit(1), // for optimization as we only need total
88 | ]),
89 | ]);
90 |
91 | return NextResponse.json(
92 | {
93 | data: { document: doc, voteResult: upvotes.total - downvotes.total },
94 | message: response.documents[0] ? "Vote Status Updated" : "Voted",
95 | },
96 | {
97 | status: 201,
98 | }
99 | );
100 | }
101 |
102 | const [upvotes, downvotes] = await Promise.all([
103 | databases.listDocuments(db, voteCollection, [
104 | Query.equal("type", type),
105 | Query.equal("typeId", typeId),
106 | Query.equal("voteStatus", "upvoted"),
107 | Query.equal("votedById", votedById),
108 | Query.limit(1), // for optimization as we only need total
109 | ]),
110 | databases.listDocuments(db, voteCollection, [
111 | Query.equal("type", type),
112 | Query.equal("typeId", typeId),
113 | Query.equal("voteStatus", "downvoted"),
114 | Query.equal("votedById", votedById),
115 | Query.limit(1), // for optimization as we only need total
116 | ]),
117 | ]);
118 |
119 | return NextResponse.json(
120 | {
121 | data: {
122 | document: null, voteResult: upvotes.total - downvotes.total
123 | },
124 | message: "Vote Withdrawn",
125 | },
126 | {
127 | status: 200,
128 | }
129 | );
130 | } catch (error: any) {
131 | return NextResponse.json(
132 | { message: error?.message || "Error deleting answer" },
133 | { status: error?.status || error?.code || 500 }
134 | );
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/components/magicui/magic-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/utils/cn";
4 | import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from "react";
5 |
6 | interface MousePosition {
7 | x: number;
8 | y: number;
9 | }
10 |
11 | function useMousePosition(): MousePosition {
12 | const [mousePosition, setMousePosition] = useState({
13 | x: 0,
14 | y: 0,
15 | });
16 |
17 | useEffect(() => {
18 | const handleMouseMove = (event: globalThis.MouseEvent) => {
19 | setMousePosition({ x: event.clientX, y: event.clientY });
20 | };
21 |
22 | window.addEventListener("mousemove", handleMouseMove);
23 |
24 | return () => {
25 | window.removeEventListener("mousemove", handleMouseMove);
26 | };
27 | }, []);
28 |
29 | return mousePosition;
30 | }
31 |
32 | interface MagicContainerProps {
33 | children?: ReactNode;
34 | className?: any;
35 | }
36 |
37 | const MagicContainer = ({ children, className }: MagicContainerProps) => {
38 | const containerRef = useRef(null);
39 | const mousePosition = useMousePosition();
40 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
41 | const containerSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
42 | const [boxes, setBoxes] = useState>([]);
43 |
44 | useEffect(() => {
45 | init();
46 | containerRef.current &&
47 | setBoxes(Array.from(containerRef.current.children).map(el => el as HTMLElement));
48 | }, []);
49 |
50 | useEffect(() => {
51 | init();
52 | window.addEventListener("resize", init);
53 |
54 | return () => {
55 | window.removeEventListener("resize", init);
56 | };
57 | }, [setBoxes]);
58 |
59 | useEffect(() => {
60 | onMouseMove();
61 | }, [mousePosition]);
62 |
63 | const init = () => {
64 | if (containerRef.current) {
65 | containerSize.current.w = containerRef.current.offsetWidth;
66 | containerSize.current.h = containerRef.current.offsetHeight;
67 | }
68 | };
69 |
70 | const onMouseMove = () => {
71 | if (containerRef.current) {
72 | const rect = containerRef.current.getBoundingClientRect();
73 | const { w, h } = containerSize.current;
74 | const x = mousePosition.x - rect.left;
75 | const y = mousePosition.y - rect.top;
76 | const inside = x < w && x > 0 && y < h && y > 0;
77 |
78 | mouse.current.x = x;
79 | mouse.current.y = y;
80 | boxes.forEach(box => {
81 | const boxX = -(box.getBoundingClientRect().left - rect.left) + mouse.current.x;
82 | const boxY = -(box.getBoundingClientRect().top - rect.top) + mouse.current.y;
83 | box.style.setProperty("--mouse-x", `${boxX}px`);
84 | box.style.setProperty("--mouse-y", `${boxY}px`);
85 |
86 | if (inside) {
87 | box.style.setProperty("--opacity", `1`);
88 | } else {
89 | box.style.setProperty("--opacity", `0`);
90 | }
91 | });
92 | }
93 | };
94 |
95 | return (
96 |
97 | {children}
98 |
99 | );
100 | };
101 |
102 | interface MagicCardProps {
103 | /**
104 | * @default
105 | * @type ReactElement
106 | * @description
107 | * The component to be rendered as the card
108 | * */
109 | as?: ReactElement;
110 | /**
111 | * @default ""
112 | * @type string
113 | * @description
114 | * The className of the card
115 | */
116 | className?: string;
117 |
118 | /**
119 | * @default ""
120 | * @type ReactNode
121 | * @description
122 | * The children of the card
123 | * */
124 | children?: ReactNode;
125 |
126 | /**
127 | * @default 600
128 | * @type number
129 | * @description
130 | * The size of the spotlight effect in pixels
131 | * */
132 | size?: number;
133 |
134 | /**
135 | * @default true
136 | * @type boolean
137 | * @description
138 | * Whether to show the spotlight
139 | * */
140 | spotlight?: boolean;
141 |
142 | /**
143 | * @default "rgba(255,255,255,0.03)"
144 | * @type string
145 | * @description
146 | * The color of the spotlight
147 | * */
148 | spotlightColor?: string;
149 |
150 | /**
151 | * @default true
152 | * @type boolean
153 | * @description
154 | * Whether to isolate the card which is being hovered
155 | * */
156 | isolated?: boolean;
157 |
158 | /**
159 | * @default "rgba(255,255,255,0.03)"
160 | * @type string
161 | * @description
162 | * The background of the card
163 | * */
164 | background?: string;
165 |
166 | [key: string]: any;
167 | }
168 |
169 | const MagicCard: React.FC = ({
170 | className,
171 | children,
172 | size = 600,
173 | spotlight = true,
174 | borderColor = "hsl(0 0% 98%)",
175 | isolated = true,
176 | ...props
177 | }) => {
178 | return (
179 |
194 | {children}
195 |
196 | {/* Background */}
197 |
198 |
199 | );
200 | };
201 |
202 | export { MagicCard, MagicContainer };
203 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 |
8 | Get started by editing
9 | src/app/page.tsx
10 |
11 |
29 |
30 |
31 |
32 |
40 |
41 |
42 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Label } from "@/components/ui/label";
5 | import { Input } from "@/components/ui/input";
6 | import { cn } from "@/lib/utils";
7 | import { IconBrandGithub, IconBrandGoogle } from "@tabler/icons-react";
8 | import { useAuthStore } from "@/store/Auth";
9 | import Link from "next/link";
10 |
11 | const BottomGradient = () => {
12 | return (
13 | <>
14 |
15 |
16 | >
17 | );
18 | };
19 |
20 | const LabelInputContainer = ({
21 | children,
22 | className,
23 | }: {
24 | children: React.ReactNode;
25 | className?: string;
26 | }) => {
27 | return {children}
;
28 | };
29 |
30 | export default function Login() {
31 | const { login } = useAuthStore();
32 | const [isLoading, setIsLoading] = React.useState(false);
33 | const [error, setError] = React.useState("");
34 |
35 | const handleSubmit = async (e: React.FormEvent) => {
36 | e.preventDefault();
37 |
38 | const formData = new FormData(e.currentTarget);
39 | const email = formData.get("email");
40 | const password = formData.get("password");
41 |
42 | if (!email || !password) {
43 | setError(() => "Please fill out all fields");
44 | return;
45 | }
46 |
47 | setIsLoading(() => true);
48 | setError(() => "");
49 |
50 | const loginResponse = await login(email.toString(), password.toString());
51 | if (loginResponse.error) {
52 | setError(() => loginResponse.error!.message);
53 | }
54 |
55 | setIsLoading(() => false);
56 | };
57 |
58 | return (
59 |
60 |
61 | Login to Riverflow
62 |
63 |
64 | Login to riverflow
65 | If you don't have an account,{" "}
66 |
67 | register
68 | {" "}
69 | with riverflow
70 |
71 |
72 | {error && (
73 |
{error}
74 | )}
75 |
76 |
77 | Email Address
78 |
85 |
86 |
87 | Password
88 |
89 |
90 |
91 |
96 | Log in →
97 |
98 |
99 |
100 |
101 |
102 |
103 |
108 |
109 |
110 | Google
111 |
112 |
113 |
114 |
119 |
120 |
121 | GitHub
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/Answers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ID, Models } from "appwrite";
4 | import React from "react";
5 | import VoteButtons from "./VoteButtons";
6 | import { useAuthStore } from "@/store/Auth";
7 | import { avatars, databases } from "@/models/client/config";
8 | import { answerCollection, db } from "@/models/name";
9 | import RTE, { MarkdownPreview } from "./RTE";
10 | import Comments from "./Comments";
11 | import slugify from "@/utils/slugify";
12 | import Link from "next/link";
13 | import { IconTrash } from "@tabler/icons-react";
14 |
15 | const Answers = ({
16 | answers: _answers,
17 | questionId,
18 | }: {
19 | answers: Models.DocumentList;
20 | questionId: string;
21 | }) => {
22 | const [answers, setAnswers] = React.useState(_answers);
23 | const [newAnswer, setNewAnswer] = React.useState("");
24 | const { user } = useAuthStore();
25 |
26 | const handleSubmit = async (e: React.FormEvent) => {
27 | e.preventDefault();
28 | if (!newAnswer || !user) return;
29 |
30 | try {
31 | const response = await fetch("/api/answer", {
32 | method: "POST",
33 | body: JSON.stringify({
34 | questionId: questionId,
35 | answer: newAnswer,
36 | authorId: user.$id,
37 | }),
38 | });
39 |
40 | const data = await response.json();
41 |
42 | if (!response.ok) throw data;
43 |
44 | setNewAnswer(() => "");
45 | setAnswers(prev => ({
46 | total: prev.total + 1,
47 | documents: [
48 | {
49 | ...data,
50 | author: user,
51 | upvotesDocuments: { documents: [], total: 0 },
52 | downvotesDocuments: { documents: [], total: 0 },
53 | comments: { documents: [], total: 0 },
54 | },
55 | ...prev.documents,
56 | ],
57 | }));
58 | } catch (error: any) {
59 | window.alert(error?.message || "Error creating answer");
60 | }
61 | };
62 |
63 | const deleteAnswer = async (answerId: string) => {
64 | try {
65 | const response = await fetch("/api/answer", {
66 | method: "DELETE",
67 | body: JSON.stringify({
68 | answerId: answerId,
69 | }),
70 | });
71 |
72 | const data = await response.json();
73 |
74 | if (!response.ok) throw data;
75 |
76 | setAnswers(prev => ({
77 | total: prev.total - 1,
78 | documents: prev.documents.filter(answer => answer.$id !== answerId),
79 | }));
80 | } catch (error: any) {
81 | window.alert(error?.message || "Error deleting answer");
82 | }
83 | };
84 |
85 | return (
86 | <>
87 | {answers.total} Answers
88 | {answers.documents.map(answer => (
89 |
90 |
91 |
97 | {user?.$id === answer.authorId ? (
98 | deleteAnswer(answer.$id)}
101 | >
102 |
103 |
104 | ) : null}
105 |
106 |
107 |
108 |
109 |
110 |
115 |
116 |
117 |
121 | {answer.author.name}
122 |
123 |
124 | {answer.author.reputation}
125 |
126 |
127 |
128 |
134 |
135 |
136 |
137 | ))}
138 |
139 |
140 | Your Answer
141 | setNewAnswer(() => value || "")} />
142 |
143 | Post Your Answer
144 |
145 |
146 | >
147 | );
148 | };
149 |
150 | export default Answers;
151 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Label } from "@/components/ui/label";
5 | import { Input } from "@/components/ui/input";
6 | import { cn } from "@/lib/utils";
7 | import { IconBrandGithub, IconBrandGoogle } from "@tabler/icons-react";
8 | import { useAuthStore } from "@/store/Auth";
9 | import Link from "next/link";
10 |
11 | const BottomGradient = () => {
12 | return (
13 | <>
14 |
15 |
16 | >
17 | );
18 | };
19 |
20 | const LabelInputContainer = ({
21 | children,
22 | className,
23 | }: {
24 | children: React.ReactNode;
25 | className?: string;
26 | }) => {
27 | return {children}
;
28 | };
29 |
30 | export default function Register() {
31 | const { login, createAccount } = useAuthStore();
32 | const [isLoading, setIsLoading] = React.useState(false);
33 | const [error, setError] = React.useState("");
34 |
35 | const handleSubmit = async (e: React.FormEvent) => {
36 | e.preventDefault();
37 |
38 | const formData = new FormData(e.currentTarget);
39 | const firstname = formData.get("firstname");
40 | const lastname = formData.get("lastname");
41 | const email = formData.get("email");
42 | const password = formData.get("password");
43 |
44 | if (!firstname || !lastname || !email || !password) {
45 | setError(() => "Please fill out all fields");
46 | return;
47 | }
48 |
49 | setIsLoading(() => true);
50 | setError(() => "");
51 |
52 | const response = await createAccount(
53 | `${firstname} ${lastname}`,
54 | email.toString(),
55 | password.toString()
56 | );
57 |
58 | if (response.error) {
59 | setError(() => response.error!.message);
60 | } else {
61 | const loginResponse = await login(email.toString(), password.toString());
62 | if (loginResponse.error) {
63 | setError(() => loginResponse.error!.message);
64 | }
65 | }
66 |
67 | setIsLoading(() => false);
68 | };
69 |
70 | return (
71 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/src/components/magicui/particles.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef, useState } from "react";
4 |
5 | interface MousePosition {
6 | x: number;
7 | y: number;
8 | }
9 |
10 | function MousePosition(): MousePosition {
11 | const [mousePosition, setMousePosition] = useState({
12 | x: 0,
13 | y: 0,
14 | });
15 |
16 | useEffect(() => {
17 | const handleMouseMove = (event: MouseEvent) => {
18 | setMousePosition({ x: event.clientX, y: event.clientY });
19 | };
20 |
21 | window.addEventListener("mousemove", handleMouseMove);
22 |
23 | return () => {
24 | window.removeEventListener("mousemove", handleMouseMove);
25 | };
26 | }, []);
27 |
28 | return mousePosition;
29 | }
30 |
31 | interface ParticlesProps {
32 | className?: string;
33 | quantity?: number;
34 | staticity?: number;
35 | ease?: number;
36 | size?: number;
37 | refresh?: boolean;
38 | color?: string;
39 | vx?: number;
40 | vy?: number;
41 | }
42 | function hexToRgb(hex: string): number[] {
43 | hex = hex.replace("#", "");
44 | const hexInt = parseInt(hex, 16);
45 | const red = (hexInt >> 16) & 255;
46 | const green = (hexInt >> 8) & 255;
47 | const blue = hexInt & 255;
48 | return [red, green, blue];
49 | }
50 |
51 | const Particles: React.FC = ({
52 | className = "",
53 | quantity = 100,
54 | staticity = 50,
55 | ease = 50,
56 | size = 0.4,
57 | refresh = false,
58 | color = "#ffffff",
59 | vx = 0,
60 | vy = 0,
61 | }) => {
62 | const canvasRef = useRef(null);
63 | const canvasContainerRef = useRef(null);
64 | const context = useRef(null);
65 | const circles = useRef([]);
66 | const mousePosition = MousePosition();
67 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
68 | const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
69 | const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
70 |
71 | useEffect(() => {
72 | if (canvasRef.current) {
73 | context.current = canvasRef.current.getContext("2d");
74 | }
75 | initCanvas();
76 | animate();
77 | window.addEventListener("resize", initCanvas);
78 |
79 | return () => {
80 | window.removeEventListener("resize", initCanvas);
81 | };
82 | }, [color]);
83 |
84 | useEffect(() => {
85 | onMouseMove();
86 | }, [mousePosition.x, mousePosition.y]);
87 |
88 | useEffect(() => {
89 | initCanvas();
90 | }, [refresh]);
91 |
92 | const initCanvas = () => {
93 | resizeCanvas();
94 | drawParticles();
95 | };
96 |
97 | const onMouseMove = () => {
98 | if (canvasRef.current) {
99 | const rect = canvasRef.current.getBoundingClientRect();
100 | const { w, h } = canvasSize.current;
101 | const x = mousePosition.x - rect.left - w / 2;
102 | const y = mousePosition.y - rect.top - h / 2;
103 | const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
104 | if (inside) {
105 | mouse.current.x = x;
106 | mouse.current.y = y;
107 | }
108 | }
109 | };
110 |
111 | type Circle = {
112 | x: number;
113 | y: number;
114 | translateX: number;
115 | translateY: number;
116 | size: number;
117 | alpha: number;
118 | targetAlpha: number;
119 | dx: number;
120 | dy: number;
121 | magnetism: number;
122 | };
123 |
124 | const resizeCanvas = () => {
125 | if (canvasContainerRef.current && canvasRef.current && context.current) {
126 | circles.current.length = 0;
127 | canvasSize.current.w = canvasContainerRef.current.offsetWidth;
128 | canvasSize.current.h = canvasContainerRef.current.offsetHeight;
129 | canvasRef.current.width = canvasSize.current.w * dpr;
130 | canvasRef.current.height = canvasSize.current.h * dpr;
131 | canvasRef.current.style.width = `${canvasSize.current.w}px`;
132 | canvasRef.current.style.height = `${canvasSize.current.h}px`;
133 | context.current.scale(dpr, dpr);
134 | }
135 | };
136 |
137 | const circleParams = (): Circle => {
138 | const x = Math.floor(Math.random() * canvasSize.current.w);
139 | const y = Math.floor(Math.random() * canvasSize.current.h);
140 | const translateX = 0;
141 | const translateY = 0;
142 | const pSize = Math.floor(Math.random() * 2) + size;
143 | const alpha = 0;
144 | const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
145 | const dx = (Math.random() - 0.5) * 0.1;
146 | const dy = (Math.random() - 0.5) * 0.1;
147 | const magnetism = 0.1 + Math.random() * 4;
148 | return {
149 | x,
150 | y,
151 | translateX,
152 | translateY,
153 | size: pSize,
154 | alpha,
155 | targetAlpha,
156 | dx,
157 | dy,
158 | magnetism,
159 | };
160 | };
161 |
162 | const rgb = hexToRgb(color);
163 |
164 | const drawCircle = (circle: Circle, update = false) => {
165 | if (context.current) {
166 | const { x, y, translateX, translateY, size, alpha } = circle;
167 | context.current.translate(translateX, translateY);
168 | context.current.beginPath();
169 | context.current.arc(x, y, size, 0, 2 * Math.PI);
170 | context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
171 | context.current.fill();
172 | context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
173 |
174 | if (!update) {
175 | circles.current.push(circle);
176 | }
177 | }
178 | };
179 |
180 | const clearContext = () => {
181 | if (context.current) {
182 | context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h);
183 | }
184 | };
185 |
186 | const drawParticles = () => {
187 | clearContext();
188 | const particleCount = quantity;
189 | for (let i = 0; i < particleCount; i++) {
190 | const circle = circleParams();
191 | drawCircle(circle);
192 | }
193 | };
194 |
195 | const remapValue = (
196 | value: number,
197 | start1: number,
198 | end1: number,
199 | start2: number,
200 | end2: number
201 | ): number => {
202 | const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
203 | return remapped > 0 ? remapped : 0;
204 | };
205 |
206 | const animate = () => {
207 | clearContext();
208 | circles.current.forEach((circle: Circle, i: number) => {
209 | // Handle the alpha value
210 | const edge = [
211 | circle.x + circle.translateX - circle.size, // distance from left edge
212 | canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
213 | circle.y + circle.translateY - circle.size, // distance from top edge
214 | canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
215 | ];
216 | const closestEdge = edge.reduce((a, b) => Math.min(a, b));
217 | const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
218 | if (remapClosestEdge > 1) {
219 | circle.alpha += 0.02;
220 | if (circle.alpha > circle.targetAlpha) {
221 | circle.alpha = circle.targetAlpha;
222 | }
223 | } else {
224 | circle.alpha = circle.targetAlpha * remapClosestEdge;
225 | }
226 | circle.x += circle.dx + vx;
227 | circle.y += circle.dy + vy;
228 | circle.translateX +=
229 | (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease;
230 | circle.translateY +=
231 | (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease;
232 |
233 | drawCircle(circle, true);
234 |
235 | // circle gets out of the canvas
236 | if (
237 | circle.x < -circle.size ||
238 | circle.x > canvasSize.current.w + circle.size ||
239 | circle.y < -circle.size ||
240 | circle.y > canvasSize.current.h + circle.size
241 | ) {
242 | // remove the circle from the array
243 | circles.current.splice(i, 1);
244 | // create a new circle
245 | const newCircle = circleParams();
246 | drawCircle(newCircle);
247 | // update the circle position
248 | }
249 | });
250 | window.requestAnimationFrame(animate);
251 | };
252 |
253 | return (
254 |
255 |
256 |
257 | );
258 | };
259 |
260 | export default Particles;
261 |
--------------------------------------------------------------------------------
/src/components/ui/background-beams.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { motion } from "framer-motion";
4 | import { cn } from "@/lib/utils";
5 |
6 | export const BackgroundBeams = React.memo(({ className }: { className?: string }) => {
7 | const paths = [
8 | "M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875",
9 | "M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867",
10 | "M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859",
11 | "M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851",
12 | "M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843",
13 | "M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835",
14 | "M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827",
15 | "M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819",
16 | "M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811",
17 | "M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803",
18 | "M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795",
19 | "M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787",
20 | "M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779",
21 | "M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771",
22 | "M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763",
23 | "M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755",
24 | "M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747",
25 | "M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739",
26 | "M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731",
27 | "M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723",
28 | "M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715",
29 | "M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707",
30 | "M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699",
31 | "M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691",
32 | "M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683",
33 | "M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675",
34 | "M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667",
35 | "M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659",
36 | "M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651",
37 | "M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643",
38 | "M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635",
39 | "M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627",
40 | "M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619",
41 | "M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611",
42 | "M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603",
43 | "M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595",
44 | "M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587",
45 | "M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579",
46 | "M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571",
47 | "M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563",
48 | "M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555",
49 | "M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547",
50 | "M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539",
51 | "M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531",
52 | "M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523",
53 | "M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515",
54 | "M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507",
55 | "M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499",
56 | "M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491",
57 | "M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483",
58 | ];
59 | return (
60 |
66 |
74 |
80 |
81 | {paths.map((path, index) => (
82 |
89 | ))}
90 |
91 | {paths.map((path, index) => (
92 |
114 |
115 |
116 |
117 |
118 |
119 | ))}
120 |
121 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | });
138 |
139 | BackgroundBeams.displayName = "BackgroundBeams";
140 |
--------------------------------------------------------------------------------
/src/app/questions/[quesId]/[quesName]/page.tsx:
--------------------------------------------------------------------------------
1 | import Answers from "@/components/Answers";
2 | import Comments from "@/components/Comments";
3 | import { MarkdownPreview } from "@/components/RTE";
4 | import VoteButtons from "@/components/VoteButtons";
5 | import Particles from "@/components/magicui/particles";
6 | import ShimmerButton from "@/components/magicui/shimmer-button";
7 | import { avatars } from "@/models/client/config";
8 | import {
9 | answerCollection,
10 | db,
11 | voteCollection,
12 | questionCollection,
13 | commentCollection,
14 | questionAttachmentBucket,
15 | } from "@/models/name";
16 | import { databases, users } from "@/models/server/config";
17 | import { storage } from "@/models/client/config";
18 | import { UserPrefs } from "@/store/Auth";
19 | import convertDateToRelativeTime from "@/utils/relativeTime";
20 | import slugify from "@/utils/slugify";
21 | import { IconEdit } from "@tabler/icons-react";
22 | import Link from "next/link";
23 | import { Query } from "node-appwrite";
24 | import React from "react";
25 | import DeleteQuestion from "./DeleteQuestion";
26 | import EditQuestion from "./EditQuestion";
27 | import { TracingBeam } from "@/components/ui/tracing-beam";
28 |
29 | const Page = async ({ params }: { params: { quesId: string; quesName: string } }) => {
30 | const [question, answers, upvotes, downvotes, comments] = await Promise.all([
31 | databases.getDocument(db, questionCollection, params.quesId),
32 | databases.listDocuments(db, answerCollection, [
33 | Query.orderDesc("$createdAt"),
34 | Query.equal("questionId", params.quesId),
35 | ]),
36 | databases.listDocuments(db, voteCollection, [
37 | Query.equal("typeId", params.quesId),
38 | Query.equal("type", "question"),
39 | Query.equal("voteStatus", "upvoted"),
40 | Query.limit(1), // for optimization
41 | ]),
42 | databases.listDocuments(db, voteCollection, [
43 | Query.equal("typeId", params.quesId),
44 | Query.equal("type", "question"),
45 | Query.equal("voteStatus", "downvoted"),
46 | Query.limit(1), // for optimization
47 | ]),
48 | databases.listDocuments(db, commentCollection, [
49 | Query.equal("type", "question"),
50 | Query.equal("typeId", params.quesId),
51 | Query.orderDesc("$createdAt"),
52 | ]),
53 | ]);
54 |
55 | // since it is dependent on the question, we fetch it here outside of the Promise.all
56 | const author = await users.get(question.authorId);
57 | [comments.documents, answers.documents] = await Promise.all([
58 | Promise.all(
59 | comments.documents.map(async comment => {
60 | const author = await users.get(comment.authorId);
61 | return {
62 | ...comment,
63 | author: {
64 | $id: author.$id,
65 | name: author.name,
66 | reputation: author.prefs.reputation,
67 | },
68 | };
69 | })
70 | ),
71 | Promise.all(
72 | answers.documents.map(async answer => {
73 | const [author, comments, upvotes, downvotes] = await Promise.all([
74 | users.get(answer.authorId),
75 | databases.listDocuments(db, commentCollection, [
76 | Query.equal("typeId", answer.$id),
77 | Query.equal("type", "answer"),
78 | Query.orderDesc("$createdAt"),
79 | ]),
80 | databases.listDocuments(db, voteCollection, [
81 | Query.equal("typeId", answer.$id),
82 | Query.equal("type", "answer"),
83 | Query.equal("voteStatus", "upvoted"),
84 | Query.limit(1), // for optimization
85 | ]),
86 | databases.listDocuments(db, voteCollection, [
87 | Query.equal("typeId", answer.$id),
88 | Query.equal("type", "answer"),
89 | Query.equal("voteStatus", "downvoted"),
90 | Query.limit(1), // for optimization
91 | ]),
92 | ]);
93 |
94 | comments.documents = await Promise.all(
95 | comments.documents.map(async comment => {
96 | const author = await users.get(comment.authorId);
97 | return {
98 | ...comment,
99 | author: {
100 | $id: author.$id,
101 | name: author.name,
102 | reputation: author.prefs.reputation,
103 | },
104 | };
105 | })
106 | );
107 |
108 | return {
109 | ...answer,
110 | comments,
111 | upvotesDocuments: upvotes,
112 | downvotesDocuments: downvotes,
113 | author: {
114 | $id: author.$id,
115 | name: author.name,
116 | reputation: author.prefs.reputation,
117 | },
118 | };
119 | })
120 | ),
121 | ]);
122 |
123 | return (
124 |
125 |
132 |
133 |
134 |
135 |
{question.title}
136 |
137 |
138 | Asked {convertDateToRelativeTime(new Date(question.$createdAt))}
139 |
140 | Answer {answers.total}
141 | Votes {upvotes.total + downvotes.total}
142 |
143 |
144 |
145 |
146 |
147 | Ask a question
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
162 |
167 |
168 |
169 |
170 |
171 |
172 |
182 |
183 |
184 | {question.tags.map((tag: string) => (
185 |
190 | #{tag}
191 |
192 | ))}
193 |
194 |
195 |
196 |
201 |
202 |
203 |
207 | {author.name}
208 |
209 |
210 | {author.prefs.reputation}
211 |
212 |
213 |
214 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | );
227 | };
228 |
229 | export default Page;
230 |
--------------------------------------------------------------------------------
/src/components/QuestionForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import RTE from "@/components/RTE";
4 | import Meteors from "@/components/magicui/meteors";
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 | import { useAuthStore } from "@/store/Auth";
8 | import { cn } from "@/lib/utils";
9 | import slugify from "@/utils/slugify";
10 | import { IconX } from "@tabler/icons-react";
11 | import { Models, ID } from "appwrite";
12 | import { useRouter } from "next/navigation";
13 | import React from "react";
14 | import { databases, storage } from "@/models/client/config";
15 | import { db, questionAttachmentBucket, questionCollection } from "@/models/name";
16 | import { Confetti } from "@/components/magicui/confetti";
17 |
18 | const LabelInputContainer = ({
19 | children,
20 | className,
21 | }: {
22 | children: React.ReactNode;
23 | className?: string;
24 | }) => {
25 | return (
26 |
32 |
33 | {children}
34 |
35 | );
36 | };
37 |
38 | /**
39 | * ******************************************************************************
40 | * ![INFO]: for buttons, refer to https://ui.aceternity.com/components/tailwindcss-buttons
41 | * ******************************************************************************
42 | */
43 | const QuestionForm = ({ question }: { question?: Models.Document }) => {
44 | const { user } = useAuthStore();
45 | const [tag, setTag] = React.useState("");
46 | const router = useRouter();
47 |
48 | const [formData, setFormData] = React.useState({
49 | title: String(question?.title || ""),
50 | content: String(question?.content || ""),
51 | authorId: user?.$id,
52 | tags: new Set((question?.tags || []) as string[]),
53 | attachment: null as File | null,
54 | });
55 |
56 | const [loading, setLoading] = React.useState(false);
57 | const [error, setError] = React.useState("");
58 |
59 | const loadConfetti = (timeInMS = 3000) => {
60 | const end = Date.now() + timeInMS; // 3 seconds
61 | const colors = ["#a786ff", "#fd8bbc", "#eca184", "#f8deb1"];
62 |
63 | const frame = () => {
64 | if (Date.now() > end) return;
65 |
66 | Confetti({
67 | particleCount: 2,
68 | angle: 60,
69 | spread: 55,
70 | startVelocity: 60,
71 | origin: { x: 0, y: 0.5 },
72 | colors: colors,
73 | });
74 | Confetti({
75 | particleCount: 2,
76 | angle: 120,
77 | spread: 55,
78 | startVelocity: 60,
79 | origin: { x: 1, y: 0.5 },
80 | colors: colors,
81 | });
82 |
83 | requestAnimationFrame(frame);
84 | };
85 |
86 | frame();
87 | };
88 |
89 | const create = async () => {
90 | if (!formData.attachment) throw new Error("Please upload an image");
91 |
92 | const storageResponse = await storage.createFile(
93 | questionAttachmentBucket,
94 | ID.unique(),
95 | formData.attachment
96 | );
97 |
98 | const response = await databases.createDocument(db, questionCollection, ID.unique(), {
99 | title: formData.title,
100 | content: formData.content,
101 | authorId: formData.authorId,
102 | tags: Array.from(formData.tags),
103 | attachmentId: storageResponse.$id,
104 | });
105 |
106 | loadConfetti();
107 |
108 | return response;
109 | };
110 |
111 | const update = async () => {
112 | if (!question) throw new Error("Please provide a question");
113 |
114 | const attachmentId = await (async () => {
115 | if (!formData.attachment) return question?.attachmentId as string;
116 |
117 | await storage.deleteFile(questionAttachmentBucket, question.attachmentId);
118 |
119 | const file = await storage.createFile(
120 | questionAttachmentBucket,
121 | ID.unique(),
122 | formData.attachment
123 | );
124 |
125 | return file.$id;
126 | })();
127 |
128 | const response = await databases.updateDocument(db, questionCollection, question.$id, {
129 | title: formData.title,
130 | content: formData.content,
131 | authorId: formData.authorId,
132 | tags: Array.from(formData.tags),
133 | attachmentId: attachmentId,
134 | });
135 |
136 | return response;
137 | };
138 |
139 | const submit = async (e: React.FormEvent) => {
140 | e.preventDefault();
141 |
142 | // didn't check for attachment because it's optional in updating
143 | if (!formData.title || !formData.content || !formData.authorId) {
144 | setError(() => "Please fill out all fields");
145 | return;
146 | }
147 |
148 | setLoading(() => true);
149 | setError(() => "");
150 |
151 | try {
152 | const response = question ? await update() : await create();
153 |
154 | router.push(`/questions/${response.$id}/${slugify(formData.title)}`);
155 | } catch (error: any) {
156 | setError(() => error.message);
157 | }
158 |
159 | setLoading(() => false);
160 | };
161 |
162 | return (
163 |
164 | {error && (
165 |
166 |
167 | {error}
168 |
169 |
170 | )}
171 |
172 |
173 | Title Address
174 |
175 |
176 | Be specific and imagine you're asking a question to another person.
177 |
178 |
179 | setFormData(prev => ({ ...prev, title: e.target.value }))}
186 | />
187 |
188 |
189 |
190 | What are the details of your problem?
191 |
192 |
193 | Introduce the problem and expand on what you put in the title. Minimum 20
194 | characters.
195 |
196 |
197 | setFormData(prev => ({ ...prev, content: value || "" }))}
200 | />
201 |
202 |
203 |
204 | Image
205 |
206 |
207 | Add image to your question to make it more clear and easier to understand.
208 |
209 |
210 | {
217 | const files = e.target.files;
218 | if (!files || files.length === 0) return;
219 | setFormData(prev => ({
220 | ...prev,
221 | attachment: files[0],
222 | }));
223 | }}
224 | />
225 |
226 |
227 |
228 | Tags
229 |
230 |
231 | Add tags to describe what your question is about. Start typing to see
232 | suggestions.
233 |
234 |
235 |
236 |
237 | setTag(() => e.target.value)}
244 | />
245 |
246 |
{
250 | if (tag.length === 0) return;
251 | setFormData(prev => ({
252 | ...prev,
253 | tags: new Set([...Array.from(prev.tags), tag]),
254 | }));
255 | setTag(() => "");
256 | }}
257 | >
258 |
259 | Add
260 |
261 |
262 |
263 | {Array.from(formData.tags).map((tag, index) => (
264 |
265 |
266 |
267 |
268 |
269 |
270 | {tag}
271 | {
273 | setFormData(prev => ({
274 | ...prev,
275 | tags: new Set(
276 | Array.from(prev.tags).filter(t => t !== tag)
277 | ),
278 | }));
279 | }}
280 | type="button"
281 | >
282 |
283 |
284 |
285 |
286 |
287 |
288 | ))}
289 |
290 |
291 |
296 | {question ? "Update" : "Publish"}
297 |
298 |
299 | );
300 | };
301 |
302 | export default QuestionForm;
303 |
--------------------------------------------------------------------------------