├── .prettierignore ├── types.d.ts ├── src ├── components │ ├── ui │ │ ├── header │ │ │ ├── index.ts │ │ │ ├── user-button.tsx │ │ │ ├── login-button.tsx │ │ │ └── header.tsx │ │ ├── post-author-skeleton.tsx │ │ ├── comments │ │ │ ├── comment-author-skeleton.tsx │ │ │ ├── comment-section-skeleton.tsx │ │ │ ├── comments-list.tsx │ │ │ ├── comment-text.tsx │ │ │ ├── comment-skeleton.tsx │ │ │ ├── comment-author.tsx │ │ │ ├── comment.tsx │ │ │ ├── comment-section.tsx │ │ │ └── comment-options.tsx │ │ ├── icons.tsx │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── post-card.tsx │ │ ├── delete-post-button.tsx │ │ ├── edit-post-button.tsx │ │ ├── post-author.tsx │ │ ├── theme-switch.tsx │ │ ├── confirm-dialog.tsx │ │ ├── card.tsx │ │ ├── text.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ └── dropdown-menu.tsx │ └── forms │ │ ├── sign-in-form.tsx │ │ ├── sign-up-form.tsx │ │ ├── create-post-form.tsx │ │ ├── edit-comment-form.tsx │ │ ├── edit-post-form.tsx │ │ └── create-comment-form.tsx ├── app │ ├── favicon.ico │ ├── globals.css │ ├── opengraph-image.jpg │ ├── providers.tsx │ ├── posts │ │ └── [slug] │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── new │ │ └── page.tsx │ ├── (auth) │ │ ├── sign-up │ │ │ └── [[...sign-up]] │ │ │ │ └── page.tsx │ │ └── sign-in │ │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── _actions │ │ ├── comment.actions.ts │ │ └── post.actions.ts │ ├── page.tsx │ ├── layout.tsx │ └── api │ │ └── webhooks │ │ └── user │ │ └── route.ts ├── lib │ ├── utils.ts │ ├── hooks │ │ └── useClerkAppearance.ts │ └── validations │ │ ├── comment.schema.ts │ │ └── post.schema.ts ├── types │ └── clerk.d.ts ├── config │ └── site.ts ├── db │ ├── index.ts │ └── schema.ts └── middleware.ts ├── public └── images │ ├── logo.png │ └── avatar.webp ├── postcss.config.js ├── drizzle.config.ts ├── .env.example ├── components.json ├── next.config.js ├── .gitignore ├── .prettierrc ├── tsconfig.json ├── tailwind.config.js ├── .eslintrc.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | .next 3 | node_modules -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tailwindcss-animate"; 2 | -------------------------------------------------------------------------------- /src/components/ui/header/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from "./header"; 2 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leojuriolli7/ribbit/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leojuriolli7/ribbit/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leojuriolli7/ribbit/HEAD/public/images/avatar.webp -------------------------------------------------------------------------------- /src/app/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leojuriolli7/ribbit/HEAD/src/app/opengraph-image.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /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/types/clerk.d.ts: -------------------------------------------------------------------------------- 1 | import _Types from "@clerk/types"; 2 | 3 | declare global { 4 | interface UserPublicMetadata { 5 | /** 6 | * This is the Clerk user id inside 7 | * the project's MySQL database. 8 | */ 9 | databaseId: number; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "Ribbit", 3 | description: "Next.js 13 demo", 4 | keywords: [ 5 | "Next.js", 6 | "React", 7 | "Tailwind CSS", 8 | "Server Components", 9 | "Server Actions", 10 | "Ribbit", 11 | ], 12 | url: "https://ribbit-zeta.vercel.app", 13 | }; 14 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import * as dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export default { 7 | schema: "./src/db/schema.ts", 8 | driver: "mysql2", 9 | out: "./drizzle", 10 | dbCredentials: { 11 | connectionString: process.env.DATABASE_URL || "", 12 | }, 13 | } satisfies Config; 14 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider } from "next-themes"; 4 | 5 | type Props = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | export function Providers({ children }: Props) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/post-author-skeleton.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Skeleton } from "./skeleton"; 4 | 5 | export const PostAuthorSkeleton = () => { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/ui/comments/comment-author-skeleton.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Skeleton } from "../skeleton"; 4 | 5 | export const CommentAuthorSkeleton = () => { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Planetscale DB credentials 2 | DATABASE_HOST= 3 | DATABASE_USERNAME= 4 | DATABASE_PASSWORD= 5 | DATABASE_URL= 6 | 7 | # Clerk auth credentials 8 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 9 | CLERK_SECRET_KEY= 10 | 11 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 12 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 13 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ 14 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ -------------------------------------------------------------------------------- /src/app/posts/[slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | export default function LoadingPost() { 4 | return ( 5 | <> 6 | 7 |
8 | 9 | 10 |
11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "./src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ui/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Loader2, 3 | Plus, 4 | type Icon as LucideIcon, 5 | LogIn, 6 | PencilIcon, 7 | X as CloseIcon, 8 | Trash2Icon, 9 | } from "lucide-react"; 10 | 11 | export type Icon = LucideIcon; 12 | 13 | export const Icons = { 14 | spinner: Loader2, 15 | plus: Plus, 16 | login: LogIn, 17 | edit: PencilIcon, 18 | close: CloseIcon, 19 | delete: Trash2Icon, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
15 | ); 16 | } 17 | 18 | export { Skeleton }; 19 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/planetscale-serverless"; 2 | import { connect } from "@planetscale/database"; 3 | 4 | import * as schema from "./schema"; 5 | 6 | const connection = connect({ 7 | host: process.env["DATABASE_HOST"], 8 | username: process.env["DATABASE_USERNAME"], 9 | password: process.env["DATABASE_PASSWORD"], 10 | }); 11 | 12 | export const db = drizzle(connection, { schema }); 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true, 5 | }, 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: "https", 10 | hostname: "img.clerk.com", 11 | }, 12 | { 13 | protocol: "https", 14 | hostname: "ribbit-zeta.vercel.app", 15 | }, 16 | ], 17 | }, 18 | }; 19 | 20 | module.exports = nextConfig; 21 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs/server"; 2 | 3 | export default authMiddleware({ 4 | // Public routes are routes that don't require authentication 5 | publicRoutes: [ 6 | "/", 7 | "/posts(.*)", 8 | "/signin(.*)", 9 | "/signup(.*)", 10 | "/sso-callback(.*)", 11 | "/api/webhooks/user", 12 | ], 13 | }); 14 | 15 | export const config = { 16 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api)(.*)"], 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/new/page.tsx: -------------------------------------------------------------------------------- 1 | import Text from "@/components/ui/text"; 2 | import { CreatePostForm } from "@/components/forms/create-post-form"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Create a post", 7 | }; 8 | 9 | export default function Home() { 10 | return ( 11 |
12 | 13 | Create a post 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ui/comments/comment-section-skeleton.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { CommentSkeleton } from "./comment-skeleton"; 4 | 5 | const COMMENTS = Array.from({ length: 4 }); 6 | 7 | export const CommentSectionSkeleton = () => { 8 | return ( 9 |
10 |
11 | {COMMENTS?.map((_, i) => { 12 | return ; 13 | })} 14 |
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/ui/comments/comments-list.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import type { CommentWithChildren } from "./comment-section"; 4 | import { Comment } from "./comment"; 5 | 6 | export const CommentsList = ({ 7 | comments, 8 | }: { 9 | comments: CommentWithChildren[]; 10 | }) => { 11 | return ( 12 |
    13 | {comments?.map((comment) => { 14 | return ; 15 | })} 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/hooks/useClerkAppearance.ts: -------------------------------------------------------------------------------- 1 | import { dark } from "@clerk/themes"; 2 | import { useTheme } from "next-themes"; 3 | 4 | /** Get the clerk appearance props (Light mode, dark mode & primary color) */ 5 | export default function useClerkAppearance() { 6 | const { resolvedTheme: theme } = useTheme(); 7 | const clerkWidgetTheme = theme === "dark" ? dark : undefined; 8 | 9 | return { 10 | baseTheme: clerkWidgetTheme, 11 | variables: { 12 | colorPrimary: "#22c55e", 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/comments/comment-text.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import Text from "../text"; 5 | 6 | type Props = { 7 | text: string; 8 | id: number; 9 | }; 10 | 11 | export const CommentText = ({ id, text }: Props) => { 12 | const params = useSearchParams(); 13 | 14 | const currentlyEditing = params.get("editingComment"); 15 | const hideText = currentlyEditing === String(id); 16 | 17 | return !hideText && {text}; 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from "@/components/forms/sign-up-form"; 2 | import type { Metadata } from "next"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export const metadata: Metadata = { 7 | title: "Sign up", 8 | }; 9 | export default function SignUpPage({ 10 | searchParams, 11 | }: { 12 | searchParams: { 13 | redirectUrl?: string; 14 | }; 15 | }) { 16 | const { redirectUrl } = searchParams || {}; 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignInForm } from "@/components/forms/sign-in-form"; 2 | import type { Metadata } from "next"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export const metadata: Metadata = { 7 | title: "Sign in", 8 | }; 9 | 10 | export default function SignInPage({ 11 | searchParams, 12 | }: { 13 | searchParams: { 14 | redirectUrl?: string; 15 | }; 16 | }) { 17 | const { redirectUrl } = searchParams || {}; 18 | 19 | return ; 20 | } 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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/components/ui/header/user-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { UserButton as ClerkUserButton } from "@clerk/nextjs"; 4 | import useClerkAppearance from "@/lib/hooks/useClerkAppearance"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export const UserButton = () => { 8 | const appearance = useClerkAppearance(); 9 | const pathname = usePathname(); 10 | 11 | return ( 12 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/validations/comment.schema.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const createCommentSchema = z.object({ 4 | text: z.string().trim().min(3, "Required"), 5 | userId: z.number(), 6 | postId: z.number(), 7 | slug: z.string().nonempty(), 8 | parentId: z.number().optional(), 9 | }); 10 | 11 | export type CreateCommentInput = z.infer; 12 | 13 | export const editCommentSchema = z.object({ 14 | text: z.string().trim().min(3, "Required"), 15 | commentId: z.number(), 16 | slug: z.string().nonempty(), 17 | }); 18 | 19 | export type EditCommentInput = z.infer; 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "singleAttributePerLine": false, 8 | "bracketSameLine": false, 9 | "jsxBracketSameLine": false, 10 | "jsxSingleQuote": false, 11 | "printWidth": 80, 12 | "proseWrap": "preserve", 13 | "quoteProps": "as-needed", 14 | "requirePragma": false, 15 | "semi": true, 16 | "singleQuote": false, 17 | "tabWidth": 2, 18 | "trailingComma": "es5", 19 | "useTabs": false, 20 | "embeddedLanguageFormatting": "auto", 21 | "vueIndentScriptAndStyle": false 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/validations/post.schema.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const createPostSchema = z.object({ 4 | title: z.string().trim().min(1, "Required"), 5 | description: z.string().trim().min(3, "Minimum of 3 characters"), 6 | userId: z.number(), 7 | }); 8 | 9 | export type CreatePostInput = z.infer; 10 | 11 | export const editPostSchema = z.object({ 12 | title: z.string().trim().min(1, "Required"), 13 | description: z.string().trim().min(3, "Minimum of 3 characters"), 14 | slug: z.string(), 15 | userId: z.number(), 16 | }); 17 | 18 | export type EditPostInput = z.infer; 19 | -------------------------------------------------------------------------------- /src/components/ui/comments/comment-skeleton.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Skeleton } from "../skeleton"; 4 | 5 | export const CommentSkeleton = () => { 6 | return ( 7 |
8 |
9 |
10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/ui/header/login-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | import { Icons } from "../icons"; 6 | import { Button } from "../button"; 7 | 8 | export const LoginButton = () => { 9 | const pathname = usePathname(); 10 | 11 | return ( 12 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/forms/sign-in-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useClerkAppearance from "@/lib/hooks/useClerkAppearance"; 4 | import { SignIn } from "@clerk/nextjs"; 5 | 6 | export const SignInForm = ({ redirectUrl }: { redirectUrl?: string }) => { 7 | const appearance = useClerkAppearance(); 8 | 9 | return ( 10 |
11 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/forms/sign-up-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useClerkAppearance from "@/lib/hooks/useClerkAppearance"; 4 | import { SignUp } from "@clerk/nextjs"; 5 | 6 | export const SignUpForm = ({ redirectUrl }: { redirectUrl?: string }) => { 7 | const appearance = useClerkAppearance(); 8 | 9 | return ( 10 |
11 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es2017", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "checkJs": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | ".next/types/**/*.ts", 33 | "postcss.config.js", 34 | "tailwind.config.js" 35 | ], 36 | "exclude": ["node_modules"] 37 | } 38 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export type TextareaProps = React.TextareaHTMLAttributes; 6 | 7 | const Textarea = React.forwardRef( 8 | ({ className, ...props }, ref) => { 9 | return ( 10 |