├── .env.server.example ├── .gitignore ├── .wasproot ├── README.md ├── main.wasp ├── memerator.png ├── migrations ├── 20230901115956_init │ └── migration.sql ├── 20230904084625_topics_audience │ └── migration.sql ├── 20230904090820_ok │ └── migration.sql ├── 20230904094512_is_admin │ └── migration.sql ├── 20230904094950_credits │ └── migration.sql └── migration_lock.toml ├── postcss.config.cjs ├── src ├── .waspignore ├── client │ ├── Layout.tsx │ ├── Main.css │ ├── pages │ │ ├── EditMemePage.tsx │ │ ├── Home.tsx │ │ └── auth │ │ │ ├── Login.tsx │ │ │ └── Signup.tsx │ ├── tsconfig.json │ └── vite-env.d.ts ├── server │ ├── actions.ts │ ├── queries.ts │ ├── tsconfig.json │ ├── utils.ts │ └── workers.ts └── shared │ └── tsconfig.json └── tailwind.config.cjs /.env.server.example: -------------------------------------------------------------------------------- 1 | # set up your own credentials on https://imgflip.com/signup and rename this file to .env.server 2 | # NOTE: make sure you register with Username and Password (not google) 3 | IMGFLIP_USERNAME= 4 | IMGFLIP_PASSWORD= 5 | 6 | # get your api key from https://platform.openai.com/ 7 | OPENAI_API_KEY= 8 | 9 | JWT_SECRET=asecretphraseatleastthirtytwocharacterslong -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.wasp/ 2 | /.env.server 3 | /.env.client 4 | 5 | # Local Netlify folder 6 | .netlify 7 | -------------------------------------------------------------------------------- /.wasproot: -------------------------------------------------------------------------------- 1 | File marking the root of Wasp project. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a repo for the Memerator meme generator app: https://damemerator.netlify.app/ 2 | 3 | 4 | 5 | It uses: 6 | - [Wasp](https://wasp-lang.dev/) a full-stack React/NodeJS framework 7 | - OpenAI's function calling API to call imgflip.com's API to generate memes 8 | 9 | Please see the tutorial on how to build this app: https://dev.to/wasp/build-your-own-ai-meme-generator-learn-how-to-use-openais-function-calls-1p21 -------------------------------------------------------------------------------- /main.wasp: -------------------------------------------------------------------------------- 1 | app Memerator { 2 | wasp: { 3 | version: "^0.11.3" 4 | }, 5 | title: "Memerator", 6 | client: { 7 | rootComponent: import { Layout } from "@client/Layout", 8 | }, 9 | db: { 10 | system: PostgreSQL, 11 | prisma: { 12 | clientPreviewFeatures: ["extendedWhereUnique"] 13 | } 14 | }, 15 | auth: { 16 | userEntity: User, 17 | methods: { 18 | usernameAndPassword: {} 19 | }, 20 | onAuthFailedRedirectTo: "/login", 21 | onAuthSucceededRedirectTo: "/" 22 | }, 23 | dependencies: [ 24 | ("openai", "4.2.0"), 25 | ("axios", "^1.4.0"), 26 | ("react-icons", "4.10.1"), 27 | ] 28 | } 29 | 30 | entity User {=psl 31 | id Int @id @default(autoincrement()) 32 | username String @unique 33 | password String 34 | memes Meme[] 35 | isAdmin Boolean @default(false) 36 | credits Int @default(2) 37 | psl=} 38 | 39 | entity Meme {=psl 40 | id String @id @default(uuid()) 41 | url String 42 | text0 String 43 | text1 String 44 | topics String 45 | audience String 46 | template Template @relation(fields: [templateId], references: [id]) 47 | templateId String 48 | user User @relation(fields: [userId], references: [id]) 49 | userId Int 50 | createdAt DateTime @default(now()) 51 | psl=} 52 | 53 | entity Template {=psl 54 | id String @id @unique 55 | name String 56 | url String 57 | width Int 58 | height Int 59 | boxCount Int 60 | memes Meme[] 61 | psl=} 62 | 63 | action createMeme { 64 | fn: import { createMeme } from "@server/actions.js", 65 | entities: [Meme, Template, User] 66 | } 67 | 68 | action editMeme { 69 | fn: import { editMeme } from "@server/actions.js", 70 | entities: [Meme, Template, User] 71 | } 72 | 73 | action deleteMeme { 74 | fn: import { deleteMeme } from "@server/actions.js", 75 | entities: [Meme] 76 | } 77 | 78 | query getAllMemes { 79 | fn: import { getAllMemes } from "@server/queries.js", 80 | entities: [Meme] 81 | } 82 | 83 | query getMeme { 84 | fn: import { getMeme } from "@server/queries.js", 85 | entities: [Meme] 86 | } 87 | 88 | query getMemeTemplates { 89 | fn: import { getMemeTemplates } from "@server/queries.js", 90 | entities: [Template] 91 | } 92 | 93 | route HomePageRoute { path: "/", to: HomePage } 94 | page HomePage { 95 | component: import { HomePage } from "@client/pages/Home", 96 | } 97 | 98 | route EditMemeRoute { path: "/meme/:id", to: EditMemePage } 99 | page EditMemePage { 100 | component: import { EditMemePage } from "@client/pages/EditMemePage", 101 | authRequired: true 102 | } 103 | 104 | route LoginRoute { path: "/login", to: LoginPage } 105 | page LoginPage { 106 | component: import Login from "@client/pages/auth/Login" 107 | } 108 | route SignupRoute { path: "/signup", to: SignupPage } 109 | page SignupPage { 110 | component: import Signup from "@client/pages/auth/Signup" 111 | } 112 | 113 | job storeMemeTemplates { 114 | executor: PgBoss, 115 | perform: { 116 | fn: import { fetchAndStoreMemeTemplates } from "@server/workers.js", 117 | }, 118 | schedule: { 119 | // daily at 7 a.m. 120 | cron: "0 7 * * *" 121 | }, 122 | entities: [Template], 123 | } 124 | -------------------------------------------------------------------------------- /memerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincanger/memerator/7a66bf336f79474275ef35379c7c502b88b02ebf/memerator.png -------------------------------------------------------------------------------- /migrations/20230901115956_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "username" TEXT NOT NULL, 5 | "password" TEXT NOT NULL, 6 | 7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "Meme" ( 12 | "id" TEXT NOT NULL, 13 | "url" TEXT NOT NULL, 14 | "text0" TEXT NOT NULL, 15 | "text1" TEXT NOT NULL, 16 | "templateId" TEXT NOT NULL, 17 | "userId" INTEGER NOT NULL, 18 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | 20 | CONSTRAINT "Meme_pkey" PRIMARY KEY ("id") 21 | ); 22 | 23 | -- CreateTable 24 | CREATE TABLE "Template" ( 25 | "id" TEXT NOT NULL, 26 | "name" TEXT NOT NULL, 27 | "url" TEXT NOT NULL, 28 | "width" INTEGER NOT NULL, 29 | "height" INTEGER NOT NULL, 30 | "boxCount" INTEGER NOT NULL, 31 | 32 | CONSTRAINT "Template_pkey" PRIMARY KEY ("id") 33 | ); 34 | 35 | -- CreateIndex 36 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 37 | 38 | -- CreateIndex 39 | CREATE UNIQUE INDEX "Template_id_key" ON "Template"("id"); 40 | 41 | -- AddForeignKey 42 | ALTER TABLE "Meme" ADD CONSTRAINT "Meme_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 43 | 44 | -- AddForeignKey 45 | ALTER TABLE "Meme" ADD CONSTRAINT "Meme_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 46 | -------------------------------------------------------------------------------- /migrations/20230904084625_topics_audience/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Meme" ADD COLUMN "audience" TEXT NOT NULL DEFAULT 'web developers', 3 | ADD COLUMN "topics" TEXT NOT NULL DEFAULT 'css bugs'; 4 | -------------------------------------------------------------------------------- /migrations/20230904090820_ok/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Meme" ALTER COLUMN "audience" DROP DEFAULT, 3 | ALTER COLUMN "topics" DROP DEFAULT; 4 | -------------------------------------------------------------------------------- /migrations/20230904094512_is_admin/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /migrations/20230904094950_credits/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "credits" INTEGER NOT NULL DEFAULT 2; 3 | -------------------------------------------------------------------------------- /migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /src/.waspignore: -------------------------------------------------------------------------------- 1 | # Ignore editor tmp files 2 | **/*~ 3 | **/#*# 4 | -------------------------------------------------------------------------------- /src/client/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import useAuth from '@wasp/auth/useAuth'; 4 | import logout from '@wasp/auth/logout'; 5 | import { FaRegLaughBeam } from 'react-icons/fa'; 6 | import "./Main.css"; 7 | 8 | export const Layout = ({ children }: { children: ReactNode }) => { 9 | const { data: user } = useAuth(); 10 | 11 | return ( 12 |
13 |
14 |
15 |
16 | 17 |

