├── .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 | setTheme(theme === "light" ? "dark" : "light")}
18 | >
19 |
20 |
21 | Toggle theme
22 |
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 |
28 | Explore Memer.ts
29 |
30 |
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 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 
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 |
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 | [](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 |
124 |
125 | {error &&
{error}
}
126 |
127 | {meme && (
128 |
129 |
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 |
164 |
165 | {error &&
{error}
}
166 | {alert &&
{alert.message}
}
167 |
168 | {meme && (
169 |
170 |
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 |
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 | handleSubmit(item.id)}
146 | >
147 | Generate Meme
148 |
149 | {generatedMemes[item.id] && (
150 |
151 |
159 |
163 |
167 | Download Meme
168 |
169 |
170 |
171 | )}
172 |
173 |
174 |
175 | Crafted with ❤️
176 |
177 |
178 |
179 |
180 | ))}
181 |
182 |
183 | );
184 | };
185 |
186 | export default MemeGenerator;
187 |
--------------------------------------------------------------------------------