├── 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 | 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 |
28 |
29 |
30 | 31 |
32 |
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 |
26 | setSearch(e.target.value)} 31 | /> 32 | 35 |
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 |
13 |
26 |
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 |
31 |
32 |
    33 | {items.map(item => ( 34 |
  • 35 | {item.title} 36 |
  • 37 | ))} 38 |
39 |
© {new Date().getFullYear()} Riverpod
40 |
41 | 51 |
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 | 46 | 47 | {page} of {totalPages || "1"} {/* incase totalPage is 0 */} 48 | 49 | 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 | {user.name} 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 | {user.name} 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 | {ques.author.name} 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 | 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 | {product.title} 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 | 89 | ) : null} 90 |
91 |
92 | ))} 93 |
94 |
95 |