18 | Memerator 19 |

20 | 21 |
22 | {user ? ( 23 | 24 | Hi, {user.username}!{' '} 25 | 28 | 29 | ) : ( 30 | 31 |

Log in

32 | 33 | )} 34 |
35 |
36 |
{children}
37 | 47 |
48 | ); 49 | }; -------------------------------------------------------------------------------- /src/client/Main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --auth-form-brand: theme(colors.primary.500); 7 | --auth-form-brand-accent: theme(colors.primary.600); 8 | --auth-form-submit-button-text-color: theme(colors.white); 9 | } -------------------------------------------------------------------------------- /src/client/pages/EditMemePage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, FormEventHandler } from 'react'; 2 | import { useQuery } from '@wasp/queries'; 3 | import editMeme from '@wasp/actions/editMeme'; 4 | import getMeme from '@wasp/queries/getMeme'; 5 | import { useParams } from 'react-router-dom'; 6 | import { AiOutlineEdit } from 'react-icons/ai'; 7 | 8 | export function EditMemePage() { 9 | // http://localhost:3000/meme/573f283c-24e2-4c45-b6b9-543d0b7cc0c7 10 | const { id } = useParams<{ id: string }>(); 11 | 12 | const [text0, setText0] = useState(''); 13 | const [text1, setText1] = useState(''); 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | const { data: meme, isLoading: isMemeLoading, error: memeError } = useQuery(getMeme, { id: id }); 17 | 18 | useEffect(() => { 19 | if (meme) { 20 | setText0(meme.text0); 21 | setText1(meme.text1); 22 | } 23 | }, [meme]); 24 | 25 | const handleSubmit: FormEventHandler = async (e) => { 26 | e.preventDefault(); 27 | try { 28 | setIsLoading(true); 29 | await editMeme({ id, text0, text1 }); 30 | } catch (error: any) { 31 | alert('Error generating meme: ' + error.message); 32 | } finally { 33 | setIsLoading(false); 34 | } 35 | }; 36 | 37 | if (isMemeLoading) return 'Loading...'; 38 | if (memeError) return 'Error: ' + memeError.message; 39 | 40 | return ( 41 |
42 |

Edit Meme

43 |
44 |
45 |
46 | 49 |