├── .env.example ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── README.md ├── app ├── (home) │ ├── layout.tsx │ └── page.tsx ├── action.ts ├── blog │ └── [id] │ │ └── page.tsx ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── all-blogs.tsx ├── generate.tsx ├── nav │ ├── footer.tsx │ ├── index.ts │ └── navbar.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── input.tsx │ └── sonner.tsx ├── lib ├── supabase.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── brand-logo.svg └── brand.png ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | SUPABASE_URL= 2 | SUPABASE_SERVICE_KEY= 3 | OPENAI_API_KEY= 4 | CLERK_SECRET_KEY= 5 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 files 30 | .env*.local 31 | .env 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | npm run build -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | yarn.lock 4 | package-lock.json 5 | public 6 | .history 7 | .env 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "jsxSingleQuote": false, 4 | "proseWrap": "always", 5 | "printWidth": 80, 6 | "trailingComma": "es5", 7 | "tabWidth": 4, 8 | "semi": true, 9 | "singleQuote": false, 10 | "plugins": ["prettier-plugin-tailwindcss"] 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI-Powered Blog Generator 2 | Screenshot 2024-02-25 at 5 36 45 PM 3 | 4 | ### Project Overview 5 | This project is an AI-powered blog generator that allows users to create blog posts with just a single click. It utilizes advanced AI algorithms for generating plagiarism-free content and fetching relevant images to accompany the text. 6 | 7 | ### Features 8 | 1. **One-Click Blog Generation**: Users can generate fully-formed blog posts with just a single click, saving time and effort. 9 | 2. **Plagiarism-Free Content**: The content generated by the AI is guaranteed to be original and plagiarism-free. 10 | 3. **Image Integration**: Relevant images are fetched automatically to complement the generated content, enhancing the visual appeal of the blog post. 11 | 12 | ## Built with 13 | 14 | - [Next.js](https://nextjs.org/) 15 | - [Tailwind CSS](https://tailwindcss.com/) 16 | - [shadcn/ui](https://ui.shadcn.com/) 17 | - [Clerk](https://clerk.com/) 18 | - [Supabase](https://supabase.com/) 19 | 20 | ## Feature Requests 21 | 22 | To request a feature open a [GitHub issue](https://github.com/anayatkhan1/GenieBlog/issues). 23 | 24 | ## Contribution Guidelines 25 | 26 | Thank you for considering contributing to our AI-powered blog generator project! Please follow these guidelines to ensure smooth collaboration: 27 | 28 | 1. Fork the repository to your GitHub account. 29 | 2. Clone the forked repository to your local machine: 30 | 31 | ```bash 32 | git clone https://github.com/your-name/GenieBlog.git 33 | ``` 34 | 35 | 3. Create a new branch for your changes: 36 | 37 | ```bash 38 | git checkout -b feature/your-feature-name 39 | ``` 40 | 41 | 4. Make your changes and ensure they adhere to the project's coding style and guidelines. 42 | 5. Test your changes thoroughly to avoid introducing bugs. 43 | 6. Commit your changes with clear and descriptive commit messages: 44 | 45 | ```bash 46 | git commit -m 'Add your descriptive commit message' 47 | ``` 48 | 49 | 7. Push your changes to your forked repository: 50 | 51 | ```bash 52 | git push origin feature/your-feature-name 53 | ``` 54 | 55 | 8. Open a pull request against the `main` branch of the original repository. 56 | 9. Provide a clear and concise description of your changes in the pull request, along with any relevant information. 57 | 10. Ensure your pull request passes all checks and tests before requesting a review. 58 | 59 | ## Setting Up Environment Variables 60 | 61 | To run the project locally, you need to set up the following environment variables: 62 | 63 | - `SUPABASE_URL`: URL of your Supabase project. 64 | - `SUPABASE_SERVICE_KEY`: Service key for accessing your Supabase project. 65 | - `OPENAI_API_KEY`: API key for accessing the OpenAI platform. 66 | - `CLERK_SECRET_KEY`: Secret key for Clerk authentication. 67 | - `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`: Publishable key for Clerk authentication. 68 | 69 | You can set these environment variables by creating a `.env.local or .env` file in the root directory of the project and adding the variables with their respective values: 70 | 71 | Ensure not to share sensitive information like API keys publicly. 72 | 73 | Once you've set up the environment variables, you can run the project locally without any issues. If you encounter any problems, feel free to reach out for assistance. 74 | -------------------------------------------------------------------------------- /app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Navbar, Footer } from "@/components/nav"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Genie Blog", 6 | description: "Generated Blog content with Powerful AI tool", 7 | }; 8 | 9 | export default function HomeLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | <> 16 | {/** Background Snippet */} 17 |
18 |
19 |
20 |
21 | 22 | {children} 23 | {/*
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import AllBlogs from "@/components/all-blogs"; 2 | import Generate from "@/components/generate"; 3 | 4 | export const maxDuration = 10; 5 | const Home = async () => { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 |
13 | ); 14 | }; 15 | 16 | export default Home; 17 | -------------------------------------------------------------------------------- /app/action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import OpenAI from "openai"; 4 | import { auth } from "@clerk/nextjs"; 5 | import { supabase } from "@/lib/supabase"; 6 | import { decode } from "base64-arraybuffer"; 7 | import { revalidatePath } from "next/cache"; 8 | import { redirect } from "next/navigation"; 9 | 10 | const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); 11 | 12 | export async function createCompletion(prompt: string) { 13 | if (!prompt) { 14 | return { error: "Ohho! Prompt is required" }; 15 | } 16 | 17 | const { userId } = auth(); 18 | if (!userId) { 19 | return { error: "User is not logged in" }; 20 | } 21 | 22 | // generate blog post using openai 23 | const messages: any = [ 24 | { 25 | role: "user", 26 | content: `write a blog post around 200 words about the following topics: [${prompt}] 27 | Before generating the blog content follow my instructions: 28 | 1. write a only title of the blog in bold under 10 words. 29 | 2. Ensure 100% originality to avoid plagiarism.`, 30 | }, 31 | ]; 32 | 33 | const completion = await openai.chat.completions.create({ 34 | messages, 35 | model: "gpt-3.5-turbo", 36 | }); 37 | 38 | const content = completion?.choices?.[0]?.message?.content; 39 | if (!content) { 40 | return { error: "Unable to generate the blog content" }; 41 | } 42 | 43 | // generate an image using openai 44 | const image = await openai.images.generate({ 45 | model: "dall-e-2", 46 | prompt: `Generate an image for a blog post about [${prompt}] 47 | Before creating an image for a blog post, adhere to these instructions: 48 | 1. A 3D unreal most realistic image of this ${prompt}.`, 49 | n: 1, 50 | size: "1024x1024", 51 | response_format: "b64_json", 52 | }); 53 | const imageName = `blog-${Date.now()}`; 54 | const imageData = image?.data?.[0]?.b64_json as string; 55 | if (!imageData) { 56 | return { error: "Unable to generate the blog image" }; 57 | } 58 | 59 | //upload the image to supabase storage 60 | const { data, error } = await supabase.storage 61 | .from("blogs") 62 | .upload(imageName, decode(imageData), { 63 | contentType: "image/png", 64 | }); 65 | if (error) { 66 | return { error: "Unable to Upload the blog image to Storage" }; 67 | } 68 | 69 | const path = data?.path; 70 | const url = process.env.SUPABASE_URL; 71 | const imageUrl = `${url}/storage/v1/object/public/blogs/${path}`; 72 | 73 | // create a new blog post in supabase 74 | const { data: blog, error: blogError } = await supabase 75 | .from("blogs") 76 | .insert([{ title: prompt, content, imageUrl, userId }]) 77 | .select(); 78 | 79 | if (blogError) { 80 | return { error: "Unable to insert the blog into the database." }; 81 | } 82 | const blogId = blog?.[0]?.id; 83 | revalidatePath("/"); 84 | redirect(`/blog/${blogId}`); 85 | } 86 | -------------------------------------------------------------------------------- /app/blog/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { getBlogId } from "@/lib/supabase"; 4 | import { ChevronLeft } from "lucide-react"; 5 | import Markdown from "react-markdown"; 6 | 7 | const Blog = async ({ params }: { params: { id: string } }) => { 8 | const { content, imageUrl } = await getBlogId(Number(params.id)); 9 | return ( 10 |
11 | 15 | 16 | Go back 17 | 18 | 19 |
20 |  21 | {content} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Blog; 28 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anayatkhan1/GenieBlog/674279c7720026515a68c21e400e4ccf1524753d/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5.9% 10%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { ClerkProvider } from "@clerk/nextjs"; 3 | import { Toaster } from "@/components/ui/sonner"; 4 | import { Inter } from "next/font/google"; 5 | import "./globals.css"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Genie Blog", 11 | description: 12 | "Step into GenieBlog, where your writing dreams come true with just a prompt! Our AI-powered platform transforms your ideas into polished blog posts in a blink. No more staring at a blank page let GenieBlog unleash your creativity effortlessly!", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /components/all-blogs.tsx: -------------------------------------------------------------------------------- 1 | import { getAllBlogs } from "@/lib/supabase"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { formData } from "@/lib/utils"; 5 | import { Card, CardContent } from "@/components/ui/card"; 6 | 7 | const AllBlogs = async () => { 8 | const blogs = await getAllBlogs(); 9 | return ( 10 |
11 |

12 | Recent Blogs 13 |

14 |
15 | {blogs?.map(blog => ( 16 | 17 | 18 | 19 | {`${blog.id}`} 26 |
27 |

28 | {blog.title} 29 |

30 |

31 | {formData(blog.created_at)} 32 |

33 |
34 | 35 |
36 |
37 | ))} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default AllBlogs; 44 | -------------------------------------------------------------------------------- /components/generate.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Input } from "./ui/input"; 3 | 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "./ui/card"; 11 | import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs"; 12 | import { Button } from "./ui/button"; 13 | import { toast } from "sonner"; 14 | import { createCompletion } from "@/app/action"; 15 | import { useFormStatus } from "react-dom"; 16 | import { cn } from "@/lib/utils"; 17 | 18 | const Generate = () => { 19 | async function action(formData: FormData) { 20 | const prompt = formData.get("prompt"); 21 | 22 | // call server action 23 | 24 | const result = await createCompletion(prompt as string); 25 | if (result?.error) { 26 | toast.error(result.error); 27 | } 28 | } 29 | 30 | return ( 31 |
32 | 33 | 34 | 35 | Genie AI Blogger 36 | 37 | 38 | Generate a blog post about anything 39 | 40 | 41 | 42 |
43 | 48 | 49 | 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | const SubmitButton = () => { 57 | const { pending } = useFormStatus(); 58 | 59 | return ( 60 | <> 61 | 62 | 72 | 73 | 74 | 75 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default Generate; 90 | -------------------------------------------------------------------------------- /components/nav/footer.tsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ; 3 | }; 4 | 5 | export default Footer; 6 | -------------------------------------------------------------------------------- /components/nav/index.ts: -------------------------------------------------------------------------------- 1 | import Footer from "./footer"; 2 | import Navbar from "./navbar"; 3 | 4 | export { Navbar, Footer }; 5 | -------------------------------------------------------------------------------- /components/nav/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { Github } from "lucide-react"; 4 | import { Button } from "../ui/button"; 5 | import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; 6 | 7 | const Navbar = () => { 8 | return ( 9 |
10 | 37 |
38 | ); 39 | }; 40 | 41 | export default Navbar; 42 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )); 42 | CardTitle.displayName = "CardTitle"; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )); 54 | CardDescription.displayName = "CardDescription"; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 27 | ); 28 | }; 29 | 30 | export { Toaster }; 31 | -------------------------------------------------------------------------------- /lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | const supabaseUrl = process.env.SUPABASE_URL as string; 4 | const supabaseKey = process.env.SUPABASE_SERVICE_KEY as string; 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseKey); 7 | 8 | export async function getBlogId(id: number) { 9 | const { data, error } = await supabase 10 | .from("blogs") 11 | .select() 12 | .eq("id", id) 13 | .single(); 14 | 15 | return data; 16 | } 17 | 18 | export async function getAllBlogs() { 19 | const { data, error } = await supabase 20 | .from("blogs") 21 | .select() 22 | .order("created_at", { ascending: false }); 23 | return data; 24 | } 25 | -------------------------------------------------------------------------------- /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 | 8 | export function formData(date: string) { 9 | return new Date(date).toLocaleDateString("en-US", { 10 | month: "long", 11 | day: "numeric", 12 | year: "numeric", 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | export default authMiddleware({ 4 | publicRoutes: () => true, 5 | }); 6 | export const config = { 7 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 8 | }; 9 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "pdjofjygvmxklpeamnld.supabase.co", 8 | }, 9 | ], 10 | }, 11 | env: { 12 | SUPABASE_URL: process.env.SUPABASE_URL, 13 | SUPABASE_SERVICE_KEY: process.env.SUPABASE_SERVICE_KEY, 14 | OPENAI_API_KEY: process.env.OPENAI_API_KEY, 15 | CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, 16 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, 17 | }, 18 | }; 19 | 20 | module.exports = nextConfig; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genie-blog", 3 | "description": "Ai generated Blog", 4 | "author": "Anayat khan", 5 | "version": "0.1.0", 6 | "private": true, 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "eslint . --ext ts --ext tsx --ext js", 12 | "format": "prettier --write ." 13 | }, 14 | "lint-staged": { 15 | "*.@(ts|tsx)": [ 16 | "npm run lint", 17 | "npm run format" 18 | ] 19 | }, 20 | "dependencies": { 21 | "@clerk/nextjs": "^4.29.7", 22 | "@radix-ui/react-icons": "^1.3.0", 23 | "@radix-ui/react-slot": "^1.0.2", 24 | "@supabase/auth-helpers-nextjs": "^0.9.0", 25 | "@supabase/supabase-js": "^2.39.7", 26 | "base64-arraybuffer": "^1.0.2", 27 | "class-variance-authority": "^0.7.0", 28 | "clsx": "^2.1.0", 29 | "lucide-react": "^0.338.0", 30 | "next": "14.1.0", 31 | "next-themes": "^0.2.1", 32 | "openai": "^4.28.0", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0", 35 | "react-markdown": "^9.0.1", 36 | "sonner": "^1.4.1", 37 | "tailwind-merge": "^2.2.1", 38 | "tailwindcss-animate": "^1.0.7" 39 | }, 40 | "devDependencies": { 41 | "@tailwindcss/typography": "^0.5.10", 42 | "@types/node": "^20.11.10", 43 | "@types/react": "^18.2.48", 44 | "@types/react-dom": "^18.2.18", 45 | "autoprefixer": "^10.4.17", 46 | "eslint": "^8.56.0", 47 | "eslint-config-next": "14.1.0", 48 | "husky": "^9.0.10", 49 | "lint-staged": "^15.2.2", 50 | "postcss": "^8.4.33", 51 | "prettier": "^3.2.4", 52 | "prettier-plugin-tailwindcss": "^0.5.11", 53 | "tailwindcss": "^3.4.1", 54 | "typescript": "^5.3.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/brand-logo.svg: -------------------------------------------------------------------------------- 1 | Halloween -------------------------------------------------------------------------------- /public/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anayatkhan1/GenieBlog/674279c7720026515a68c21e400e4ccf1524753d/public/brand.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{ts,tsx}", 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [ 78 | require("@tailwindcss/typography"), 79 | require("tailwindcss-animate"), 80 | ], 81 | }; 82 | 83 | export default config; 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------