├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── ISSUE_TEMPLATE │ └── bug_report.yml ├── .eslintrc.json ├── image.png ├── public ├── favicon.ico ├── favicon-96x96.png ├── fonts │ └── playwrite.ttf ├── apple-touch-icon.png ├── web-app-manifest-192x192.png ├── web-app-manifest-512x512.png └── site.webmanifest ├── postcss.config.mjs ├── lib ├── meme.ts ├── constants │ └── MemeOptions.ts └── utils.ts ├── next.config.mjs ├── components ├── theme-provider.tsx ├── tailwind-indicator.tsx ├── theme-toggle.tsx ├── ui │ ├── input.tsx │ ├── button.tsx │ ├── card.tsx │ ├── drawer.tsx │ ├── annu.tsx │ ├── dialog.tsx │ └── select.tsx └── meme.tsx ├── components.json ├── .gitignore ├── hooks └── use-media-query.ts ├── tsconfig.json ├── package.json ├── app ├── page.tsx ├── layout.tsx ├── api │ └── meme │ │ └── route.ts └── memer │ └── page.tsx ├── README.md ├── styles └── globals.css └── prvs.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @avalynndev -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/image.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/fonts/playwrite.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/public/fonts/playwrite.ttf -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalynndev/memergez/HEAD/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/meme.ts: -------------------------------------------------------------------------------- 1 | export const parseUrlQuery = () => { 2 | const params = new URLSearchParams(window.location.search); 3 | 4 | const query: Record = {}; 5 | for (const [key, value] of params.entries()) { 6 | query[key] = value; 7 | } 8 | return { query }; 9 | }; 10 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "i.imgflip.com", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Memergez", 3 | "short_name": "Memergez", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 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 | -------------------------------------------------------------------------------- /hooks/use-media-query.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches); 9 | } 10 | 11 | const result = matchMedia(query); 12 | result.addEventListener("change", onChange); 13 | setValue(result.matches); 14 | 15 | return () => result.removeEventListener("change", onChange); 16 | }, [query]); 17 | 18 | return value; 19 | } 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **What kind of change does this PR introduce?** 4 | 5 | 6 | 7 | **If relevant, did you update the documentation?** 8 | 9 | **Summary** 10 | 11 | 12 | 13 | 14 | **Other information** -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' -------------------------------------------------------------------------------- /components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null; 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 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", "index.mjs"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | 9 | export function ThemeToggle() { 10 | const { setTheme, theme } = useTheme(); 11 | 12 | return ( 13 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/constants/MemeOptions.ts: -------------------------------------------------------------------------------- 1 | export interface MemeOption { 2 | value: string; 3 | label: string; 4 | type: "image" | "text"; 5 | } 6 | 7 | export const MemeOptions: MemeOption[] = [ 8 | { value: "trash", label: "Trash", type: "image" }, 9 | { value: "vr", label: "VR", type: "text" }, 10 | { value: "dab", label: "Dab", type: "image" }, 11 | { value: "disability", label: "Disability", type: "image" }, 12 | { value: "door", label: "Door", type: "image" }, 13 | { value: "egg", label: "Egg", type: "image"}, 14 | { value: "excuseme", label: "Excuse Me", type: "text" }, 15 | { value: "failure", label: "Failure", type: "image" }, 16 | { value: "hitler", label: "Hitler", type: "image" }, 17 | { value: "humanity", label: "Humanity", type: "text" }, 18 | { value: "idelete", label: "Delete", type: "image" }, 19 | { value: "jail", label: "Jail", type: "image" }, 20 | { value: "roblox", label: "Roblox", type: "image" }, 21 | { value: "satan", label: "Satan", type: "image" }, 22 | { value: "stonks", label: "Stonks", type: "text" } 23 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memergez", 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-dialog": "^1.1.15", 13 | "@radix-ui/react-icons": "^1.3.2", 14 | "@radix-ui/react-select": "^2.2.6", 15 | "@radix-ui/react-slot": "^1.2.3", 16 | "class-variance-authority": "^0.7.1", 17 | "clsx": "^2.1.1", 18 | "memer.ts-canvas": "^0.0.3", 19 | "next": "^15.5.2", 20 | "next-themes": "^0.4.6", 21 | "react": "^19.1.0", 22 | "react-dom": "^19.1.1", 23 | "tailwind-merge": "^3.3.1", 24 | "tailwindcss-animate": "^1.0.7", 25 | "vaul": "^1.1.2" 26 | }, 27 | "devDependencies": { 28 | "@tailwindcss/postcss": "^4.1.12", 29 | "@types/node": "^24", 30 | "@types/react": "^19.1.12", 31 | "@types/react-dom": "^19.1.9", 32 | "eslint": "^8", 33 | "eslint-config-next": "15.5.2", 34 | "postcss": "^8", 35 | "tailwindcss": "^4.1.12", 36 | "typescript": "^5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import MemeGenerator from "@/components/meme"; 3 | import { ThemeToggle } from "@/components/theme-toggle"; 4 | import Link from "next/link"; 5 | import { ArrowRightIcon } from "@radix-ui/react-icons"; 6 | import { Button } from "@/components/ui/button"; 7 | 8 | const MemePage = () => { 9 | return ( 10 |
11 |
12 |
13 |

14 | Generate Your Perfect Meme{" "} 15 | Now. 16 |

17 |

18 | I created this site so you can easily create and share memes for 19 | fun, humor, or just to express yourself. Get started below! 20 |

21 |
22 | 27 | 31 | 32 | 33 |
34 |
35 |
36 | 37 |
38 | ); 39 | }; 40 | 41 | export default MemePage; 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-hidden 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-sm hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs 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 { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { Inter as FontSans } from "next/font/google"; 3 | import { TailwindIndicator } from "@/components/tailwind-indicator"; 4 | import { ThemeProvider } from "@/components/theme-provider"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import "@/styles/globals.css"; 8 | 9 | const fontSans = FontSans({ 10 | subsets: ["latin"], 11 | variable: "--font-sans", 12 | }); 13 | 14 | export const viewport: Viewport = { 15 | themeColor: [ 16 | { media: "(prefers-color-scheme: light)", color: "white" }, 17 | { media: "(prefers-color-scheme: dark)", color: "black" }, 18 | ], 19 | }; 20 | 21 | export const metadata: Metadata = { 22 | title: { 23 | default: "memergez", 24 | template: `%s - memergez`, 25 | }, 26 | description: 27 | "A site where you can generate memes with ease. created using shadcn-ui", 28 | }; 29 | 30 | export default function RootLayout({ 31 | children, 32 | }: Readonly<{ 33 | children: React.ReactNode; 34 | }>) { 35 | return ( 36 | 37 | 38 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 60 | 61 |
62 |
{children}
63 |
64 | 65 |
66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Memergez 4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 |

14 | 15 | ![alt text](image.png) 16 | 17 | ## What is Memergez? 18 | 19 | The one stop for generating memes with ease! Explore **[memergez.vercel.app](https://memergez.vercel.app)**. Created using `imgflip`. 20 | Includes more than 70 Meme commands. Just enter the Avatar/Text and get a meme in seconds. 21 | 22 | ## Images 23 | 24 |
25 | Home Page 26 |
27 | 28 | 29 | ## Installation 🛠️ 30 | 31 | ### 1. Clone this repository using 32 | 33 | ```bash 34 | git clone https://github.com/avalynndev/memergez.git 35 | ``` 36 | 37 | ```bash 38 | cd memergez 39 | ``` 40 | 41 | ### 2. Installation 42 | 43 | ### Install Dependencies 44 | 45 | ```bash 46 | npm install 47 | ``` 48 | 49 | ### 3. Run on development &/or production 50 | 51 | - Run on development mode 52 | 53 | ```bash 54 | npm run dev 55 | ``` 56 | 57 | - Run on production mode 58 | 59 | ```bash 60 | npm start 61 | ``` 62 | 63 | > Deploy **your own memergez** Instance on Vercel 64 | 65 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Favalynndev%2Fmemergez) 66 | 67 | 68 | ## Found a Bug? 🐞 69 | 70 | Uh-oh, looks like you stumbled upon a bug? No worries, we're here to squash it! Just head over to our [**issues**](https://github.com/avalynndev/memergez/issues) section on GitHub and let us know what's up. 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Found a Bug? 2 | description: Spotted something off? Let us know. 3 | title: '[Bug]: ' 4 | assignees: 5 | - avalynndev 6 | body: 7 | - type: input 8 | id: bug-description 9 | attributes: 10 | label: What went wrong? 11 | description: Tell us about the bug like you're telling a friend. Keep it simple and clear. 12 | placeholder: 'Like, when I try to play a video, it just won’t start...' 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: steps-to-reproduce 18 | attributes: 19 | label: How'd you stumble upon it? 20 | description: Walk us through how you found this bug, step by step. 21 | placeholder: "1. I was on the homepage\n2. Clicked on the play button\n3. And boom, nothing happened" 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | id: expected-vs-actual 27 | attributes: 28 | label: What you hoped for vs. What actually happened 29 | description: Share what you were expecting and then what really went down. 30 | placeholder: "Hoped for: A cool video starts playing.\nBut actually: Got a whole lot of nothing." 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: additional-info 36 | attributes: 37 | label: Anything else we should know? 38 | description: Got more details or a screenshot? Throw them in here. 39 | placeholder: 'FYI: This only happens in Chrome for me...' 40 | validations: 41 | required: false 42 | 43 | - type: dropdown 44 | id: browsers-affected 45 | attributes: 46 | label: Which browsers were a bummer? 47 | multiple: true 48 | options: 49 | - Firefox 50 | - Chrome 51 | - Safari 52 | - Microsoft Edge 53 | validations: 54 | required: true 55 | 56 | - type: textarea 57 | id: logs 58 | attributes: 59 | label: Got logs? 60 | description: If you've got some techy details or logs, we'd love to see them. 61 | render: shell 62 | validations: 63 | required: false 64 | 65 | - type: checkboxes 66 | id: code-of-conduct 67 | attributes: 68 | label: Code of Conduct Agreement 69 | options: 70 | - label: I agree to follow this project's Code of Conduct. 71 | required: true -------------------------------------------------------------------------------- /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 | /** 9 | * Validates if a string is a valid URL that could potentially be an image URL 10 | * @param text The text to validate as URL 11 | * @param checkImageHints Whether to check for image-related hints in the URL (optional, default: false) 12 | * @returns boolean indicating if the text is a valid URL 13 | */ 14 | export function isValidImageUrl(text: string, checkImageHints: boolean = false): boolean { 15 | // Basic check if the input is empty or not a string 16 | if (!text || typeof text !== 'string') { 17 | return false; 18 | } 19 | 20 | // Use URL constructor for basic URL validation 21 | try { 22 | const url = new URL(text); 23 | 24 | // Check if protocol is http or https 25 | if (url.protocol !== 'http:' && url.protocol !== 'https:') { 26 | return false; 27 | } 28 | 29 | // Optional: Check for image-related hints in the URL 30 | if (checkImageHints) { 31 | // Common image file extensions 32 | const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff', '.ico', '.avif']; 33 | 34 | // Check for image extensions in the pathname 35 | const hasImageExtension = imageExtensions.some(ext => 36 | url.pathname.toLowerCase().endsWith(ext) 37 | ); 38 | 39 | // Check for image-related patterns in the URL path or query parameters 40 | const lowercaseUrl = url.toString().toLowerCase(); 41 | const hasImagePattern = 42 | /\/img\/|\/image\/|\/images\/|\/media\/|\/photos\/|\/thumbnails\/|\/picture\//.test(lowercaseUrl) || 43 | /[?&](image|img|photo|pic)=/.test(lowercaseUrl) || 44 | /\/(media|content|cdn|assets)\//.test(lowercaseUrl); 45 | 46 | // If neither condition is met, it might not be an image URL 47 | if (!hasImageExtension && !hasImagePattern) { 48 | // Still allow CDN URLs that often don't have extensions or clear patterns 49 | const isCdnUrl = /cloudinary\.com|cloudfront\.net|imgur\.com|imgix\.net|res\.cloudinary\.com/.test(lowercaseUrl); 50 | if (!isCdnUrl) { 51 | return false; 52 | } 53 | } 54 | } 55 | 56 | return true; 57 | } catch (error) { 58 | return false; 59 | } 60 | } -------------------------------------------------------------------------------- /app/api/meme/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import path from "path"; 3 | import { Memer } from "memer.ts-canvas"; 4 | import { registerFont } from "canvas"; 5 | 6 | export async function POST(req: Request) { 7 | try { 8 | const { text, option }: { text: string; option: string } = await req.json(); 9 | 10 | if (!text) { 11 | return NextResponse.json({ error: "Text is required" }, { status: 400 }); 12 | } 13 | 14 | const memer = new Memer(); 15 | let base64Image; 16 | 17 | registerFont(path.resolve(process.cwd(), "public/fonts/playwrite.ttf"), { 18 | family: "Playwrite", 19 | }); 20 | 21 | if (option === "vr") { 22 | base64Image = await memer.vr(text, false); 23 | } else if (option === "trash") { 24 | base64Image = await memer.trash(text, false); 25 | } else if (option === "dab") { 26 | base64Image = await memer.dab(text, false); 27 | } else if (option === "disability") { 28 | base64Image = await memer.disability(text, false); 29 | } else if (option === "door") { 30 | base64Image = await memer.door(text, false); 31 | } else if (option === "egg") { 32 | base64Image = await memer.egg(text, false); 33 | } else if (option === "excuseme") { 34 | base64Image = await memer.excuseme(text, false); 35 | } else if (option === "failure") { 36 | base64Image = await memer.failure(text, false); 37 | } else if (option === "hitler") { 38 | base64Image = await memer.hitler(text, false); 39 | } else if (option === "humanity") { 40 | base64Image = await memer.humanity(text, false); 41 | } else if (option === "idelete") { 42 | base64Image = await memer.idelete(text, false); 43 | } else if (option === "jail") { 44 | base64Image = await memer.jail(text, false); 45 | } else if (option === "roblox") { 46 | base64Image = await memer.roblox(text, false); 47 | } else if (option === "satan") { 48 | base64Image = await memer.satan(text, false); 49 | } else if (option === "stonks") { 50 | base64Image = await memer.stonks(text, false); 51 | } else { 52 | return NextResponse.json( 53 | { error: "Invalid meme option" }, 54 | { status: 400 } 55 | ); 56 | } 57 | 58 | return NextResponse.json({ meme: base64Image }); 59 | } catch (error) { 60 | console.error(error); 61 | return NextResponse.json( 62 | { error: "Failed to generate meme, you might have entered an unsupported image type." }, 63 | { status: 500 } 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Drawer as DrawerPrimitive } from "vaul" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ) 17 | Drawer.displayName = "Drawer" 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal 22 | 23 | const DrawerClose = DrawerPrimitive.Close 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )) 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )) 56 | DrawerContent.displayName = "DrawerContent" 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ) 67 | DrawerHeader.displayName = "DrawerHeader" 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ) 78 | DrawerFooter.displayName = "DrawerFooter" 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )) 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )) 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | } 119 | -------------------------------------------------------------------------------- /components/ui/annu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { useMediaQuery } from "@/hooks/use-media-query"; 7 | import { 8 | Dialog, 9 | DialogClose, 10 | DialogContent, 11 | DialogDescription, 12 | DialogFooter, 13 | DialogHeader, 14 | DialogTitle, 15 | DialogTrigger, 16 | } from "@/components/ui/dialog"; 17 | import { 18 | Drawer, 19 | DrawerClose, 20 | DrawerContent, 21 | DrawerDescription, 22 | DrawerFooter, 23 | DrawerHeader, 24 | DrawerTitle, 25 | DrawerTrigger, 26 | } from "@/components/ui/drawer"; 27 | 28 | interface BaseProps { 29 | children: React.ReactNode; 30 | } 31 | 32 | interface RootAnnuProps extends BaseProps { 33 | open?: boolean; 34 | onOpenChange?: (open: boolean) => void; 35 | } 36 | 37 | interface AnnuProps extends BaseProps { 38 | className?: string; 39 | asChild?: true; 40 | } 41 | 42 | const desktop = "(min-width: 768px)"; 43 | 44 | const Annu = ({ children, ...props }: RootAnnuProps) => { 45 | const isDesktop = useMediaQuery(desktop); 46 | const Annu = isDesktop ? Dialog : Drawer; 47 | 48 | return {children}; 49 | }; 50 | 51 | const AnnuTrigger = ({ className, children, ...props }: AnnuProps) => { 52 | const isDesktop = useMediaQuery(desktop); 53 | const AnnuTrigger = isDesktop ? DialogTrigger : DrawerTrigger; 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | 62 | const AnnuClose = ({ className, children, ...props }: AnnuProps) => { 63 | const isDesktop = useMediaQuery(desktop); 64 | const AnnuClose = isDesktop ? DialogClose : DrawerClose; 65 | 66 | return ( 67 | 68 | {children} 69 | 70 | ); 71 | }; 72 | 73 | const AnnuContent = ({ className, children, ...props }: AnnuProps) => { 74 | const isDesktop = useMediaQuery(desktop); 75 | const AnnuContent = isDesktop ? DialogContent : DrawerContent; 76 | 77 | return ( 78 | 79 | {children} 80 | 81 | ); 82 | }; 83 | 84 | const AnnuDescription = ({ className, children, ...props }: AnnuProps) => { 85 | const isDesktop = useMediaQuery(desktop); 86 | const AnnuDescription = isDesktop ? DialogDescription : DrawerDescription; 87 | 88 | return ( 89 | 90 | {children} 91 | 92 | ); 93 | }; 94 | 95 | const AnnuHeader = ({ className, children, ...props }: AnnuProps) => { 96 | const isDesktop = useMediaQuery(desktop); 97 | const AnnuHeader = isDesktop ? DialogHeader : DrawerHeader; 98 | 99 | return ( 100 | 101 | {children} 102 | 103 | ); 104 | }; 105 | 106 | const AnnuTitle = ({ className, children, ...props }: AnnuProps) => { 107 | const isDesktop = useMediaQuery(desktop); 108 | const AnnuTitle = isDesktop ? DialogTitle : DrawerTitle; 109 | 110 | return ( 111 | 112 | {children} 113 | 114 | ); 115 | }; 116 | 117 | const AnnuBody = ({ className, children, ...props }: AnnuProps) => { 118 | return ( 119 |
120 | {children} 121 |
122 | ); 123 | }; 124 | 125 | const AnnuFooter = ({ className, children, ...props }: AnnuProps) => { 126 | const isDesktop = useMediaQuery(desktop); 127 | const AnnuFooter = isDesktop ? DialogFooter : DrawerFooter; 128 | 129 | return ( 130 | 131 | {children} 132 | 133 | ); 134 | }; 135 | 136 | export { 137 | Annu, 138 | AnnuTrigger, 139 | AnnuClose, 140 | AnnuContent, 141 | AnnuDescription, 142 | AnnuHeader, 143 | AnnuTitle, 144 | AnnuBody, 145 | AnnuFooter, 146 | }; 147 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @custom-variant dark (&:is(.dark *)); 4 | 5 | @theme { 6 | --color-border: hsl(var(--border)); 7 | --color-input: hsl(var(--input)); 8 | --color-ring: hsl(var(--ring)); 9 | --color-background: hsl(var(--background)); 10 | --color-foreground: hsl(var(--foreground)); 11 | 12 | --color-primary: hsl(var(--primary)); 13 | --color-primary-foreground: hsl(var(--primary-foreground)); 14 | 15 | --color-secondary: hsl(var(--secondary)); 16 | --color-secondary-foreground: hsl(var(--secondary-foreground)); 17 | 18 | --color-destructive: hsl(var(--destructive)); 19 | --color-destructive-foreground: hsl(var(--destructive-foreground)); 20 | 21 | --color-muted: hsl(var(--muted)); 22 | --color-muted-foreground: hsl(var(--muted-foreground)); 23 | 24 | --color-accent: hsl(var(--accent)); 25 | --color-accent-foreground: hsl(var(--accent-foreground)); 26 | 27 | --color-popover: hsl(var(--popover)); 28 | --color-popover-foreground: hsl(var(--popover-foreground)); 29 | 30 | --color-card: hsl(var(--card)); 31 | --color-card-foreground: hsl(var(--card-foreground)); 32 | 33 | --radius-lg: var(--radius); 34 | --radius-md: calc(var(--radius) - 2px); 35 | --radius-sm: calc(var(--radius) - 4px); 36 | 37 | --animate-accordion-down: accordion-down 0.2s ease-out; 38 | --animate-accordion-up: accordion-up 0.2s ease-out; 39 | 40 | @keyframes accordion-down { 41 | from { 42 | height: 0; 43 | } 44 | to { 45 | height: var(--radix-accordion-content-height); 46 | } 47 | } 48 | @keyframes accordion-up { 49 | from { 50 | height: var(--radix-accordion-content-height); 51 | } 52 | to { 53 | height: 0; 54 | } 55 | } 56 | } 57 | 58 | @utility container { 59 | margin-inline: auto; 60 | padding-inline: 2rem; 61 | @media (width >= --theme(--breakpoint-sm)) { 62 | max-width: none; 63 | } 64 | @media (width >= 1400px) { 65 | max-width: 1400px; 66 | } 67 | } 68 | 69 | /* 70 | The default border color has changed to `currentColor` in Tailwind CSS v4, 71 | so we've added these compatibility styles to make sure everything still 72 | looks the same as it did with Tailwind CSS v3. 73 | 74 | If we ever want to remove these styles, we need to add an explicit border 75 | color utility to any element that depends on these defaults. 76 | */ 77 | @layer base { 78 | *, 79 | ::after, 80 | ::before, 81 | ::backdrop, 82 | ::file-selector-button { 83 | border-color: var(--color-gray-200, currentColor); 84 | } 85 | } 86 | 87 | @layer base { 88 | :root { 89 | --background: 0 0% 100%; 90 | --foreground: 240 10% 3.9%; 91 | --card: 0 0% 100%; 92 | --card-foreground: 240 10% 3.9%; 93 | --popover: 0 0% 100%; 94 | --popover-foreground: 240 10% 3.9%; 95 | --primary: 240 5.9% 10%; 96 | --primary-foreground: 0 0% 98%; 97 | --secondary: 240 4.8% 95.9%; 98 | --secondary-foreground: 240 5.9% 10%; 99 | --muted: 240 4.8% 95.9%; 100 | --muted-foreground: 240 3.8% 46.1%; 101 | --accent: 240 4.8% 95.9%; 102 | --accent-foreground: 240 5.9% 10%; 103 | --destructive: 0 84.2% 60.2%; 104 | --destructive-foreground: 0 0% 98%; 105 | --border: 240 5.9% 90%; 106 | --input: 240 5.9% 90%; 107 | --ring: 240 10% 3.9%; 108 | --radius: 0.5rem; 109 | --chart-1: 12 76% 61%; 110 | --chart-2: 173 58% 39%; 111 | --chart-3: 197 37% 24%; 112 | --chart-4: 43 74% 66%; 113 | --chart-5: 27 87% 67%; 114 | } 115 | 116 | .dark { 117 | --background: 240 10% 3.9%; 118 | --foreground: 0 0% 98%; 119 | --card: 240 10% 3.9%; 120 | --card-foreground: 0 0% 98%; 121 | --popover: 240 10% 3.9%; 122 | --popover-foreground: 0 0% 98%; 123 | --primary: 0 0% 98%; 124 | --primary-foreground: 240 5.9% 10%; 125 | --secondary: 240 3.7% 15.9%; 126 | --secondary-foreground: 0 0% 98%; 127 | --muted: 240 3.7% 15.9%; 128 | --muted-foreground: 240 5% 64.9%; 129 | --accent: 240 3.7% 15.9%; 130 | --accent-foreground: 0 0% 98%; 131 | --destructive: 0 62.8% 30.6%; 132 | --destructive-foreground: 0 0% 98%; 133 | --border: 240 3.7% 15.9%; 134 | --input: 240 3.7% 15.9%; 135 | --ring: 240 4.9% 83.9%; 136 | --chart-1: 220 70% 50%; 137 | --chart-2: 160 60% 45%; 138 | --chart-3: 30 80% 55%; 139 | --chart-4: 280 65% 60%; 140 | --chart-5: 340 75% 55%; 141 | } 142 | } 143 | 144 | @layer base { 145 | * { 146 | @apply border-border; 147 | } 148 | body { 149 | @apply bg-background text-foreground; 150 | } 151 | } -------------------------------------------------------------------------------- /prvs.txt: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import MemeGenerator from "@/components/meme"; 3 | import { cn } from "@/lib/utils"; 4 | import { ThemeToggle } from "@/components/theme-toggle"; 5 | import { Button, buttonVariants } from "@/components/ui/button"; 6 | import { Input } from "@/components/ui/input"; 7 | import { 8 | Select, 9 | SelectTrigger, 10 | SelectValue, 11 | SelectContent, 12 | SelectItem, 13 | } from "@/components/ui/select"; 14 | import Link from "next/link"; 15 | import { useState } from "react"; 16 | import { ArrowRightIcon } from "@radix-ui/react-icons"; 17 | 18 | const MemePage = () => { 19 | const [text, setText] = useState(""); 20 | const [meme, setMeme] = useState(null); 21 | const [loading, setLoading] = useState(false); 22 | const [error, setError] = useState(null); 23 | const [option, setOption] = useState("trash"); 24 | 25 | const handleSubmit = async (e: React.FormEvent) => { 26 | e.preventDefault(); 27 | setLoading(true); 28 | setError(null); 29 | 30 | try { 31 | const response = await fetch(`/api/meme`, { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | body: JSON.stringify({ text, option }), 37 | }); 38 | 39 | const data = await response.json(); 40 | 41 | if (response.ok) { 42 | if (data.meme) { 43 | setMeme(`data:image/png;base64,${data.meme}`); 44 | } else { 45 | setError("Failed to generate meme. Please try again."); 46 | } 47 | } else { 48 | setError(data.error || "Failed to generate meme."); 49 | } 50 | } catch (error) { 51 | setError("An error occurred while generating the meme."); 52 | console.error("An error occurred:", error); 53 | } finally { 54 | setLoading(false); 55 | } 56 | }; 57 | 58 | return ( 59 |
60 |
61 |
62 |

Generate Meme

63 |
64 | 65 | 66 | 71 | More Memes 72 | 73 | 74 |
75 |
79 | setText(e.target.value)} 83 | placeholder="Enter your text/avatar URL" 84 | required 85 | className="w-full p-3 rounded-lg" 86 | /> 87 | 88 | 113 | 114 | 123 |
124 | 125 | {error &&

{error}

} 126 | 127 | {meme && ( 128 |
129 | Generated Meme 134 |
135 | )} 136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default MemePage; 143 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from "@radix-ui/react-icons" 10 | import * as SelectPrimitive from "@radix-ui/react-select" 11 | 12 | import { cn } from "@/lib/utils" 13 | 14 | const Select = SelectPrimitive.Root 15 | 16 | const SelectGroup = SelectPrimitive.Group 17 | 18 | const SelectValue = SelectPrimitive.Value 19 | 20 | const SelectTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | span]:line-clamp-1", 28 | className 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | 37 | )) 38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 39 | 40 | const SelectScrollUpButton = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | 53 | 54 | )) 55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 56 | 57 | const SelectScrollDownButton = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | 70 | 71 | )) 72 | SelectScrollDownButton.displayName = 73 | SelectPrimitive.ScrollDownButton.displayName 74 | 75 | const SelectContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, position = "popper", ...props }, ref) => ( 79 | 80 | 91 | 92 | 99 | {children} 100 | 101 | 102 | 103 | 104 | )) 105 | SelectContent.displayName = SelectPrimitive.Content.displayName 106 | 107 | const SelectLabel = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, ...props }, ref) => ( 111 | 116 | )) 117 | SelectLabel.displayName = SelectPrimitive.Label.displayName 118 | 119 | const SelectItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | SelectItem.displayName = SelectPrimitive.Item.displayName 140 | 141 | const SelectSeparator = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef 144 | >(({ className, ...props }, ref) => ( 145 | 150 | )) 151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 152 | 153 | export { 154 | Select, 155 | SelectGroup, 156 | SelectValue, 157 | SelectTrigger, 158 | SelectContent, 159 | SelectLabel, 160 | SelectItem, 161 | SelectSeparator, 162 | SelectScrollUpButton, 163 | SelectScrollDownButton, 164 | } 165 | -------------------------------------------------------------------------------- /app/memer/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn, isValidImageUrl } from "@/lib/utils"; 3 | import { ThemeToggle } from "@/components/theme-toggle"; 4 | import { Button, buttonVariants } from "@/components/ui/button"; 5 | import { Input } from "@/components/ui/input"; 6 | import { 7 | Select, 8 | SelectTrigger, 9 | SelectValue, 10 | SelectContent, 11 | SelectItem, 12 | } from "@/components/ui/select"; 13 | import Link from "next/link"; 14 | import { useEffect, useState } from "react"; 15 | import { MemeOptions } from "@/lib/constants/MemeOptions"; 16 | 17 | const MemePage = () => { 18 | const [text, setText ] = useState(""); 19 | const [meme, setMeme] = useState(null); 20 | const [loading, setLoading] = useState(false); 21 | const [error, setError] = useState(null); 22 | const [alert, setAlert] = useState<{message: string, type: "passable" | "serious"} | null>(null); 23 | const [option, setOption] = useState("trash"); 24 | 25 | const handleSubmit = async (e: React.FormEvent) => { 26 | e.preventDefault(); 27 | if (alert && alert.type === "serious") return; 28 | 29 | setLoading(true); 30 | setError(null); 31 | setAlert(null); 32 | 33 | try { 34 | const response = await fetch(`/api/meme`, { 35 | method: "POST", 36 | headers: { 37 | "Content-Type": "application/json", 38 | }, 39 | body: JSON.stringify({ text, option }), 40 | }); 41 | 42 | const data = await response.json(); 43 | 44 | if (response.ok) { 45 | if (data.meme) { 46 | setMeme(`data:image/png;base64,${data.meme}`); 47 | } else { 48 | setError("Failed to generate meme. Please try again."); 49 | } 50 | } else { 51 | setError(data.error || "Failed to generate meme."); 52 | } 53 | } catch (error) { 54 | setError("An error occurred while generating the meme."); 55 | console.error("An error occurred:", error); 56 | } finally { 57 | setLoading(false); 58 | } 59 | }; 60 | 61 | useEffect(() => { 62 | if (!option || !text) { 63 | setError(null); 64 | setAlert(null); 65 | return; 66 | }; 67 | 68 | const selectedOption = MemeOptions.find((m) => m.value === option); 69 | if (!selectedOption) { 70 | setError("Invalid meme type selected"); 71 | return; 72 | } 73 | 74 | const is_Text_A_Valid_URL = isValidImageUrl(text); 75 | 76 | if (selectedOption.type === "image" && !is_Text_A_Valid_URL) { 77 | setAlert({ 78 | message: "Please enter a valid image URL for this meme type", 79 | type: "serious" 80 | }); 81 | return; 82 | } 83 | 84 | if (selectedOption.type === "text" && is_Text_A_Valid_URL) { 85 | setAlert({ 86 | message: "Please enter text instead of an image URL for this meme type", 87 | type: "passable" 88 | }); 89 | return; 90 | } 91 | 92 | setAlert(null); 93 | setError(null); 94 | }, [option, text, isValidImageUrl, MemeOptions]); 95 | 96 | useEffect(() => { 97 | if (error) { 98 | const timer = setTimeout(() => { 99 | setError(null); 100 | }, 5000); 101 | 102 | // Clean up the timer when component unmounts or error changes 103 | return () => clearTimeout(timer); 104 | } 105 | }, [error]); 106 | 107 | return ( 108 |
109 |
110 |
111 |

112 | Generate a meme using 113 | Memer.ts 114 |

115 |
116 | 123 | home 124 | 125 | 126 |
127 |
131 | setText(e.target.value)} 135 | placeholder="Enter your text/avatar URL" 136 | required 137 | className="w-full p-3 rounded-xl" 138 | /> 139 | 140 | 153 | 154 | 163 |
164 | 165 | {error &&

{error}

} 166 | {alert &&

{alert.message}

} 167 | 168 | {meme && ( 169 |
170 | Generated Meme 175 |
176 | )} 177 |
178 |
179 |
180 | ); 181 | }; 182 | 183 | export default MemePage; 184 | -------------------------------------------------------------------------------- /components/meme.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { 3 | Annu, 4 | AnnuBody, 5 | AnnuContent, 6 | AnnuDescription, 7 | AnnuFooter, 8 | AnnuHeader, 9 | AnnuTitle, 10 | AnnuTrigger, 11 | } from "@/components/ui/annu"; 12 | import { Card, CardContent } from "@/components/ui/card"; 13 | import React, { useEffect, useState } from "react"; 14 | import { Button } from "./ui/button"; 15 | import { Input } from "./ui/input"; 16 | import { ArrowRightIcon } from "@radix-ui/react-icons"; 17 | import Link from "next/link"; 18 | 19 | interface Meme { 20 | id: string; 21 | name: string; 22 | url: string; 23 | width: number; 24 | height: number; 25 | box_count: number; 26 | } 27 | 28 | const MemeGenerator: React.FC = () => { 29 | const [inputs, setInputs] = useState<{ [key: string]: string[] }>({}); 30 | const [memes, setMemes] = useState([]); 31 | const [generatedMemes, setGeneratedMemes] = useState<{ 32 | [key: string]: string | null; 33 | }>({}); 34 | 35 | useEffect(() => { 36 | async function fetchMemes() { 37 | try { 38 | const response = await fetch("https://api.imgflip.com/get_memes", { 39 | next: { revalidate: 21600 }, 40 | }); 41 | const data = await response.json(); 42 | if (data.success) { 43 | setMemes(data.data.memes); 44 | } else { 45 | console.error("Failed to fetch memes"); 46 | } 47 | } catch (error) { 48 | console.error("Error fetching memes:", error); 49 | } 50 | } 51 | 52 | fetchMemes(); 53 | }, []); 54 | 55 | const handleInputChange = (memeId: string, index: number, value: string) => { 56 | setInputs((prevInputs) => ({ 57 | ...prevInputs, 58 | [memeId]: prevInputs[memeId] 59 | ? prevInputs[memeId].map((input, i) => (i === index ? value : input)) 60 | : Array(memes.find((m) => m.id === memeId)?.box_count || 0) 61 | .fill("") 62 | .map((input, i) => (i === index ? value : input)), 63 | })); 64 | }; 65 | 66 | const handleSubmit = async (memeId: string) => { 67 | const selectedMeme = memes.find((m) => m.id === memeId); 68 | if (!selectedMeme || !inputs[memeId]) return; 69 | 70 | const formData = new URLSearchParams(); 71 | formData.append("template_id", memeId); 72 | formData.append("username", "clutchgodfrfr"); 73 | formData.append("password", "$KzdWSUV-z6SUqD"); 74 | 75 | inputs[memeId].forEach((text, index) => { 76 | formData.append(`boxes[${index}][text]`, text); 77 | }); 78 | 79 | try { 80 | const response = await fetch("https://api.imgflip.com/caption_image", { 81 | method: "POST", 82 | body: formData, 83 | }); 84 | const data = await response.json(); 85 | 86 | if (data.success) { 87 | setGeneratedMemes((prevMemes) => ({ 88 | ...prevMemes, 89 | [memeId]: data.data.url, 90 | })); 91 | } else { 92 | console.error("Failed to generate meme"); 93 | } 94 | } catch (error) { 95 | console.error("Error generating meme:", error); 96 | } 97 | }; 98 | 99 | return ( 100 |
101 |
102 | {memes.map((item) => ( 103 | 104 | 105 |
106 |
107 | {item.name} 115 |
116 | {item.name} 117 |
118 |
119 |
120 |
121 | 122 | 123 | {item.name} 124 | 125 | Generate a new meme using this template 126 | 127 | 128 | 129 |
130 | {Array.from({ length: item.box_count }).map((_, index) => ( 131 | 138 | handleInputChange(item.id, index, e.target.value) 139 | } 140 | /> 141 | ))} 142 |
143 | 149 | {generatedMemes[item.id] && ( 150 |
151 | Generated Meme 159 | 163 | 169 | 170 |
171 | )} 172 |
173 | 174 | 175 | Crafted with ❤️ 176 | 177 | 178 |
179 |
180 | ))} 181 |
182 |
183 | ); 184 | }; 185 | 186 | export default MemeGenerator; 187 | --------------------------------------------------------------------------------