├── .nvmrc
├── store
├── index.ts
└── colorStore.ts
├── lib
├── constants.ts
├── validations
│ ├── auth.ts
│ ├── user.ts
│ ├── search-generations.ts
│ ├── generate.ts
│ ├── og.ts
│ └── post.ts
├── exceptions.ts
├── stripe.ts
├── session.ts
├── supabase.ts
├── db.ts
├── client-helpers.ts
├── upstash.ts
├── subscription.ts
├── toc.ts
├── generations.ts
├── auth.ts
├── open-ai-stream.ts
└── utils.ts
├── .prettierignore
├── .commitlintrc.json
├── public
├── emm.jpg
├── anime.png
├── sam.jpeg
├── energy.png
├── favicon.png
├── hassan.jpeg
├── shadcn.jpeg
├── shield1.png
├── steven.jpeg
├── landscape.png
├── pixelfy-og.png
├── warhammer.png
├── favicon-16x16.png
├── favicon-32x32.png
├── scenario-logo.png
├── cyberpunk-robot.png
├── apple-touch-icon.png
├── character-portrait.png
├── pixel-background.png
├── anime-cyberpunk-girl.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── examples
│ ├── animeStyle
│ │ ├── dbz.png
│ │ ├── anime.png
│ │ ├── levi.png
│ │ ├── anime-girl.png
│ │ ├── fullmetal.png
│ │ ├── anime-boy-1.png
│ │ ├── anime-boy-2.png
│ │ ├── fullmetal-2.png
│ │ ├── demon-slayer-1.png
│ │ ├── demon-slayer-2.png
│ │ ├── demon-slayer-3.png
│ │ ├── demon-slayer-4.png
│ │ └── hunter-x-hunter.png
│ ├── fantasyRpg
│ │ ├── lux.png
│ │ ├── neo.png
│ │ ├── owl.png
│ │ ├── ekko.png
│ │ ├── vayne.png
│ │ ├── anduinn.png
│ │ ├── illidan.png
│ │ ├── thrall.png
│ │ ├── deathwing.png
│ │ ├── lich-king.png
│ │ ├── scrapyard.png
│ │ ├── warhammer.png
│ │ └── anime-cyberpunk-girl.png
│ ├── shields
│ │ ├── shield1.png
│ │ ├── shield2.png
│ │ ├── shield3.png
│ │ ├── shield4.png
│ │ └── shield5.png
│ ├── skillArt
│ │ ├── skull1.png
│ │ ├── skull2.png
│ │ ├── skull3.png
│ │ ├── skull4.png
│ │ ├── frostbolt1.png
│ │ ├── frostbolt2.png
│ │ ├── frostbolt3.png
│ │ └── frostbolt4.png
│ ├── landscapePortrait
│ │ ├── tower.png
│ │ ├── gothic-1.png
│ │ ├── gothic-2.png
│ │ ├── gothic-3.png
│ │ ├── gothic-4.png
│ │ ├── horde-1.png
│ │ ├── horde-2.png
│ │ ├── horde-3.png
│ │ ├── horde-4.png
│ │ ├── farmhouse.png
│ │ ├── bladerunner-1.png
│ │ ├── bladerunner-2.png
│ │ ├── bladerunner-3.png
│ │ └── bladerunner-4.png
│ └── pixelPortrait
│ │ ├── energy-2.png
│ │ ├── energy-3.png
│ │ └── energy-4.png
├── images
│ ├── avatars
│ │ └── shadcn.png
│ └── blog
│ │ ├── blog-post-1.jpg
│ │ ├── blog-post-2.jpg
│ │ ├── blog-post-3.jpg
│ │ └── blog-post-4.jpg
├── site.webmanifest
└── vercel.svg
├── app
├── opengraph-image.jpg
├── (auth)
│ ├── layout.tsx
│ ├── login
│ │ └── page.tsx
│ └── register
│ │ └── page.tsx
├── robots.ts
├── (dashboard)
│ └── dashboard
│ │ ├── loading.tsx
│ │ ├── refer-users
│ │ └── loading.tsx
│ │ ├── generations
│ │ └── loading.tsx
│ │ ├── page.tsx
│ │ └── layout.tsx
├── i
│ └── [id]
│ │ ├── loading.tsx
│ │ └── layout.tsx
├── (examples)
│ ├── examples
│ │ └── layout.tsx
│ └── layout.tsx
├── (marketing)
│ └── layout.tsx
├── (credits)
│ └── credits
│ │ ├── layout.tsx
│ │ └── page.tsx
├── sitemap.ts
├── api
│ ├── users
│ │ ├── [userId]
│ │ │ └── route.ts
│ │ └── stripe
│ │ │ └── route.ts
│ ├── auth
│ │ └── [...nextauth]
│ │ │ └── _route.tsx
│ ├── generate
│ │ └── prompt-generate
│ │ │ └── route.ts
│ └── feedback
│ │ └── route.ts
└── (legal)
│ └── layout.tsx
├── .husky
├── commit-msg
└── pre-commit
├── assets
└── fonts
│ ├── Inter-Bold.ttf
│ ├── Inter-Regular.ttf
│ ├── CalSans-SemiBold.ttf
│ ├── CalSans-SemiBold.woff
│ └── CalSans-SemiBold.woff2
├── postcss.config.js
├── .prettierrc
├── prisma
└── migrations
│ ├── migration_lock.toml
│ ├── 20221118173244_add_stripe_columns
│ └── migration.sql
│ └── 20221021182747_init
│ └── migration.sql
├── mailing.config.json
├── components
├── analytics.tsx
├── ui
│ ├── aspect-ratio.tsx
│ ├── skeleton.tsx
│ ├── collapsible.tsx
│ ├── label.tsx
│ ├── separator.tsx
│ ├── progress.tsx
│ ├── input.tsx
│ ├── textarea.tsx
│ ├── toaster.tsx
│ ├── hover-card.tsx
│ ├── checkbox.tsx
│ ├── tooltip.tsx
│ ├── popover.tsx
│ ├── slider.tsx
│ ├── badge.tsx
│ ├── switch.tsx
│ ├── radio-group.tsx
│ ├── avatar.tsx
│ ├── toggle.tsx
│ ├── scroll-area.tsx
│ ├── alert.tsx
│ ├── button.tsx
│ ├── tabs.tsx
│ ├── card.tsx
│ ├── accordion.tsx
│ └── calendar.tsx
├── session-provider.tsx
├── pixelated-image.tsx
├── shell.tsx
├── theme-provider.tsx
├── card-skeleton.tsx
├── login-button.tsx
├── page-header.tsx
├── callout.tsx
├── header.tsx
├── user-avatar.tsx
├── download-image-button.tsx
├── carbon.tsx
├── modal.tsx
├── mdx-card.tsx
├── model-select-button.tsx
├── search.tsx
├── stats.tsx
├── motion-card-hover.tsx
├── copy-link-button.tsx
├── nav.tsx
├── stripe-pricing-table.tsx
├── mode-toggle.tsx
├── mobile-nav.tsx
├── search-generations-input.tsx
├── background.module.css
├── remove-background-button.tsx
├── sidebar-nav.tsx
├── images-selector.tsx
├── empty-placeholder.tsx
├── guidance-selector.tsx
├── image-influence-slider.tsx
├── image-amount-selector.tsx
├── sampling-step-selector.tsx
├── main-nav.tsx
├── generations-pagination.tsx
├── billing-form.tsx
├── toc.tsx
├── site-footer.tsx
└── user-name-form.tsx
├── .editorconfig
├── hooks
├── use-mounted.ts
└── use-lock-body.ts
├── config
├── marketing.ts
├── subscriptions.ts
├── site.ts
├── docs.ts
└── dashboard.ts
├── types
├── next-auth.d.ts
├── index.d.ts
└── scenario.ts
├── emails
├── previews
│ └── Account.tsx
├── components
│ ├── theme.ts
│ ├── Text.tsx
│ ├── Divider.tsx
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── ButtonPrimary.tsx
├── index.ts
└── LoginLink.tsx
├── next.config.mjs
├── .gitignore
├── pages
└── api
│ └── cron
│ └── index.ts
├── .eslintrc.json
├── prettier.config.js
├── tsconfig.json
├── styles
└── mdx.css
├── middleware.ts
└── .env.example
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.18.0
2 |
--------------------------------------------------------------------------------
/store/index.ts:
--------------------------------------------------------------------------------
1 | export { useColorStore } from "./colorStore"
2 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const LOCALHOST_IP = "98.234.147.44"
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .next
4 | build
5 | .contentlayer
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/emm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/emm.jpg
--------------------------------------------------------------------------------
/public/anime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/anime.png
--------------------------------------------------------------------------------
/public/sam.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/sam.jpeg
--------------------------------------------------------------------------------
/public/energy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/energy.png
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/public/hassan.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/hassan.jpeg
--------------------------------------------------------------------------------
/public/shadcn.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/shadcn.jpeg
--------------------------------------------------------------------------------
/public/shield1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/shield1.png
--------------------------------------------------------------------------------
/public/steven.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/steven.jpeg
--------------------------------------------------------------------------------
/public/landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/landscape.png
--------------------------------------------------------------------------------
/public/pixelfy-og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/pixelfy-og.png
--------------------------------------------------------------------------------
/public/warhammer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/warhammer.png
--------------------------------------------------------------------------------
/app/opengraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/app/opengraph-image.jpg
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/scenario-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/scenario-logo.png
--------------------------------------------------------------------------------
/public/cyberpunk-robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/cyberpunk-robot.png
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx pretty-quick --staged
5 |
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/assets/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/character-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/character-portrait.png
--------------------------------------------------------------------------------
/public/pixel-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/pixel-background.png
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/assets/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/public/anime-cyberpunk-girl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/anime-cyberpunk-girl.png
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/assets/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/assets/fonts/CalSans-SemiBold.woff
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/dbz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/dbz.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/lux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/lux.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/neo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/neo.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/owl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/owl.png
--------------------------------------------------------------------------------
/public/images/avatars/shadcn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/images/avatars/shadcn.png
--------------------------------------------------------------------------------
/public/images/blog/blog-post-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/images/blog/blog-post-1.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/images/blog/blog-post-2.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/images/blog/blog-post-3.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/images/blog/blog-post-4.jpg
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/assets/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/public/examples/animeStyle/anime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/anime.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/levi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/levi.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/ekko.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/ekko.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/vayne.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/vayne.png
--------------------------------------------------------------------------------
/public/examples/shields/shield1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/shields/shield1.png
--------------------------------------------------------------------------------
/public/examples/shields/shield2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/shields/shield2.png
--------------------------------------------------------------------------------
/public/examples/shields/shield3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/shields/shield3.png
--------------------------------------------------------------------------------
/public/examples/shields/shield4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/shields/shield4.png
--------------------------------------------------------------------------------
/public/examples/shields/shield5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/shields/shield5.png
--------------------------------------------------------------------------------
/public/examples/skillArt/skull1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/skull1.png
--------------------------------------------------------------------------------
/public/examples/skillArt/skull2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/skull2.png
--------------------------------------------------------------------------------
/public/examples/skillArt/skull3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/skull3.png
--------------------------------------------------------------------------------
/public/examples/skillArt/skull4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/skull4.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/anduinn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/anduinn.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/illidan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/illidan.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/thrall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/thrall.png
--------------------------------------------------------------------------------
/public/examples/skillArt/frostbolt1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/frostbolt1.png
--------------------------------------------------------------------------------
/public/examples/skillArt/frostbolt2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/frostbolt2.png
--------------------------------------------------------------------------------
/public/examples/skillArt/frostbolt3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/frostbolt3.png
--------------------------------------------------------------------------------
/public/examples/skillArt/frostbolt4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/skillArt/frostbolt4.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/anime-girl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/anime-girl.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/fullmetal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/fullmetal.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/deathwing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/deathwing.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/lich-king.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/lich-king.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/scrapyard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/scrapyard.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/warhammer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/warhammer.png
--------------------------------------------------------------------------------
/lib/validations/auth.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userAuthSchema = z.object({
4 | email: z.string().email(),
5 | })
6 |
--------------------------------------------------------------------------------
/public/examples/animeStyle/anime-boy-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/anime-boy-1.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/anime-boy-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/anime-boy-2.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/fullmetal-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/fullmetal-2.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/tower.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/tower.png
--------------------------------------------------------------------------------
/public/examples/pixelPortrait/energy-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/pixelPortrait/energy-2.png
--------------------------------------------------------------------------------
/public/examples/pixelPortrait/energy-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/pixelPortrait/energy-3.png
--------------------------------------------------------------------------------
/public/examples/pixelPortrait/energy-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/pixelPortrait/energy-4.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "singleQuote": false,
5 | "tabWidth": 4,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/lib/validations/user.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userNameSchema = z.object({
4 | name: z.string().min(3).max(32),
5 | })
6 |
--------------------------------------------------------------------------------
/public/examples/animeStyle/demon-slayer-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/demon-slayer-1.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/demon-slayer-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/demon-slayer-2.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/demon-slayer-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/demon-slayer-3.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/demon-slayer-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/demon-slayer-4.png
--------------------------------------------------------------------------------
/public/examples/animeStyle/hunter-x-hunter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/animeStyle/hunter-x-hunter.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/gothic-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/gothic-1.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/gothic-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/gothic-2.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/gothic-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/gothic-3.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/gothic-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/gothic-4.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/horde-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/horde-1.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/horde-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/horde-2.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/horde-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/horde-3.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/horde-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/horde-4.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/farmhouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/farmhouse.png
--------------------------------------------------------------------------------
/public/examples/fantasyRpg/anime-cyberpunk-girl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/fantasyRpg/anime-cyberpunk-girl.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/bladerunner-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/bladerunner-1.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/bladerunner-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/bladerunner-2.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/bladerunner-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/bladerunner-3.png
--------------------------------------------------------------------------------
/public/examples/landscapePortrait/bladerunner-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidTParks/pixelfy/HEAD/public/examples/landscapePortrait/bladerunner-4.png
--------------------------------------------------------------------------------
/prisma/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 = "mysql"
--------------------------------------------------------------------------------
/lib/exceptions.ts:
--------------------------------------------------------------------------------
1 | export class RequiresProPlanError extends Error {
2 | constructor(message = "This action requires a pro plan") {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/lib/validations/search-generations.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const searchGenerationsSchema = z.object({
4 | input: z.string().optional(),
5 | })
6 |
--------------------------------------------------------------------------------
/mailing.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript": true,
3 | "emailsDir": "./emails",
4 | "outDir": "./previews_html",
5 | "anonymousId": "809bd078-1867-48f9-9169-9d870291905a"
6 | }
7 |
--------------------------------------------------------------------------------
/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe"
2 |
3 | export const stripe = new Stripe(process.env.STRIPE_API_KEY || "", {
4 | apiVersion: "2022-11-15",
5 | typescript: true,
6 | })
7 |
--------------------------------------------------------------------------------
/lib/validations/generate.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const generateSchema = z.object({
4 | prompt: z.string().max(500),
5 | modelId: z.string().optional(),
6 | })
7 |
--------------------------------------------------------------------------------
/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
4 |
5 | export function Analytics() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/lib/validations/og.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const ogImageSchema = z.object({
4 | heading: z.string(),
5 | type: z.string(),
6 | mode: z.enum(["light", "dark"]).default("dark"),
7 | })
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode
3 | }
4 |
5 | export default function AuthLayout({ children }: AuthLayoutProps) {
6 | return
{children}
7 | }
8 |
--------------------------------------------------------------------------------
/lib/validations/post.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const postPatchSchema = z.object({
4 | title: z.string().min(3).max(128).optional(),
5 |
6 | // TODO: Type this properly from editorjs block types?
7 | content: z.any().optional(),
8 | })
9 |
--------------------------------------------------------------------------------
/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | setMounted(true)
8 | }, [])
9 |
10 | return mounted
11 | }
12 |
--------------------------------------------------------------------------------
/components/session-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"
4 |
5 | export function SessionProvider({ children }) {
6 | return {children}
7 | }
8 |
--------------------------------------------------------------------------------
/lib/session.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 |
3 | import { authOptions } from "@/lib/auth"
4 |
5 | export async function getCurrentUser() {
6 | const session = await getServerSession(authOptions)
7 |
8 | return session?.user
9 | }
10 |
--------------------------------------------------------------------------------
/lib/supabase.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@supabase/supabase-js"
2 |
3 | const SUPABASE_URL = process.env.SUPABASE_URL as string
4 | const SUPABASE_KEY = process.env.SUPABASE_KEY as string
5 | // Create a single supabase client for interacting with your database
6 | export const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
7 |
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next"
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: "*",
7 | allow: "/",
8 | disallow: "/dashboard/",
9 | },
10 | sitemap: "https://pixelfy.ai/sitemap.xml",
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/config/marketing.ts:
--------------------------------------------------------------------------------
1 | import { MarketingConfig } from "types"
2 |
3 | export const marketingConfig: MarketingConfig = {
4 | mainNav: [
5 | {
6 | title: "Features",
7 | href: "/#features",
8 | },
9 | {
10 | title: "Examples",
11 | href: "/#examples",
12 | },
13 | ],
14 | }
15 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/hooks/use-lock-body.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | // @see https://usehooks.com/useLockBodyScroll.
4 | export function useLockBody() {
5 | React.useLayoutEffect((): (() => void) => {
6 | const originalStyle: string = window.getComputedStyle(
7 | document.body
8 | ).overflow
9 | document.body.style.overflow = "hidden"
10 | return () => (document.body.style.overflow = originalStyle)
11 | }, [])
12 | }
13 |
--------------------------------------------------------------------------------
/components/pixelated-image.tsx:
--------------------------------------------------------------------------------
1 | import { pixelateImage } from "@/lib/utils"
2 | import Image, { ImageProps } from "next/image"
3 |
4 | type TPixelatedImageProps = ImageProps & {
5 | src: string
6 | }
7 | export async function PixelatedImage(props: TPixelatedImageProps) {
8 | const pixelatedImage = await pixelateImage({ remoteUrl: props.src })
9 |
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import { User } from "next-auth"
2 | import { JWT } from "next-auth/jwt"
3 |
4 | type UserId = string
5 |
6 | declare module "next-auth/jwt" {
7 | interface JWT {
8 | id: UserId
9 | credits: number
10 | }
11 | }
12 |
13 | declare module "next-auth" {
14 | interface Session {
15 | user: User & {
16 | id: UserId
17 | credits: number
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface DashboardShellProps extends React.HTMLAttributes {}
6 |
7 | export function DashboardShell({
8 | children,
9 | className,
10 | ...props
11 | }: DashboardShellProps) {
12 | return (
13 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client"
2 |
3 | declare global {
4 | // eslint-disable-next-line no-var
5 | var cachedPrisma: PrismaClient
6 | }
7 |
8 | let prisma: PrismaClient
9 | if (process.env.NODE_ENV === "production") {
10 | prisma = new PrismaClient()
11 | } else {
12 | if (!global.cachedPrisma) {
13 | global.cachedPrisma = new PrismaClient()
14 | }
15 | prisma = global.cachedPrisma
16 | }
17 |
18 | export const db = prisma
19 |
--------------------------------------------------------------------------------
/config/subscriptions.ts:
--------------------------------------------------------------------------------
1 | import { SubscriptionPlan } from "types"
2 |
3 | export const freePlan: SubscriptionPlan = {
4 | name: "Free",
5 | description:
6 | "The free plan is limited to 3 posts. Upgrade to the PRO plan for unlimited posts.",
7 | stripePriceId: "",
8 | }
9 |
10 | export const proPlan: SubscriptionPlan = {
11 | name: "PRO",
12 | description: "The PRO plan has unlimited posts.",
13 | stripePriceId: process.env.STRIPE_PRO_MONTHLY_PLAN_ID || "",
14 | }
15 |
--------------------------------------------------------------------------------
/emails/previews/Account.tsx:
--------------------------------------------------------------------------------
1 | import { default as LoginLinkEmail } from "../LoginLink"
2 | import { default as WelcomeEmailChild } from "../WelcomeEmail"
3 |
4 | export function LoginLink() {
5 | return (
6 |
7 | )
8 | }
9 |
10 | export function WelcomeEmail() {
11 | return
12 | }
13 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SessionProvider } from "./session-provider"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { ThemeProviderProps } from "next-themes/dist/types"
6 | import * as React from "react"
7 |
8 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | import { SiteConfig } from "types"
2 |
3 | export const siteConfig: SiteConfig = {
4 | name: "Pixelfy",
5 | description:
6 | "Pixelfy is an AI-powered application that generates stunning pixel art images. Create unique pixel art with our artificial intelligence tool.",
7 | url: "https://pixelfy.ai",
8 | ogImage: "https://pixelfy.ai/pixelfy-og.png",
9 | links: {
10 | twitter: "https://twitter.com/dparksdev",
11 | github: "https://github.com/DavidTParks/pixelfy",
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: [
6 | "avatars.githubusercontent.com",
7 | "cdn.cloud.scenario.com",
8 | "jpxwqgklwwytoznbpbmn.supabase.co",
9 | "lh3.googleusercontent.com"
10 | ],
11 | },
12 | experimental: {
13 | appDir: true,
14 | serverComponentsExternalPackages: ["@prisma/client"],
15 | },
16 | }
17 |
18 | export default nextConfig
19 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pixelfy",
3 | "short_name": "Pixelfy",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/emails/components/theme.ts:
--------------------------------------------------------------------------------
1 | // Colors
2 | export const black = "#030711"
3 | export const white = "#fff"
4 | export const purple = "#bd11e0"
5 | export const blue = "#2563EB"
6 | export const gold = "#fadf98"
7 | export const grayDark = "#888"
8 | export const grayLight = "#eaeaea"
9 |
10 | // Typography
11 | export const textSm = 14
12 | export const textBase = 16
13 | export const textLg = 24
14 | export const textXl = 30
15 | export const leadingTight = "120%"
16 | export const leadingRelaxed = "160%"
17 |
18 | // Borders
19 | export const borderBase = 6
20 |
--------------------------------------------------------------------------------
/components/card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
2 | import { Skeleton } from "@/components/ui/skeleton"
3 |
4 | export function CardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | .vscode
40 | .contentlayer
41 | .mailing
42 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/card-skeleton"
2 | import { DashboardHeader } from "@/components/header"
3 | import { DashboardShell } from "@/components/shell"
4 |
5 | export default function DashboardSettingsLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/config/docs.ts:
--------------------------------------------------------------------------------
1 | import { scenarioModelData } from "@/lib/generators"
2 | import { DocsConfig } from "types"
3 |
4 | const sideBarExamples = Object.values(scenarioModelData).map(
5 | ({ slug, name, examples }) => ({
6 | title: name,
7 | href: `/examples/${slug}`,
8 | })
9 | )
10 |
11 | export const docsConfig: DocsConfig = {
12 | mainNav: [
13 | {
14 | title: "Create",
15 | href: "/dashboard",
16 | },
17 | ],
18 | sidebarNav: [
19 | {
20 | title: "Styles",
21 | items: sideBarExamples,
22 | },
23 | ],
24 | }
25 |
--------------------------------------------------------------------------------
/components/login-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { buttonVariants } from "./ui/button"
4 | import { cn } from "@/lib/utils"
5 | import { useSession } from "next-auth/react"
6 | import Link from "next/link"
7 |
8 | export function LoginButton() {
9 | const { status } = useSession()
10 | return (
11 |
18 | {status === "authenticated" ? "Dashboard" : "Login"}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/emails/components/Text.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { MjmlText } from "mjml-react"
3 |
4 | type TextProps = {
5 | maxWidth?: number
6 | } & React.ComponentProps
7 |
8 | export default function Text({ children, maxWidth, ...props }: TextProps) {
9 | if (maxWidth) {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | } else
16 | return (
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/refer-users/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/card-skeleton"
2 | import { DashboardHeader } from "@/components/header"
3 | import { DashboardShell } from "@/components/shell"
4 |
5 | export default function DashboardSettingsLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface DocsPageHeaderProps extends React.HTMLAttributes {
4 | heading: string
5 | text?: string
6 | }
7 |
8 | export function DocsPageHeader({
9 | heading,
10 | text,
11 | className,
12 | ...props
13 | }: DocsPageHeaderProps) {
14 | return (
15 | <>
16 |
17 |
18 | {heading}
19 |
20 | {text &&
{text}
}
21 |
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/lib/client-helpers.ts:
--------------------------------------------------------------------------------
1 | import va from "@vercel/analytics"
2 |
3 | export async function downloadImage(url: string, imageName: string) {
4 | va.track("downloadImageSelected", {
5 | url,
6 | imageName,
7 | })
8 | const a = document.createElement("a")
9 | a.href = await toDataURL(url)
10 | a.download = imageName
11 | document.body.appendChild(a)
12 | a.click()
13 | document.body.removeChild(a)
14 | }
15 |
16 | export function toDataURL(url: string) {
17 | return fetch(url)
18 | .then((response) => {
19 | return response.blob()
20 | })
21 | .then((blob) => {
22 | return URL.createObjectURL(blob)
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/components/callout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface CalloutProps {
4 | icon?: string
5 | children?: React.ReactNode
6 | type?: "default" | "warning" | "danger"
7 | }
8 |
9 | export function Callout({
10 | children,
11 | icon,
12 | type = "default",
13 | ...props
14 | }: CalloutProps) {
15 | return (
16 |
23 | {icon &&
{icon}}
24 |
{children}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | interface DashboardHeaderProps {
2 | heading: string
3 | text?: string
4 | children?: React.ReactNode
5 | }
6 |
7 | export function DashboardHeader({
8 | heading,
9 | text,
10 | children,
11 | }: DashboardHeaderProps) {
12 | return (
13 |
14 |
15 |
{heading}
16 | {text && (
17 |
{text}
18 | )}
19 |
20 | {children}
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/lib/upstash.ts:
--------------------------------------------------------------------------------
1 | import { Ratelimit } from "@upstash/ratelimit"
2 | import { Redis } from "@upstash/redis"
3 |
4 | const redisRestUrl = process.env.UPSTASH_REDIS_REST_URL as string
5 | const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN as string
6 |
7 | export const redis = new Redis({
8 | url: redisRestUrl,
9 | token: redisToken,
10 | })
11 |
12 | export const ratelimit = (
13 | requests: number = 10,
14 | seconds:
15 | | `${number} ms`
16 | | `${number} s`
17 | | `${number} m`
18 | | `${number} h`
19 | | `${number} d` = "10 s"
20 | ) => {
21 | return new Ratelimit({
22 | redis: redis,
23 | limiter: Ratelimit.slidingWindow(requests, seconds),
24 | analytics: true,
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/pages/api/cron/index.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db"
2 | import { verifySignature } from "@upstash/qstash/nextjs"
3 |
4 | async function handler(req, res) {
5 | const fifteenMinutesAgo = new Date(Date.now() - 1000 * 60 * 15)
6 |
7 | // Update generations as timed out if 15 minutes have passed
8 | await db.generation.updateMany({
9 | where: {
10 | createdAt: {
11 | lte: fifteenMinutesAgo,
12 | },
13 | status: "PROCESSING",
14 | },
15 | data: {
16 | status: "TIMEOUT",
17 | },
18 | })
19 |
20 | res.status(200).end()
21 | }
22 |
23 | export default verifySignature(handler)
24 |
25 | export const config = {
26 | api: {
27 | bodyParser: false,
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client"
2 | import { AvatarProps } from "@radix-ui/react-avatar"
3 |
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
5 | import { Icons } from "@/components/icons"
6 |
7 | interface UserAvatarProps extends AvatarProps {
8 | user: Pick
9 | }
10 |
11 | export function UserAvatar({ user, ...props }: UserAvatarProps) {
12 | return (
13 |
14 | {user.image ? (
15 |
16 | ) : (
17 |
18 | {user.name}
19 |
20 |
21 | )}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/download-image-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Icons } from "@/components/icons"
4 | import { Button } from "@/components/ui/button"
5 | import { downloadImage } from "@/lib/client-helpers"
6 |
7 | interface IDownloadImageButton {
8 | src: string
9 | name: string
10 | }
11 | export const DownloadImageButton = ({ src, name }: IDownloadImageButton) => {
12 | return (
13 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/emails/components/Divider.tsx:
--------------------------------------------------------------------------------
1 | import { MjmlDivider } from "mjml-react"
2 | import { grayDark, grayLight } from "./theme"
3 |
4 | export default function Divider({
5 | bottomPadding,
6 | }: {
7 | bottomPadding?: boolean
8 | }): JSX.Element {
9 | return (
10 | <>
11 |
17 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/emails/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { MjmlColumn, MjmlImage, MjmlSection, MjmlText } from "mjml-react"
2 |
3 | export default function Header({ title }: { title: string }): JSX.Element {
4 | return (
5 |
6 |
7 |
15 |
16 | {title}
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/app/i/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { DashboardShell } from "@/components/shell"
2 | import { AspectRatio } from "@/components/ui/aspect-ratio"
3 | import { Skeleton } from "@/components/ui/skeleton"
4 |
5 | export default function LoadingImage() {
6 | return (
7 |
8 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/carbon.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect, useRef } from "react"
4 |
5 | export const Carbon = () => {
6 | useEffect(() => {
7 | const script = document.createElement("script")
8 | const carbonContainer = document.getElementById("carbon-container")
9 | script.setAttribute("async", "true")
10 | script.setAttribute(
11 | "src",
12 | "//cdn.carbonads.com/carbon.js?serve=CWYDP5QM&placement=wwwpixelfyai"
13 | )
14 | script.setAttribute("id", "_carbonads_js")
15 |
16 | if (carbonContainer) {
17 | carbonContainer.appendChild(script)
18 | }
19 |
20 | return () => {
21 | if (carbonContainer) {
22 | carbonContainer.removeChild(script)
23 | }
24 | }
25 | }, [])
26 | return null
27 | }
28 |
--------------------------------------------------------------------------------
/components/modal.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Dialog, DialogContent, DialogFooter } from "@/components/ui/dialog"
4 | import { useRouter } from "next/navigation"
5 |
6 | interface IModal {
7 | footer: React.ReactNode
8 | children: React.ReactNode
9 | }
10 | export default function Modal({ children, footer }: IModal) {
11 | const router = useRouter()
12 |
13 | const handleOnOpenChange = (open: boolean) => {
14 | if (!open) {
15 | router.back()
16 | }
17 | }
18 | return (
19 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "@next/next/no-html-link-for-pages": "off",
12 | "react/jsx-key": "off",
13 | "tailwindcss/no-custom-classname": "off",
14 | "tailwindcss/classnames-order": "off",
15 | "react/no-unescaped-entities": "off"
16 | },
17 | "settings": {
18 | "tailwindcss": {
19 | "callees": ["cn"],
20 | "config": "tailwind.config.js"
21 | },
22 | "next": {
23 | "rootDir": true
24 | }
25 | },
26 | "overrides": [
27 | {
28 | "files": ["*.ts", "*.tsx"],
29 | "parser": "@typescript-eslint/parser"
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import * as React from "react"
3 |
4 | export interface TextareaProps
5 | extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | }
20 | )
21 | Textarea.displayName = "Textarea"
22 |
23 | export { Textarea }
24 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "",
13 | "^types$",
14 | "^@/types/(.*)$",
15 | "^@/config/(.*)$",
16 | "^@/lib/(.*)$",
17 | "^@/hooks/(.*)$",
18 | "^@/components/ui/(.*)$",
19 | "^@/components/(.*)$",
20 | "^@/styles/(.*)$",
21 | "^@/app/(.*)$",
22 | "",
23 | "^[./]",
24 | ],
25 | importOrderSeparation: false,
26 | importOrderSortSpecifiers: true,
27 | importOrderBuiltinModulesToTop: true,
28 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
29 | importOrderMergeDuplicateImports: true,
30 | importOrderCombineTypeAndValueImports: true,
31 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
32 | }
33 |
--------------------------------------------------------------------------------
/app/(examples)/examples/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Carbon } from "@/components/carbon"
2 | import { DocsSidebarNav } from "@/components/sidebar-nav"
3 | import { docsConfig } from "@/config/docs"
4 |
5 | interface DocsLayoutProps {
6 | children: React.ReactNode
7 | }
8 |
9 | export default function DocsLayout({ children }: DocsLayoutProps) {
10 | return (
11 |
12 |
17 |
18 | {children}
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./*"]
20 | },
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "strictNullChecks": true
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts",
33 | ".contentlayer/generated"
34 | ],
35 | "exclude": ["node_modules"]
36 | }
37 |
--------------------------------------------------------------------------------
/styles/mdx.css:
--------------------------------------------------------------------------------
1 | [data-rehype-pretty-code-fragment] code {
2 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black;
3 | counter-reset: line;
4 | box-decoration-break: clone;
5 | }
6 | [data-rehype-pretty-code-fragment] .line {
7 | @apply px-4 py-1;
8 | }
9 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before {
10 | counter-increment: line;
11 | content: counter(line);
12 | display: inline-block;
13 | width: 1rem;
14 | margin-right: 1rem;
15 | text-align: right;
16 | color: gray;
17 | }
18 | [data-rehype-pretty-code-fragment] .line--highlighted {
19 | @apply bg-slate-300 bg-opacity-10;
20 | }
21 | [data-rehype-pretty-code-fragment] .line-highlighted span {
22 | @apply relative;
23 | }
24 | [data-rehype-pretty-code-fragment] .word--highlighted {
25 | @apply rounded-md bg-slate-300 bg-opacity-10 p-1;
26 | }
27 | [data-rehype-pretty-code-title] {
28 | @apply mt-4 py-2 px-4 text-sm font-medium;
29 | }
30 | [data-rehype-pretty-code-title] + pre {
31 | @apply mt-0;
32 | }
33 |
--------------------------------------------------------------------------------
/components/mdx-card.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface CardProps extends React.HTMLAttributes {
6 | href?: string
7 | disabled?: boolean
8 | }
9 |
10 | export function MdxCard({
11 | href,
12 | className,
13 | children,
14 | disabled,
15 | ...props
16 | }: CardProps) {
17 | return (
18 |
26 |
27 |
28 | {children}
29 |
30 |
31 | {href && (
32 |
33 |
View
34 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/app/i/[id]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { LoginButton } from "@/components/login-button"
2 | import { MainNav } from "@/components/main-nav"
3 | import { SiteFooter } from "@/components/site-footer"
4 | import { marketingConfig } from "@/config/marketing"
5 |
6 | interface MarketingLayoutProps {
7 | children: React.ReactNode
8 | }
9 |
10 | export default async function MarketingLayout({
11 | children,
12 | }: MarketingLayoutProps) {
13 | return (
14 | <>
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
{children}
25 |
26 |
27 | >
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/lib/subscription.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // TODO: Fix this when we turn strict mode on.
3 | import { UserSubscriptionPlan } from "types"
4 | import { freePlan, proPlan } from "@/config/subscriptions"
5 | import { db } from "@/lib/db"
6 |
7 | export async function getUserSubscriptionPlan(
8 | userId: string
9 | ): Promise {
10 | const user = await db.user.findFirst({
11 | where: {
12 | id: userId,
13 | },
14 | select: {
15 | stripeSubscriptionId: true,
16 | stripeCurrentPeriodEnd: true,
17 | stripeCustomerId: true,
18 | stripePriceId: true,
19 | },
20 | })
21 |
22 | if (!user) {
23 | throw new Error("User not found")
24 | }
25 |
26 | // Check if user is on a pro plan.
27 | const isPro =
28 | user.stripePriceId &&
29 | user.stripeCurrentPeriodEnd?.getTime() + 86_400_000 > Date.now()
30 |
31 | const plan = isPro ? proPlan : freePlan
32 |
33 | return {
34 | ...plan,
35 | ...user,
36 | stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd?.getTime(),
37 | isPro,
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { LoginButton } from "@/components/login-button"
2 | import { MainNav } from "@/components/main-nav"
3 | import { SiteFooter } from "@/components/site-footer"
4 | import { marketingConfig } from "@/config/marketing"
5 |
6 | interface MarketingLayoutProps {
7 | children: React.ReactNode
8 | }
9 |
10 | export default async function MarketingLayout({
11 | children,
12 | }: MarketingLayoutProps) {
13 | return (
14 | <>
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
{children}
25 |
26 |
27 | >
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { DashboardConfig } from "types"
2 |
3 | export const dashboardConfig: DashboardConfig = {
4 | mainNav: [
5 | {
6 | title: "Create",
7 | href: "/dashboard",
8 | },
9 | {
10 | title: "Examples",
11 | href: "/examples/pixel-background",
12 | },
13 | ],
14 | sidebarNav: [
15 | {
16 | title: "Create",
17 | href: "/dashboard",
18 | icon: "terminal",
19 | },
20 | {
21 | title: "Generations",
22 | href: "/dashboard/generations",
23 | icon: "imagePlus",
24 | },
25 | {
26 | title: "Refer & Earn",
27 | href: "/dashboard/refer-users",
28 | icon: "userPlus",
29 | },
30 | {
31 | title: "Buy credits",
32 | href: "/credits",
33 | icon: "billing",
34 | },
35 | // {
36 | // title: "Settings",
37 | // href: "/dashboard/settings",
38 | // icon: "settings",
39 | // },
40 | ],
41 | }
42 |
--------------------------------------------------------------------------------
/components/model-select-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectLabel,
9 | SelectTrigger,
10 | SelectValue,
11 | } from "@/components/ui/select"
12 | import * as React from "react"
13 | import { ComponentProps } from "react"
14 |
15 | export type TModelSelectButton = ComponentProps & {}
16 | export function ModelSelectButton({ ...props }: TModelSelectButton) {
17 | return (
18 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/app/(credits)/credits/layout.tsx:
--------------------------------------------------------------------------------
1 | import { MainNav } from "@/components/main-nav"
2 | import { SiteFooter } from "@/components/site-footer"
3 | import { UserAccountNav } from "@/components/user-account-nav"
4 | import { dashboardConfig } from "@/config/dashboard"
5 |
6 | interface DashboardLayoutProps {
7 | children?: React.ReactNode
8 | }
9 |
10 | export default async function DashboardLayout({
11 | children,
12 | }: DashboardLayoutProps) {
13 | return (
14 |
15 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/emails/index.ts:
--------------------------------------------------------------------------------
1 | import { buildSendMail } from "mailing-core"
2 | import nodemailer from "nodemailer"
3 |
4 | export const sendMail = buildSendMail({
5 | transport: nodemailer.createTransport({
6 | host: "smtp.postmarkapp.com",
7 | port: 587,
8 | auth: {
9 | user: process.env.POSTMARK_API_KEY,
10 | pass: process.env.POSTMARK_API_KEY,
11 | },
12 | pool: true,
13 | }),
14 | defaultFrom: "David from Pixelfy ",
15 | configPath: "./mailing.config.json",
16 | })
17 |
18 | export const asyncSendMail = async ({ component, to, subject }) => {
19 | return sendMail({ component, to, subject })
20 | }
21 |
22 | export const sendMarketingMail = buildSendMail({
23 | transport: nodemailer.createTransport({
24 | host: "smtp-broadcasts.postmarkapp.com",
25 | port: 587,
26 | auth: {
27 | user: process.env.POSTMARK_MARKETING_API_KEY,
28 | pass: process.env.POSTMARK_MARKETING_API_SECRET,
29 | },
30 | pool: true,
31 | }),
32 | defaultFrom: "David from Pixelfy ",
33 | configPath: "./mailing.config.json",
34 | })
35 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/prisma/migrations/20221118173244_add_stripe_columns/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[stripe_customer_id]` on the table `users` will be added. If there are existing duplicate values, this will fail.
5 | - A unique constraint covering the columns `[stripe_subscription_id]` on the table `users` will be added. If there are existing duplicate values, this will fail.
6 |
7 | */
8 | -- DropForeignKey
9 | ALTER TABLE `accounts` DROP FOREIGN KEY `accounts_userId_fkey`;
10 |
11 | -- DropForeignKey
12 | ALTER TABLE `posts` DROP FOREIGN KEY `posts_authorId_fkey`;
13 |
14 | -- DropForeignKey
15 | ALTER TABLE `sessions` DROP FOREIGN KEY `sessions_userId_fkey`;
16 |
17 | -- AlterTable
18 | ALTER TABLE `users` ADD COLUMN `stripe_current_period_end` DATETIME(3) NULL,
19 | ADD COLUMN `stripe_customer_id` VARCHAR(191) NULL,
20 | ADD COLUMN `stripe_price_id` VARCHAR(191) NULL,
21 | ADD COLUMN `stripe_subscription_id` VARCHAR(191) NULL;
22 |
23 | -- CreateIndex
24 | CREATE UNIQUE INDEX `users_stripe_customer_id_key` ON `users`(`stripe_customer_id`);
25 |
26 | -- CreateIndex
27 | CREATE UNIQUE INDEX `users_stripe_subscription_id_key` ON `users`(`stripe_subscription_id`);
28 |
--------------------------------------------------------------------------------
/store/colorStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand"
2 |
3 | interface IssueSelectStore {
4 | colorPaletteEnabled: boolean
5 | colors: number[][]
6 | toggleColorPalette: () => void
7 | addColor: (color: number[]) => void
8 | removeColor: (index: number) => void
9 | disableColorPalette: () => void
10 | }
11 |
12 | export const useColorStore = create((set) => ({
13 | colorPaletteEnabled: false,
14 | colors: [],
15 | toggleColorPalette: () => {
16 | set((state) => ({
17 | colorPaletteEnabled: !state.colorPaletteEnabled,
18 | }))
19 | },
20 | disableColorPalette: () => {
21 | set(() => ({
22 | colorPaletteEnabled: false,
23 | }))
24 | },
25 | enableColorPalette: () => {
26 | set(() => ({
27 | colorPaletteEnabled: true,
28 | }))
29 | },
30 | addColor: (color: number[]) => {
31 | set((state) => ({
32 | colors: [...state.colors, color],
33 | }))
34 | },
35 | removeColor: (index: number) => {
36 | set((state) => ({
37 | colors: state.colors.filter((_, i) => i !== index) as number[][],
38 | }))
39 | },
40 | }))
41 |
--------------------------------------------------------------------------------
/emails/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { MjmlColumn, MjmlSection, MjmlText } from "mjml-react"
2 |
3 | export default function Footer({
4 | unsubscribe,
5 | footnote = true,
6 | }: {
7 | unsubscribe?: boolean
8 | footnote?: boolean
9 | }): JSX.Element {
10 | return (
11 |
12 |
13 |
14 | © {new Date().getFullYear()} Pixelfy.ai
15 | {unsubscribe && (
16 | <>
17 | ·
18 |
19 | Unsubscribe
20 |
21 | >
22 | )}
23 |
24 | {footnote && (
25 |
26 | If you have any feedback or questions about this email,
27 | simply reply to it. I'd love to hear from you!
28 |
29 | )}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/search.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { Input } from "@/components/ui/input"
7 | import { toast } from "@/components/ui/use-toast"
8 |
9 | interface DocsSearchProps extends React.HTMLAttributes {}
10 |
11 | export function DocsSearch({ className, ...props }: DocsSearchProps) {
12 | function onSubmit(event: React.SyntheticEvent) {
13 | event.preventDefault()
14 |
15 | return toast({
16 | title: "Not implemented",
17 | description: "We're still working on the search.",
18 | })
19 | }
20 |
21 | return (
22 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
13 | secondary:
14 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
15 | destructive:
16 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/generations/loading.tsx:
--------------------------------------------------------------------------------
1 | import { DashboardHeader } from "@/components/header"
2 | import { ImageLoadingCard } from "@/components/image-loading-card"
3 | import { SearchGenerationsInput } from "@/components/search-generations-input"
4 | import { DashboardShell } from "@/components/shell"
5 | import { Skeleton } from "@/components/ui/skeleton"
6 |
7 | export function SkeletonCardLoader() {
8 | return (
9 |
14 | )
15 | }
16 |
17 | export default function DashboardSettingsLoading() {
18 | return (
19 |
20 |
24 |
25 |
26 |
27 | {Array.from(Array(20), (e, i) => {
28 | return
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { GenerationForm } from "@/components/create/generation-form"
2 | import { DashboardHeader } from "@/components/header"
3 | import { DashboardShell } from "@/components/shell"
4 | import { authOptions } from "@/lib/auth"
5 | import { getCurrentUser } from "@/lib/session"
6 | import { redirect } from "next/navigation"
7 |
8 | export const metadata = {
9 | title: "Dashboard",
10 | }
11 |
12 | export default async function DashboardPage() {
13 | const user = await getCurrentUser()
14 |
15 | if (!user) {
16 | redirect(authOptions?.pages?.signIn || "/login")
17 | }
18 |
19 | return (
20 |
21 |
25 | {/* */}
26 |
27 |
28 |
29 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/components/stats.tsx:
--------------------------------------------------------------------------------
1 | interface IStats {
2 | stats: {
3 | name: string
4 | value: number
5 | unit?: string
6 | }[]
7 | }
8 |
9 | export function Stats({ stats }: IStats) {
10 | return (
11 |
12 |
13 |
14 | {stats.map((stat) => (
15 |
19 |
20 | {stat.name}
21 |
22 |
23 |
24 | {stat.value}
25 |
26 |
27 |
28 | ))}
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/motion-card-hover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { motion, useMotionTemplate, useMotionValue } from "framer-motion"
4 | import { MouseEvent, PropsWithChildren } from "react"
5 |
6 | export function MotionCardHover({ children }: PropsWithChildren) {
7 | let mouseX = useMotionValue(0)
8 | let mouseY = useMotionValue(0)
9 |
10 | function handleMouseMove({ currentTarget, clientX, clientY }: MouseEvent) {
11 | let { left, top } = currentTarget.getBoundingClientRect()
12 |
13 | mouseX.set(clientX - left)
14 | mouseY.set(clientY - top)
15 | }
16 |
17 | return (
18 |
22 |
34 |
{children}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db"
2 | import { scenarioModelData } from "@/lib/generators"
3 | import { MetadataRoute } from "next"
4 |
5 | export default async function sitemap(): Promise {
6 | const images = await db.outputImage.findMany({
7 | select: {
8 | id: true,
9 | updatedAt: true,
10 | },
11 | })
12 |
13 | const examplesSitemapEntry = Object.keys(scenarioModelData).map((style) => {
14 | return {
15 | url: `https://pixelfy.ai/examples/${scenarioModelData[style].slug}`,
16 | lastModified: new Date(),
17 | }
18 | })
19 |
20 | const imageSitemapEntry = images.map((image) => {
21 | return {
22 | url: `https://pixelfy.ai/i/${image.id}`,
23 | lastModified: image.updatedAt,
24 | }
25 | })
26 |
27 | return [
28 | {
29 | url: "https://pixelfy.ai",
30 | lastModified: new Date(),
31 | },
32 | {
33 | url: "https://pixelfy.ai/tos",
34 | lastModified: new Date(),
35 | },
36 | {
37 | url: "https://pixelfy.ai/privacy-policy",
38 | lastModified: new Date(),
39 | },
40 | ...examplesSitemapEntry,
41 | ...imageSitemapEntry,
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/components/copy-link-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "./ui/button"
4 | import { Icons } from "@/components/icons"
5 | import { toast } from "@/components/ui/use-toast"
6 | import va from "@vercel/analytics"
7 | import * as React from "react"
8 | import { ComponentProps } from "react"
9 | import { MouseEvent } from "react"
10 |
11 | type ButtonProps = ComponentProps
12 |
13 | export type TCopyLinkButton = ButtonProps & {
14 | url: string
15 | }
16 |
17 | export const CopyLinkButton = ({ url, ...props }: TCopyLinkButton) => {
18 | const [linkCopied, setLinkCopied] = React.useState(false)
19 |
20 | const onClick = () => {
21 | va.track("copy-link-button-clicked", {
22 | url,
23 | })
24 | navigator.clipboard.writeText(url)
25 | toast({
26 | title: "Link copied!",
27 | description: "You can now paste it anywhere you want.",
28 | })
29 | setLinkCopied(true)
30 | }
31 |
32 | return (
33 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/app/(examples)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { LoginButton } from "@/components/login-button"
2 | import { MainNav } from "@/components/main-nav"
3 | import { DocsSidebarNav } from "@/components/sidebar-nav"
4 | import { SiteFooter } from "@/components/site-footer"
5 | import { docsConfig } from "@/config/docs"
6 |
7 | interface DocsLayoutProps {
8 | children: React.ReactNode
9 | }
10 |
11 | export default async function DocsLayout({ children }: DocsLayoutProps) {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
{children}
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/components/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { usePathname } from "next/navigation"
5 |
6 | import { SidebarNavItem } from "types"
7 | import { cn } from "@/lib/utils"
8 | import { Icons } from "@/components/icons"
9 |
10 | interface DashboardNavProps {
11 | items: SidebarNavItem[]
12 | }
13 |
14 | export function DashboardNav({ items }: DashboardNavProps) {
15 | const path = usePathname()
16 |
17 | if (!items?.length) {
18 | return null
19 | }
20 |
21 | return (
22 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/emails/components/ButtonPrimary.tsx:
--------------------------------------------------------------------------------
1 | import { black, grayLight, white } from "./theme"
2 | import { borderBase, leadingTight, textSm } from "./theme"
3 | import { MjmlButton } from "mjml-react"
4 |
5 | export default function ButtonPrimary({
6 | link,
7 | uiText,
8 | }: {
9 | link: string
10 | uiText: string
11 | }): JSX.Element {
12 | return (
13 | <>
14 |
26 | {uiText}
27 |
28 |
40 | {uiText}
41 |
42 | >
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client"
2 | import type { Icon } from "lucide-react"
3 |
4 | import { Icons } from "@/components/icons"
5 |
6 | export type NavItem = {
7 | title: string
8 | href: string
9 | disabled?: boolean
10 | }
11 |
12 | export type MainNavItem = NavItem
13 |
14 | export type SidebarNavItem = {
15 | title: string
16 | disabled?: boolean
17 | external?: boolean
18 | icon?: keyof typeof Icons
19 | } & (
20 | | {
21 | href: string
22 | items?: never
23 | }
24 | | {
25 | href?: string
26 | items: NavLink[]
27 | }
28 | )
29 |
30 | export type SiteConfig = {
31 | name: string
32 | description: string
33 | url: string
34 | ogImage: string
35 | links: {
36 | twitter: string
37 | github: string
38 | }
39 | }
40 |
41 | export type DocsConfig = {
42 | mainNav: MainNavItem[]
43 | sidebarNav: SidebarNavItem[]
44 | }
45 |
46 | export type MarketingConfig = {
47 | mainNav: MainNavItem[]
48 | }
49 |
50 | export type DashboardConfig = {
51 | mainNav: MainNavItem[]
52 | sidebarNav: SidebarNavItem[]
53 | }
54 |
55 | export type SubscriptionPlan = {
56 | name: string
57 | description: string
58 | stripePriceId: string
59 | }
60 |
61 | export type UserSubscriptionPlan = SubscriptionPlan &
62 | Pick & {
63 | stripeCurrentPeriodEnd: number
64 | isPro: boolean
65 | }
66 |
--------------------------------------------------------------------------------
/app/api/users/[userId]/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 | import { z } from "zod"
3 |
4 | import { authOptions } from "@/lib/auth"
5 | import { db } from "@/lib/db"
6 | import { userNameSchema } from "@/lib/validations/user"
7 |
8 | const routeContextSchema = z.object({
9 | params: z.object({
10 | userId: z.string(),
11 | }),
12 | })
13 |
14 | export async function PATCH(
15 | req: Request,
16 | context: z.infer
17 | ) {
18 | try {
19 | // Validate the route context.
20 | const { params } = routeContextSchema.parse(context)
21 |
22 | // Ensure user is authentication and has access to this user.
23 | const session = await getServerSession(authOptions)
24 | if (!session?.user || params.userId !== session?.user.id) {
25 | return new Response(null, { status: 403 })
26 | }
27 |
28 | // Get the request body and validate it.
29 | const body = await req.json()
30 | const payload = userNameSchema.parse(body)
31 |
32 | // Update the user.
33 | await db.user.update({
34 | where: {
35 | id: session.user.id,
36 | },
37 | data: {
38 | name: payload.name,
39 | },
40 | })
41 |
42 | return new Response(null, { status: 200 })
43 | } catch (error) {
44 | if (error instanceof z.ZodError) {
45 | return new Response(JSON.stringify(error.issues), { status: 422 })
46 | }
47 |
48 | return new Response(null, { status: 500 })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/components/stripe-pricing-table.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import Head from "next/head"
5 | import Script from "next/script"
6 |
7 | type TStripePricingTable = {
8 | clientReferenceId: string
9 | }
10 | export const StripePricingTable = ({
11 | clientReferenceId,
12 | }: TStripePricingTable) => {
13 | const { theme } = useTheme()
14 |
15 | return (
16 | <>
17 |
21 |
22 | {theme === "light" ? (
23 | <>
24 | {/* @ts-ignore */}
25 |
30 | >
31 | ) : (
32 | <>
33 | {/* @ts-ignore */}
34 |
39 | >
40 | )}
41 | >
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useTheme } from "next-themes"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu"
13 | import { Icons } from "@/components/icons"
14 |
15 | export function ModeToggle() {
16 | const { setTheme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
26 |
27 |
28 | setTheme("light")}>
29 |
30 | Light
31 |
32 | setTheme("dark")}>
33 |
34 | Dark
35 |
36 | setTheme("system")}>
37 |
38 | System
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "bg-transparent border border-input hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Carbon } from "@/components/carbon"
2 | import { MainNav } from "@/components/main-nav"
3 | import { DashboardNav } from "@/components/nav"
4 | import { SiteFooter } from "@/components/site-footer"
5 | import { UserAccountNav } from "@/components/user-account-nav"
6 | import { dashboardConfig } from "@/config/dashboard"
7 |
8 | interface DashboardLayoutProps {
9 | children?: React.ReactNode
10 | }
11 |
12 | export default async function DashboardLayout({
13 | children,
14 | }: DashboardLayoutProps) {
15 | return (
16 |
17 |
23 |
24 |
32 |
33 | {children}
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Link from "next/link"
3 |
4 | import { MainNavItem } from "types"
5 | import { siteConfig } from "@/config/site"
6 | import { cn } from "@/lib/utils"
7 | import { useLockBody } from "@/hooks/use-lock-body"
8 | import { Icons } from "@/components/icons"
9 |
10 | interface MobileNavProps {
11 | items: MainNavItem[]
12 | children?: React.ReactNode
13 | }
14 |
15 | export function MobileNav({ items, children }: MobileNavProps) {
16 | useLockBody()
17 |
18 | return (
19 |
24 |
25 |
26 |
27 | {siteConfig.name}
28 |
29 |
43 | {children}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/components/search-generations-input.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Input } from "@/components/ui/input"
4 | import { searchString } from "@/lib/utils"
5 | import { searchGenerationsSchema } from "@/lib/validations/search-generations"
6 | import { zodResolver } from "@hookform/resolvers/zod"
7 | import va from "@vercel/analytics"
8 | import { useRouter } from "next/navigation"
9 | import { useSearchParams } from "next/navigation"
10 | import * as React from "react"
11 | import { useForm } from "react-hook-form"
12 | import * as z from "zod"
13 |
14 | type FormData = z.infer
15 |
16 | export const SearchGenerationsInput = () => {
17 | const router = useRouter()
18 |
19 | const searchParams = useSearchParams()
20 | const search = decodeURIComponent(searchParams?.get("search") ?? "")
21 |
22 | const {
23 | handleSubmit,
24 | register,
25 | formState: { errors },
26 | } = useForm({
27 | resolver: zodResolver(searchGenerationsSchema),
28 | defaultValues: {
29 | input: search ?? "",
30 | },
31 | })
32 |
33 | async function onSubmit(data: FormData) {
34 | if (data.input) {
35 | va.track("generationsSearched", {
36 | input: data.input,
37 | })
38 | }
39 |
40 | router.push(`/dashboard/generations?${searchString("1", data.input)}`)
41 | router.refresh()
42 | }
43 |
44 | return (
45 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/app/(legal)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { MainNav } from "@/components/main-nav"
2 | import { SiteFooter } from "@/components/site-footer"
3 | import { buttonVariants } from "@/components/ui/button"
4 | import { marketingConfig } from "@/config/marketing"
5 | import { cn } from "@/lib/utils"
6 | import Link from "next/link"
7 |
8 | interface MarketingLayoutProps {
9 | children: React.ReactNode
10 | }
11 |
12 | export default async function MarketingLayout({
13 | children,
14 | }: MarketingLayoutProps) {
15 | return (
16 | <>
17 |
18 |
37 |
38 | {children}
39 |
40 |
41 |
42 | >
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next"
2 | import Link from "next/link"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 | import { Icons } from "@/components/icons"
7 | import { UserAuthForm } from "@/components/user-auth-form"
8 |
9 | export const metadata: Metadata = {
10 | title: "Login",
11 | description: "Login to your account",
12 | }
13 |
14 | export default function LoginPage() {
15 | return (
16 |
17 |
24 | <>
25 |
26 | Back
27 | >
28 |
29 |
30 |
31 |
32 |
33 | Welcome back
34 |
35 |
36 | Enter your email to sign in to your account
37 |
38 |
39 |
40 |
41 |
45 | Don't have an account? Sign Up
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/components/background.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | width: 100vw;
3 | min-height: 100vh;
4 | position: fixed;
5 | display: flex;
6 | justify-content: center;
7 | padding: 120px 24px 160px 24px;
8 | pointer-events: none;
9 | }
10 |
11 | .main:before {
12 | background: radial-gradient(circle, rgba(255, 255, 255, 0) 0, #ffffff 100%);
13 | position: absolute;
14 | content: "";
15 | z-index: 2;
16 | width: 100%;
17 | height: 100%;
18 | top: 0;
19 | }
20 |
21 | .main:after {
22 | content: "";
23 | /* background-image: url("/_static/grid.svg"); */
24 |
25 | z-index: 1;
26 | position: absolute;
27 | width: 100%;
28 | height: 100%;
29 | top: 0;
30 | inset: 0;
31 | opacity: 0.3;
32 | filter: invert(0.5);
33 | }
34 |
35 | .content {
36 | height: fit-content;
37 | z-index: 3;
38 | width: 100%;
39 | max-width: 640px;
40 | background-image: radial-gradient(
41 | at 27% 37%,
42 | hsla(215, 98%, 61%, 1) 0px,
43 | transparent 0%
44 | ),
45 | radial-gradient(at 97% 21%, #d80000 0px, transparent 50%),
46 | radial-gradient(at 52% 99%, #bb2424 0px, transparent 50%),
47 | radial-gradient(at 10% 29%, #d80039 0px, transparent 50%),
48 | radial-gradient(at 97% 96%, rgb(228, 149, 149) 0px, transparent 50%),
49 | radial-gradient(at 33% 50%, #ec4848 0px, transparent 50%),
50 | radial-gradient(at 79% 53%, #e90e0e 0px, transparent 50%);
51 | position: absolute;
52 | content: "";
53 | width: 100%;
54 | height: 100%;
55 | filter: blur(100px) saturate(150%);
56 | top: 80px;
57 | opacity: 0.15;
58 | }
59 |
60 | @media screen and (max-width: 640px) {
61 | .content {
62 | display: none;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive:
13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline:
15 | "border border-input hover:bg-accent hover:text-accent-foreground",
16 | secondary:
17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18 | ghost: "hover:bg-accent hover:text-accent-foreground",
19 | link: "underline-offset-4 hover:underline text-primary",
20 | },
21 | size: {
22 | default: "h-10 py-2 px-4",
23 | sm: "h-9 px-3 rounded-md",
24 | lg: "h-11 px-8 rounded-md",
25 | },
26 | },
27 | defaultVariants: {
28 | variant: "default",
29 | size: "default",
30 | },
31 | }
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {}
37 |
38 | const Button = React.forwardRef(
39 | ({ className, variant, size, ...props }, ref) => {
40 | return (
41 |
46 | )
47 | }
48 | )
49 | Button.displayName = "Button"
50 |
51 | export { Button, buttonVariants }
52 |
--------------------------------------------------------------------------------
/lib/toc.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // TODO: Fix this when we turn strict mode on.
3 |
4 | import { toc } from "mdast-util-toc"
5 | import { remark } from "remark"
6 | import { visit } from "unist-util-visit"
7 |
8 | const textTypes = ["text", "emphasis", "strong", "inlineCode"]
9 |
10 | function flattenNode(node) {
11 | const p = []
12 | visit(node, (node) => {
13 | if (!textTypes.includes(node.type)) return
14 | p.push(node.value)
15 | })
16 | return p.join(``)
17 | }
18 |
19 | interface Item {
20 | title: string
21 | url: string
22 | items?: Item[]
23 | }
24 |
25 | interface Items {
26 | items?: Item[]
27 | }
28 |
29 | function getItems(node, current): Items {
30 | if (!node) {
31 | return {}
32 | }
33 |
34 | if (node.type === "paragraph") {
35 | visit(node, (item) => {
36 | if (item.type === "link") {
37 | current.url = item.url
38 | current.title = flattenNode(node)
39 | }
40 |
41 | if (item.type === "text") {
42 | current.title = flattenNode(node)
43 | }
44 | })
45 |
46 | return current
47 | }
48 |
49 | if (node.type === "list") {
50 | current.items = node.children.map((i) => getItems(i, {}))
51 |
52 | return current
53 | } else if (node.type === "listItem") {
54 | const heading = getItems(node.children[0], {})
55 |
56 | if (node.children.length > 1) {
57 | getItems(node.children[1], heading)
58 | }
59 |
60 | return heading
61 | }
62 |
63 | return {}
64 | }
65 |
66 | const getToc = () => (node, file) => {
67 | const table = toc(node)
68 | file.data = getItems(table.map, {})
69 | }
70 |
71 | export type TableOfContents = Items
72 |
73 | export async function getTableOfContents(
74 | content: string
75 | ): Promise {
76 | const result = await remark().use(getToc).process(content)
77 |
78 | return result.data
79 | }
80 |
--------------------------------------------------------------------------------
/components/remove-background-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Icons } from "@/components/icons"
4 | import { Button } from "@/components/ui/button"
5 | import { toast } from "@/components/ui/use-toast"
6 | import { downloadImage } from "@/lib/client-helpers"
7 | import { ImageMinus } from "lucide-react"
8 |
9 | interface IDownloadImageButton {
10 | imageId: string
11 | src: string
12 | name: string
13 | }
14 | export const RemoveBackgroundButton = ({
15 | src,
16 | name,
17 | imageId,
18 | }: IDownloadImageButton) => {
19 | const removeImageBackground = async (e: any) => {
20 | e.preventDefault()
21 |
22 | const response = await fetch("/api/images/remove-background", {
23 | method: "PUT",
24 | headers: {
25 | "Content-Type": "application/json",
26 | },
27 | body: JSON.stringify({
28 | imageId,
29 | }),
30 | })
31 |
32 | if (!response.ok) {
33 | if (response.status === 429) {
34 | return toast({
35 | title: "Too many requests",
36 | description:
37 | "You have gone over your limit for requests to generate prompts. Try again in a second.",
38 | variant: "destructive",
39 | })
40 | } else {
41 | throw new Error(response.statusText)
42 | }
43 | }
44 | }
45 | return (
46 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/_route.tsx:
--------------------------------------------------------------------------------
1 | import LoginLink from "@/emails/LoginLink"
2 | import WelcomeEmail from "@/emails/WelcomeEmail"
3 | import { sendMail, sendMarketingMail } from "@/emails/index"
4 | import { authOptions } from "@/lib/auth"
5 | import NextAuth from "next-auth"
6 | import EmailProvider from "next-auth/providers/email"
7 | import GoogleProvider from "next-auth/providers/google"
8 |
9 | const googleClientId = process.env.GOOGLE_CLIENT_ID as string
10 | const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET as string
11 |
12 | let authOptionsWithEvents = {
13 | ...authOptions,
14 | events: {
15 | async signIn(message) {
16 | if (message.isNewUser) {
17 | const email = message.user.email
18 | const name = message.user.name
19 |
20 | if (email) {
21 | await Promise.all([
22 | sendMarketingMail({
23 | subject: "🎨 Welcome to Pixelfy",
24 | to: email,
25 | component: (
26 |
27 | ),
28 | }),
29 | ])
30 | }
31 | }
32 | },
33 | },
34 | }
35 |
36 | authOptionsWithEvents.providers = [
37 | GoogleProvider({
38 | clientId: googleClientId,
39 | clientSecret: googleClientSecret,
40 | }),
41 | EmailProvider({
42 | sendVerificationRequest: async ({ identifier, url, provider }) => {
43 | await sendMail({
44 | subject: "Your Pixelfy.ai Login Link",
45 | to: identifier,
46 | component: ,
47 | })
48 | },
49 | }),
50 | ]
51 |
52 | const handler = NextAuth(authOptionsWithEvents)
53 |
54 | export { handler as GET, handler as POST }
55 |
--------------------------------------------------------------------------------
/components/sidebar-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { usePathname } from "next/navigation"
5 |
6 | import { SidebarNavItem } from "types"
7 | import { cn } from "@/lib/utils"
8 |
9 | export interface DocsSidebarNavProps {
10 | items: SidebarNavItem[]
11 | }
12 |
13 | export function DocsSidebarNav({ items }: DocsSidebarNavProps) {
14 | const pathname = usePathname()
15 |
16 | return items.length ? (
17 |
18 | {items.map((item, index) => (
19 |
20 |
21 | {item.title}
22 |
23 | {item.items ? (
24 |
25 | ) : null}
26 |
27 | ))}
28 |
29 | ) : null
30 | }
31 |
32 | interface DocsSidebarNavItemsProps {
33 | items: SidebarNavItem[]
34 | pathname: string | null
35 | }
36 |
37 | export function DocsSidebarNavItems({
38 | items,
39 | pathname,
40 | }: DocsSidebarNavItemsProps) {
41 | return items?.length ? (
42 |
43 | {items.map((item, index) =>
44 | !item.disabled && item.href ? (
45 |
57 | {item.title}
58 |
59 | ) : (
60 |
61 | {item.title}
62 |
63 | )
64 | )}
65 |
66 | ) : null
67 | }
68 |
--------------------------------------------------------------------------------
/app/api/users/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 | import { z } from "zod"
3 |
4 | import { proPlan } from "@/config/subscriptions"
5 | import { authOptions } from "@/lib/auth"
6 | import { stripe } from "@/lib/stripe"
7 | import { getUserSubscriptionPlan } from "@/lib/subscription"
8 | import { absoluteUrl } from "@/lib/utils"
9 |
10 | const billingUrl = absoluteUrl("/dashboard/billing")
11 |
12 | export async function GET(req: Request) {
13 | try {
14 | const session = await getServerSession(authOptions)
15 |
16 | if (!session?.user || !session?.user.email) {
17 | return new Response(null, { status: 403 })
18 | }
19 |
20 | const subscriptionPlan = await getUserSubscriptionPlan(session.user.id)
21 |
22 | // The user is on the pro plan.
23 | // Create a portal session to manage subscription.
24 | if (subscriptionPlan.isPro && subscriptionPlan.stripeCustomerId) {
25 | const stripeSession = await stripe.billingPortal.sessions.create({
26 | customer: subscriptionPlan.stripeCustomerId,
27 | return_url: billingUrl,
28 | })
29 |
30 | return new Response(JSON.stringify({ url: stripeSession.url }))
31 | }
32 |
33 | // The user is on the free plan.
34 | // Create a checkout session to upgrade.
35 | const stripeSession = await stripe.checkout.sessions.create({
36 | success_url: billingUrl,
37 | cancel_url: billingUrl,
38 | payment_method_types: ["card"],
39 | mode: "subscription",
40 | billing_address_collection: "auto",
41 | customer_email: session.user.email,
42 | line_items: [
43 | {
44 | price: proPlan.stripePriceId,
45 | quantity: 1,
46 | },
47 | ],
48 | metadata: {
49 | userId: session.user.id,
50 | },
51 | })
52 |
53 | return new Response(JSON.stringify({ url: stripeSession.url }))
54 | } catch (error) {
55 | if (error instanceof z.ZodError) {
56 | return new Response(JSON.stringify(error.issues), { status: 422 })
57 | }
58 |
59 | return new Response(null, { status: 500 })
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { getToken } from "next-auth/jwt"
2 | import { withAuth } from "next-auth/middleware"
3 | import { NextRequest, NextResponse } from "next/server"
4 |
5 | export default withAuth(
6 | async function middleware(req: NextRequest) {
7 | const token = await getToken({ req })
8 | const isAuth = !!token
9 | const isAuthPage =
10 | req.nextUrl.pathname.startsWith("/login") ||
11 | req.nextUrl.pathname.startsWith("/register")
12 |
13 | const response = NextResponse.next()
14 |
15 | const isReferralCodeInSearchParams = req.nextUrl.searchParams.has("ref")
16 |
17 | if (isAuthPage) {
18 | if (isAuth) {
19 | return NextResponse.redirect(new URL("/dashboard", req.url))
20 | }
21 |
22 | if (
23 | isReferralCodeInSearchParams &&
24 | !!req.nextUrl.searchParams.get("ref")
25 | ) {
26 | response.cookies.set({
27 | name: "referralCode",
28 | value: req.nextUrl.searchParams.get("ref") as string,
29 | path: "/",
30 | })
31 |
32 | return response
33 | }
34 |
35 | return null
36 | }
37 |
38 | if (!isAuth) {
39 | let from = req.nextUrl.pathname
40 | if (req.nextUrl.search) {
41 | from += req.nextUrl.search
42 | }
43 |
44 | return NextResponse.redirect(
45 | new URL(`/login?from=${encodeURIComponent(from)}`, req.url)
46 | )
47 | }
48 | },
49 | {
50 | callbacks: {
51 | async authorized() {
52 | // This is a work-around for handling redirect on auth pages.
53 | // We return true here so that the middleware function above
54 | // is always called.
55 | return true
56 | },
57 | },
58 | }
59 | )
60 |
61 | export const config = {
62 | matcher: ["/dashboard/:path*", "/editor/:path*", "/login", "/register"],
63 | }
64 |
--------------------------------------------------------------------------------
/lib/generations.ts:
--------------------------------------------------------------------------------
1 | import { db } from "./db"
2 | import { getCurrentUser } from "@/lib/session"
3 | import { cache } from "react"
4 | import "server-only"
5 |
6 | const PAGE_SIZE = 20
7 |
8 | type TGenerationCount = {
9 | search?: string
10 | }
11 |
12 | export const preloadGenerationCount = ({ search }: TGenerationCount) => {
13 | void getGenerationCount({ search })
14 | }
15 |
16 | export const getGenerationCount = cache(
17 | async ({ search }: TGenerationCount) => {
18 | const user = await getCurrentUser()
19 |
20 | if (!user) throw new Error("No user found")
21 |
22 | return db.outputImage.count({
23 | where: {
24 | generation: {
25 | status: "COMPLETE",
26 | user: {
27 | id: user.id,
28 | },
29 | prompt: {
30 | search: search,
31 | },
32 | },
33 | },
34 | })
35 | }
36 | )
37 |
38 | type TGenerationsForUser = {
39 | page: number
40 | search?: string
41 | }
42 |
43 | export const preloadGenerations = ({ page, search }: TGenerationsForUser) => {
44 | void getUserGenerations({ page, search })
45 | }
46 |
47 | export const getUserGenerations = cache(
48 | async ({ page, search }: TGenerationsForUser) => {
49 | const user = await getCurrentUser()
50 |
51 | if (!user) throw new Error("No user found")
52 |
53 | return db.outputImage.findMany({
54 | where: {
55 | generation: {
56 | status: "COMPLETE",
57 | user: {
58 | id: user.id,
59 | },
60 | prompt: {
61 | search: search,
62 | },
63 | },
64 | },
65 | take: PAGE_SIZE,
66 | skip: (page - 1) * PAGE_SIZE,
67 | include: {
68 | generation: true,
69 | },
70 | orderBy: {
71 | createdAt: "desc",
72 | },
73 | })
74 | }
75 | )
76 |
--------------------------------------------------------------------------------
/app/api/generate/prompt-generate/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth"
2 | import { LOCALHOST_IP } from "@/lib/constants"
3 | import { db } from "@/lib/db"
4 | import { OpenAIStream, OpenAIStreamPayload } from "@/lib/open-ai-stream"
5 | import { ratelimit } from "@/lib/upstash"
6 | import { ipAddress } from "@vercel/edge"
7 | import { getServerSession } from "next-auth/next"
8 |
9 | if (!process.env.OPENAI_API_KEY) {
10 | throw new Error("Missing env var from OpenAI")
11 | }
12 |
13 | export async function POST(req: Request) {
14 | const session = await getServerSession(authOptions)
15 | if (!session?.user) {
16 | return new Response(null, { status: 403 })
17 | }
18 |
19 | const ip = ipAddress(req) || LOCALHOST_IP
20 | const { success } = await ratelimit().limit(ip)
21 |
22 | if (!success) {
23 | return new Response("Don't DDoS me pls 🥺", { status: 429 })
24 | }
25 |
26 | const user = await db.user.findUniqueOrThrow({
27 | where: {
28 | id: session.user.id,
29 | },
30 | select: {
31 | credits: true,
32 | },
33 | })
34 |
35 | if (user.credits === 0) {
36 | return new Response(
37 | JSON.stringify({
38 | message:
39 | "You are out of credits. The prompt builder is only available to users who have more than 0 credits. It does not cost credits to use the prompt builder.",
40 | }),
41 | { status: 402 }
42 | )
43 | }
44 |
45 | const { prompt } = (await req.json()) as {
46 | prompt?: string
47 | }
48 |
49 | if (!prompt) {
50 | return new Response("No prompt in the request", { status: 400 })
51 | }
52 |
53 | const payload: OpenAIStreamPayload = {
54 | model: "gpt-3.5-turbo-16k",
55 | messages: [{ role: "user", content: prompt }],
56 | temperature: 0.7,
57 | top_p: 1,
58 | frequency_penalty: 0,
59 | presence_penalty: 0,
60 | max_tokens: 200,
61 | stream: true,
62 | n: 1,
63 | }
64 |
65 | const stream = await OpenAIStream(payload)
66 | return new Response(stream)
67 | }
68 |
--------------------------------------------------------------------------------
/components/images-selector.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | HoverCard,
5 | HoverCardContent,
6 | HoverCardTrigger,
7 | } from "@/components/ui/hover-card"
8 | import { Label } from "@/components/ui/label"
9 | import { Slider } from "@/components/ui/slider"
10 | import { SliderProps } from "@radix-ui/react-slider"
11 | import * as React from "react"
12 |
13 | interface TemperatureSelectorProps {
14 | defaultValue: SliderProps["defaultValue"]
15 | }
16 |
17 | export function ImageSelector({ defaultValue }: TemperatureSelectorProps) {
18 | const [value, setValue] = React.useState(defaultValue)
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {value}
29 |
30 |
31 |
40 |
41 |
42 |
47 | The amount of images you want to generate for this prompt.
48 | Default is 4. 1 image = 1 credit;
49 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/components/empty-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { Icons } from "@/components/icons"
5 |
6 | interface EmptyPlaceholderProps extends React.HTMLAttributes {}
7 |
8 | export function EmptyPlaceholder({
9 | className,
10 | children,
11 | ...props
12 | }: EmptyPlaceholderProps) {
13 | return (
14 |
21 |
22 | {children}
23 |
24 |
25 | )
26 | }
27 |
28 | interface EmptyPlaceholderIconProps
29 | extends Partial> {
30 | name: keyof typeof Icons
31 | }
32 |
33 | EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
34 | name,
35 | className,
36 | ...props
37 | }: EmptyPlaceholderIconProps) {
38 | const Icon = Icons[name]
39 |
40 | if (!Icon) {
41 | return null
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | interface EmptyPlacholderTitleProps
52 | extends React.HTMLAttributes {}
53 |
54 | EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
55 | className,
56 | ...props
57 | }: EmptyPlacholderTitleProps) {
58 | return (
59 |
60 | )
61 | }
62 |
63 | interface EmptyPlacholderDescriptionProps
64 | extends React.HTMLAttributes {}
65 |
66 | EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
67 | className,
68 | ...props
69 | }: EmptyPlacholderDescriptionProps) {
70 | return (
71 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/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 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ))
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
61 |
--------------------------------------------------------------------------------
/components/guidance-selector.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | HoverCard,
5 | HoverCardContent,
6 | HoverCardTrigger,
7 | } from "@/components/ui/hover-card"
8 | import { Label } from "@/components/ui/label"
9 | import { Slider } from "@/components/ui/slider"
10 | import { SliderProps } from "@radix-ui/react-slider"
11 | import * as React from "react"
12 |
13 | interface TemperatureSelectorProps {
14 | defaultValue: SliderProps["defaultValue"]
15 | onValueChange: SliderProps["onValueChange"]
16 | value: SliderProps["value"]
17 | }
18 |
19 | export function GuidanceSelector({
20 | defaultValue,
21 | onValueChange,
22 | value,
23 | }: TemperatureSelectorProps) {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {value}
33 |
34 |
35 |
45 |
46 |
47 |
52 | Higher values will keep images closer to the prompt
53 |
54 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/components/image-influence-slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | HoverCard,
5 | HoverCardContent,
6 | HoverCardTrigger,
7 | } from "@/components/ui/hover-card"
8 | import { Label } from "@/components/ui/label"
9 | import { Slider } from "@/components/ui/slider"
10 | import { SliderProps } from "@radix-ui/react-slider"
11 | import * as React from "react"
12 |
13 | interface ImageInfluencerSliderProps {
14 | defaultValue: SliderProps["defaultValue"]
15 | onValueChange: SliderProps["onValueChange"]
16 | value: SliderProps["value"]
17 | }
18 |
19 | export function ImageInfluencerSlider({
20 | defaultValue,
21 | onValueChange,
22 | value,
23 | }: ImageInfluencerSliderProps) {
24 | // const [value, setValue] = React.useState(defaultValue)
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {value}
35 |
36 |
37 |
48 |
49 |
50 |
55 | Higher values will keep images closer to the source image.
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # -----------------------------------------------------------------------------
2 | # App
3 | # -----------------------------------------------------------------------------
4 | NEXT_PUBLIC_APP_URL=
5 |
6 | # -----------------------------------------------------------------------------
7 | # Authentication (NextAuth.js)
8 | # -----------------------------------------------------------------------------
9 | NEXTAUTH_URL=
10 | NEXTAUTH_SECRET=
11 |
12 | # -----------------------------------------------------------------------------
13 | # Authentication Google OAuth
14 | # -----------------------------------------------------------------------------
15 | GOOGLE_CLIENT_ID=
16 | GOOGLE_CLIENT_SECRET=
17 |
18 | # -----------------------------------------------------------------------------
19 | # Database production (MySQL - PlanetScale)
20 | # -----------------------------------------------------------------------------
21 | DATABASE_URL=
22 |
23 | # -----------------------------------------------------------------------------
24 | # Email (Postmark)
25 | # -----------------------------------------------------------------------------
26 | SMTP_FROM=
27 | EMAIL_SERVER_HOST=
28 | EMAIL_SERVER_PORT=
29 | EMAIL_SERVER_USER=
30 | EMAIL_SERVER_PASSWORD=
31 | EMAIL_FROM=
32 |
33 | # -----------------------------------------------------------------------------
34 | # Subscriptions (Stripe)
35 | # -----------------------------------------------------------------------------
36 | STRIPE_API_KEY=
37 | STRIPE_WEBHOOK_SECRET=
38 | STRIPE_PRO_MONTHLY_PLAN_ID=
39 |
40 | # -----------------------------------------------------------------------------
41 | # Scenario API Token
42 | # -----------------------------------------------------------------------------
43 | SCENARIO_API_TOKEN=
44 | SCENARIO_SECRET=
45 |
46 | # -----------------------------------------------------------------------------
47 | # Chat GPT API Key
48 | # -----------------------------------------------------------------------------
49 | OPENAI_API_KEY=
50 |
51 | # -----------------------------------------------------------------------------
52 | # Upstash Redis
53 | # -----------------------------------------------------------------------------
54 | UPSTASH_REDIS_REST_URL=
55 | UPSTASH_REDIS_REST_TOKEN=
56 |
57 | # -----------------------------------------------------------------------------
58 | # Supabase API
59 | # -----------------------------------------------------------------------------
60 | SUPABASE_URL=
61 | SUPABASE_KEY=
--------------------------------------------------------------------------------
/components/image-amount-selector.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | HoverCard,
5 | HoverCardContent,
6 | HoverCardTrigger,
7 | } from "@/components/ui/hover-card"
8 | import { Label } from "@/components/ui/label"
9 | import { Slider } from "@/components/ui/slider"
10 | import { SliderProps } from "@radix-ui/react-slider"
11 | import * as React from "react"
12 |
13 | interface TemperatureSelectorProps {
14 | defaultValue: SliderProps["defaultValue"]
15 | onValueChange: SliderProps["onValueChange"]
16 | value: SliderProps["value"]
17 | }
18 |
19 | export function ImageAmountSelector({
20 | defaultValue,
21 | onValueChange,
22 | value,
23 | }: TemperatureSelectorProps) {
24 | // const [value, setValue] = React.useState(defaultValue)
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {value}
35 |
36 |
37 |
48 |
49 |
50 |
55 | The amount of images you want in the generation. The default
56 | is 4. 1 credit is equal to 4 image generations.
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db"
2 | import { PrismaAdapter } from "@next-auth/prisma-adapter"
3 | import { NextAuthOptions } from "next-auth"
4 | import EmailProvider from "next-auth/providers/email"
5 | import GoogleProvider from "next-auth/providers/google"
6 |
7 | const googleClientId = process.env.GOOGLE_CLIENT_ID as string
8 | const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET as string
9 | export const authOptions: NextAuthOptions = {
10 | // huh any! I know.
11 | // This is a temporary fix for prisma client.
12 | // @see https://github.com/prisma/prisma/issues/16117
13 | adapter: PrismaAdapter(db as any),
14 | session: {
15 | strategy: "jwt",
16 | },
17 | pages: {
18 | signIn: "/login",
19 | },
20 | providers: [
21 | GoogleProvider({
22 | clientId: googleClientId,
23 | clientSecret: googleClientSecret,
24 | }),
25 | EmailProvider({
26 | server: {
27 | host: process.env.EMAIL_SERVER_HOST,
28 | // @ts-ignore
29 | port: process.env.EMAIL_SERVER_PORT,
30 | auth: {
31 | user: process.env.EMAIL_SERVER_USER,
32 | pass: process.env.EMAIL_SERVER_PASSWORD,
33 | },
34 | },
35 | from: process.env.EMAIL_FROM,
36 | }),
37 | ],
38 | callbacks: {
39 | async session({ token, session }) {
40 | if (token) {
41 | session.user.id = token.id
42 | session.user.name = token.name
43 | session.user.email = token.email
44 | session.user.image = token.picture
45 | session.user.credits = token.credits
46 | }
47 |
48 | return session
49 | },
50 | async jwt({ token, user }) {
51 | const dbUser = await db.user.findFirst({
52 | where: {
53 | email: token.email,
54 | },
55 | })
56 |
57 | if (!dbUser) {
58 | if (user) {
59 | token.id = user?.id
60 | }
61 | return token
62 | }
63 |
64 | return {
65 | id: dbUser.id,
66 | name: dbUser.name,
67 | email: dbUser.email,
68 | picture: dbUser.image,
69 | credits: dbUser.credits,
70 | }
71 | },
72 | },
73 | }
74 |
--------------------------------------------------------------------------------
/components/sampling-step-selector.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | HoverCard,
5 | HoverCardContent,
6 | HoverCardTrigger,
7 | } from "@/components/ui/hover-card"
8 | import { Label } from "@/components/ui/label"
9 | import { Slider } from "@/components/ui/slider"
10 | import { SliderProps } from "@radix-ui/react-slider"
11 | import * as React from "react"
12 |
13 | interface TemperatureSelectorProps {
14 | defaultValue: SliderProps["defaultValue"]
15 | onValueChange: SliderProps["onValueChange"]
16 | value: SliderProps["value"]
17 | }
18 |
19 | export function SamplingStepSelector({
20 | defaultValue,
21 | onValueChange,
22 | value,
23 | }: TemperatureSelectorProps) {
24 | // const [value, setValue] = React.useState(defaultValue)
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {value}
35 |
36 |
37 |
48 |
49 |
50 |
55 | Higher steps are good for highly detailed images and will
56 | take longer, lower steps will add fewer extra details. The
57 | default is 50.
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/components/main-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Icons } from "@/components/icons"
4 | import { MobileNav } from "@/components/mobile-nav"
5 | import { siteConfig } from "@/config/site"
6 | import { cn } from "@/lib/utils"
7 | import Link from "next/link"
8 | import { useSelectedLayoutSegment } from "next/navigation"
9 | import * as React from "react"
10 | import { MainNavItem } from "types"
11 |
12 | interface MainNavProps {
13 | items?: MainNavItem[]
14 | children?: React.ReactNode
15 | }
16 |
17 | export function MainNav({ items, children }: MainNavProps) {
18 | const segment = useSelectedLayoutSegment()
19 | const [showMobileMenu, setShowMobileMenu] = React.useState(false)
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | {siteConfig.name}
27 |
28 |
29 | {items?.length ? (
30 |
47 | ) : null}
48 |
55 | {showMobileMenu && items && (
56 | {children}
57 | )}
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from "@/components/icons"
2 | import { buttonVariants } from "@/components/ui/button"
3 | import { UserAuthForm } from "@/components/user-auth-form"
4 | import { cn } from "@/lib/utils"
5 | import Link from "next/link"
6 |
7 | export const metadata = {
8 | title: "Create an account",
9 | description: "Create an account to get started.",
10 | }
11 |
12 | export default function RegisterPage() {
13 | return (
14 |
15 |
22 | Login
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Create an account
31 |
32 |
33 | Enter your email below to create your account
34 |
35 |
36 |
37 |
38 | By clicking continue, you agree to our{" "}
39 |
43 | Terms of Service
44 | {" "}
45 | and{" "}
46 |
50 | Privacy Policy
51 |
52 | .
53 |
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
56 | IconRight: ({ ...props }) => ,
57 | }}
58 | {...props}
59 | />
60 | )
61 | }
62 | Calendar.displayName = "Calendar"
63 |
64 | export { Calendar }
65 |
--------------------------------------------------------------------------------
/emails/LoginLink.tsx:
--------------------------------------------------------------------------------
1 | import ButtonPrimary from "./components/ButtonPrimary"
2 | import Divider from "./components/Divider"
3 | import Footer from "./components/Footer"
4 | import Head from "./components/Head"
5 | import Header from "./components/Header"
6 | import { purple } from "./components/theme"
7 | import {
8 | Mjml,
9 | MjmlBody,
10 | MjmlColumn,
11 | MjmlSection,
12 | MjmlText,
13 | MjmlWrapper,
14 | } from "mjml-react"
15 |
16 | export default function LoginLink({ url }: { url: string }): JSX.Element {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Welcome to Pixelfy!
27 |
28 |
29 | Please click the magic link below to sign in to
30 | your account.
31 |
32 |
33 |
34 | If you're on a mobile device, you can also copy
35 | the link below and paste it into the browser of
36 | your choice.
37 |
38 |
39 |
46 | {url.replace(/^https?:\/\//, "")}
47 |
48 |
49 |
50 | If you did not request this email, you can
51 | safely ignore it.
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/components/generations-pagination.tsx:
--------------------------------------------------------------------------------
1 | import { DownloadImageButton } from "@/components/download-image-button"
2 | import { EmptyPlaceholder } from "@/components/empty-placeholder"
3 | import { DashboardHeader } from "@/components/header"
4 | import { SearchGenerationsInput } from "@/components/search-generations-input"
5 | import { DashboardShell } from "@/components/shell"
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | HoverCard,
9 | HoverCardContent,
10 | HoverCardTrigger,
11 | } from "@/components/ui/hover-card"
12 | import { authOptions } from "@/lib/auth"
13 | import {
14 | getGenerationCount,
15 | getUserGenerations,
16 | preloadGenerationCount,
17 | preloadGenerations,
18 | } from "@/lib/generations"
19 | import { getCurrentUser } from "@/lib/session"
20 | import Image from "next/image"
21 | import Link from "next/link"
22 | import { redirect } from "next/navigation"
23 |
24 | export const metadata = {
25 | title: "Generations",
26 | description: "View all of your past generations",
27 | }
28 |
29 | const pageSize = 20
30 |
31 | export async function GenerationPagePagination({
32 | search,
33 | page,
34 | }: {
35 | search: string | undefined
36 | page: number
37 | }) {
38 | const user = await getCurrentUser()
39 |
40 | if (!user) {
41 | redirect(authOptions?.pages?.signIn || "/login")
42 | }
43 |
44 | const countQuery = getGenerationCount({ search })
45 | const generatedImageCount = await countQuery
46 |
47 | const nextPageWithOptionalSearchQueryString = search
48 | ? `/dashboard/generations?page=${page + 1}&search=${search}`
49 | : `/dashboard/generations?page=${page + 1}`
50 |
51 | const previousPageWithOptionalSearchQueryString = search
52 | ? `/dashboard/generations?page=${page - 1}&search=${search}`
53 | : `/dashboard/generations?page=${page - 1}`
54 |
55 | return (
56 | <>
57 | {generatedImageCount > 20 && (
58 |
59 | {page > 1 && (
60 |
61 |
62 |
63 | )}
64 |
65 | {page * pageSize < generatedImageCount && (
66 |
67 |
74 |
75 | )}
76 |
77 | )}
78 | >
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/lib/open-ai-stream.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createParser,
3 | ParsedEvent,
4 | ReconnectInterval,
5 | } from "eventsource-parser"
6 |
7 | export type ChatGPTAgent = "user" | "system"
8 |
9 | export interface ChatGPTMessage {
10 | role: ChatGPTAgent
11 | content: string
12 | }
13 |
14 | export interface OpenAIStreamPayload {
15 | model: string
16 | messages: ChatGPTMessage[]
17 | temperature: number
18 | top_p: number
19 | frequency_penalty: number
20 | presence_penalty: number
21 | max_tokens: number
22 | stream: boolean
23 | n: number
24 | }
25 |
26 | export async function OpenAIStream(payload: OpenAIStreamPayload) {
27 | const encoder = new TextEncoder()
28 | const decoder = new TextDecoder()
29 |
30 | let counter = 0
31 |
32 | const res = await fetch("https://api.openai.com/v1/chat/completions", {
33 | headers: {
34 | "Content-Type": "application/json",
35 | Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}`,
36 | },
37 | method: "POST",
38 | body: JSON.stringify(payload),
39 | })
40 |
41 | const stream = new ReadableStream({
42 | async start(controller) {
43 | // callback
44 | function onParse(event: ParsedEvent | ReconnectInterval) {
45 | if (event.type === "event") {
46 | const data = event.data
47 | // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
48 | if (data === "[DONE]") {
49 | controller.close()
50 | return
51 | }
52 | try {
53 | const json = JSON.parse(data)
54 | const text = json.choices[0].delta?.content || ""
55 | if (counter < 2 && (text.match(/\n/) || []).length) {
56 | // this is a prefix character (i.e., "\n\n"), do nothing
57 | return
58 | }
59 | const queue = encoder.encode(text)
60 | controller.enqueue(queue)
61 | counter++
62 | } catch (e) {
63 | // maybe parse error
64 | controller.error(e)
65 | }
66 | }
67 | }
68 |
69 | // stream response (SSE) from OpenAI may be fragmented into multiple chunks
70 | // this ensures we properly read chunks and invoke an event for each SSE event stream
71 | const parser = createParser(onParse)
72 | // https://web.dev/streams/#asynchronous-iteration
73 | for await (const chunk of res.body as any) {
74 | parser.feed(decoder.decode(chunk))
75 | }
76 | },
77 | })
78 |
79 | return stream
80 | }
81 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx"
2 | import Jimp from "jimp"
3 | import { twMerge } from "tailwind-merge"
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export function formatDate(input: string | number): string {
10 | const date = new Date(input)
11 | return date.toLocaleDateString("en-US", {
12 | month: "long",
13 | day: "numeric",
14 | year: "numeric",
15 | })
16 | }
17 |
18 | export function absoluteUrl(path: string) {
19 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
20 | }
21 |
22 | export type TPixelateImage = {
23 | remoteUrl: string
24 | pixelSize?: number
25 | }
26 |
27 | export async function pixelateImage({
28 | remoteUrl,
29 | pixelSize = 8,
30 | }: TPixelateImage) {
31 | const imageFromScenario = await fetch(remoteUrl, {
32 | headers: {
33 | "Content-Type": "application/json",
34 | Authorization: `Basic ${scenarioAuthToken}`,
35 | },
36 | }).then((res) => res.json())
37 |
38 | const image = await Jimp.read(imageFromScenario.asset.url).then((image) =>
39 | image.pixelate(pixelSize, 0, 0, 512, 512)
40 | )
41 |
42 | return image.getBase64Async(image.getMIME())
43 | }
44 |
45 | const scenarioToken = process.env.SCENARIO_API_TOKEN as string
46 | const scenarioSecret = process.env.SCENARIO_SECRET as string
47 |
48 | // export const scenarioAuthToken = `${btoa(`${scenarioToken}:${scenarioSecret}`)}`
49 |
50 | export const scenarioAuthToken = Buffer.from(`${scenarioToken}:${scenarioSecret}`).toString("base64");
51 |
52 | export function cleanSearchParams(urlSearchParams: URLSearchParams) {
53 | let cleanedParams = urlSearchParams
54 | let keysForDel = []
55 |
56 | urlSearchParams.forEach((value, key) => {
57 | if (value == "null" || value === "undefined" || !value) {
58 | // @ts-ignore
59 | keysForDel.push(key)
60 | }
61 | })
62 |
63 | keysForDel.forEach((key) => {
64 | cleanedParams.delete(key)
65 | })
66 |
67 | return cleanedParams
68 | }
69 |
70 | export function searchString(
71 | page: string | null | undefined,
72 | search: string | null | undefined,
73 | sort?: string | null | undefined
74 | ): string {
75 | // @ts-ignore
76 | const searchParameters = new URLSearchParams({
77 | page,
78 | search: encodeURIComponent(search ?? ""),
79 | sort,
80 | })
81 |
82 | return cleanSearchParams(searchParameters)?.toString()
83 | }
84 |
85 | export const convertBase64 = (file) =>
86 | new Promise((resolve, reject) => {
87 | const fileReader = new FileReader()
88 | fileReader.readAsDataURL(file)
89 |
90 | fileReader.onload = () => {
91 | resolve(fileReader.result)
92 | }
93 |
94 | fileReader.onerror = (error) => {
95 | reject(error)
96 | }
97 | })
98 |
--------------------------------------------------------------------------------
/components/billing-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { UserSubscriptionPlan } from "types"
6 | import { cn, formatDate } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardFooter,
13 | CardHeader,
14 | CardTitle,
15 | } from "@/components/ui/card"
16 | import { toast } from "@/components/ui/use-toast"
17 | import { Icons } from "@/components/icons"
18 |
19 | interface BillingFormProps extends React.HTMLAttributes {
20 | subscriptionPlan: UserSubscriptionPlan & {
21 | isCanceled: boolean
22 | }
23 | }
24 |
25 | export function BillingForm({
26 | subscriptionPlan,
27 | className,
28 | ...props
29 | }: BillingFormProps) {
30 | const [isLoading, setIsLoading] = React.useState(false)
31 |
32 | async function onSubmit(event) {
33 | event.preventDefault()
34 | setIsLoading(!isLoading)
35 |
36 | // Get a Stripe session URL.
37 | const response = await fetch("/api/users/stripe")
38 |
39 | if (!response?.ok) {
40 | return toast({
41 | title: "Something went wrong.",
42 | description: "Please refresh the page and try again.",
43 | variant: "destructive",
44 | })
45 | }
46 |
47 | // Redirect to the Stripe session.
48 | // This could be a checkout page for initial upgrade.
49 | // Or portal to manage existing subscription.
50 | const session = await response.json()
51 | if (session) {
52 | window.location.href = session.url
53 | }
54 | }
55 |
56 | return (
57 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/types/scenario.ts:
--------------------------------------------------------------------------------
1 | import { Generation, OutputImage } from "@prisma/client"
2 |
3 | export interface ScenarioInferenceResponse {
4 | inference: Inference
5 | job: Job
6 | }
7 |
8 | export interface Job {
9 | jobId: string
10 | jobType: string
11 | metadata: JobMetadata
12 | ownerId: string
13 | authorId: string
14 | createdAt: string
15 | updatedAt: string
16 | status: string
17 | statusHistory: StatusHistoryItem[]
18 | progress: number
19 | }
20 |
21 | export interface JobMetadata {
22 | baseModelId: string
23 | inferenceId: string
24 | input: any // You might want to type this more specifically
25 | modelId: string
26 | modelType: string
27 | priority: number
28 | assetIds: string[]
29 | }
30 |
31 | export interface StatusHistoryItem {
32 | // Add specific fields based on your needs
33 | status?: string
34 | timestamp?: string
35 | }
36 |
37 | export interface Inference {
38 | id: string
39 | userId: string
40 | ownerId: string
41 | authorId: string
42 | modelId: string
43 | createdAt: string
44 | parameters: InferenceParameters
45 | status: string
46 | images: ScenarioImage[]
47 | imagesNumber: number
48 | displayPrompt: string
49 | }
50 |
51 | export interface InferenceParameters {
52 | numSamples: number
53 | guidance: number
54 | numInferenceSteps: number
55 | width: number
56 | height: number
57 | type: string
58 | prompt: string
59 | negativePrompt?: string
60 | strength?: number
61 | modality?: string
62 | image?: string
63 | }
64 |
65 | export interface InferenceImage {
66 | id: string
67 | url: string
68 | seed: string
69 | pixelated?: string
70 | }
71 |
72 | export interface ScenarioInferenceProgressResponse {
73 | inference: ScenarioInferenceProgress
74 | outputImages: OutputImage[]
75 | }
76 |
77 | export interface ScenarioInferenceProgress {
78 | id: string
79 | userId: string
80 | ownerId: string
81 | authorId: string
82 | modelId: string
83 | createdAt: string
84 | parameters: InferenceParameters
85 | status: string
86 | images: ScenarioImage[]
87 | imagesNumber: number
88 | progress: number
89 | displayPrompt: string
90 | }
91 |
92 | export interface ScenarioImage {
93 | id: string
94 | url: string
95 | seed: string
96 | pixelated?: string
97 | }
98 |
99 | export interface ScenarioPixelateResponse {
100 | asset: Asset
101 | image: string
102 | }
103 |
104 | export interface Asset {
105 | id: string
106 | mimeType: string
107 | type: Type
108 | ownerId: string
109 | authorId: string
110 | createdAt: string
111 | updatedAt: string
112 | privacy: string
113 | tags: any[]
114 | collectionIds: any[]
115 | }
116 |
117 | export interface Type {
118 | source: string
119 | parentId: string
120 | rootParentId: string
121 | kind: string
122 | pixelGridSize: number
123 | }
124 |
--------------------------------------------------------------------------------
/prisma/migrations/20221021182747_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `accounts` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `userId` VARCHAR(191) NOT NULL,
5 | `type` VARCHAR(191) NOT NULL,
6 | `provider` VARCHAR(191) NOT NULL,
7 | `providerAccountId` VARCHAR(191) NOT NULL,
8 | `refresh_token` TEXT NULL,
9 | `access_token` TEXT NULL,
10 | `expires_at` INTEGER NULL,
11 | `token_type` VARCHAR(191) NULL,
12 | `scope` VARCHAR(191) NULL,
13 | `id_token` TEXT NULL,
14 | `session_state` VARCHAR(191) NULL,
15 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
16 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
17 |
18 | UNIQUE INDEX `accounts_provider_providerAccountId_key`(`provider`, `providerAccountId`),
19 | PRIMARY KEY (`id`)
20 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
21 |
22 | -- CreateTable
23 | CREATE TABLE `sessions` (
24 | `id` VARCHAR(191) NOT NULL,
25 | `sessionToken` VARCHAR(191) NOT NULL,
26 | `userId` VARCHAR(191) NOT NULL,
27 | `expires` DATETIME(3) NOT NULL,
28 |
29 | UNIQUE INDEX `sessions_sessionToken_key`(`sessionToken`),
30 | PRIMARY KEY (`id`)
31 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
32 |
33 | -- CreateTable
34 | CREATE TABLE `users` (
35 | `id` VARCHAR(191) NOT NULL,
36 | `name` VARCHAR(191) NULL,
37 | `email` VARCHAR(191) NULL,
38 | `emailVerified` DATETIME(3) NULL,
39 | `image` VARCHAR(191) NULL,
40 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
41 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
42 |
43 | UNIQUE INDEX `users_email_key`(`email`),
44 | PRIMARY KEY (`id`)
45 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
46 |
47 | -- CreateTable
48 | CREATE TABLE `verification_tokens` (
49 | `identifier` VARCHAR(191) NOT NULL,
50 | `token` VARCHAR(191) NOT NULL,
51 | `expires` DATETIME(3) NOT NULL,
52 |
53 | UNIQUE INDEX `verification_tokens_token_key`(`token`),
54 | UNIQUE INDEX `verification_tokens_identifier_token_key`(`identifier`, `token`)
55 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
56 |
57 | -- CreateTable
58 | CREATE TABLE `posts` (
59 | `id` VARCHAR(191) NOT NULL,
60 | `title` VARCHAR(191) NOT NULL,
61 | `content` JSON NULL,
62 | `published` BOOLEAN NOT NULL DEFAULT false,
63 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
64 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
65 | `authorId` VARCHAR(191) NOT NULL,
66 |
67 | PRIMARY KEY (`id`)
68 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
69 |
70 | -- AddForeignKey
71 | ALTER TABLE `accounts` ADD CONSTRAINT `accounts_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
72 |
73 | -- AddForeignKey
74 | ALTER TABLE `sessions` ADD CONSTRAINT `sessions_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
75 |
76 | -- AddForeignKey
77 | ALTER TABLE `posts` ADD CONSTRAINT `posts_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
78 |
--------------------------------------------------------------------------------
/components/toc.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { TableOfContents } from "@/lib/toc"
6 | import { cn } from "@/lib/utils"
7 | import { useMounted } from "@/hooks/use-mounted"
8 |
9 | interface TocProps {
10 | toc: TableOfContents
11 | }
12 |
13 | export function DashboardTableOfContents({ toc }: TocProps) {
14 | const itemIds = React.useMemo(
15 | () =>
16 | toc.items
17 | ? toc.items
18 | .flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
19 | .flat()
20 | .filter(Boolean)
21 | .map((id) => id?.split("#")[1])
22 | : [],
23 | [toc]
24 | )
25 | const activeHeading = useActiveItem(itemIds)
26 | const mounted = useMounted()
27 |
28 | if (!toc?.items) {
29 | return null
30 | }
31 |
32 | return mounted ? (
33 |
34 |
On This Page
35 |
36 |
37 | ) : null
38 | }
39 |
40 | function useActiveItem(itemIds: (string | undefined)[]) {
41 | const [activeId, setActiveId] = React.useState("")
42 |
43 | React.useEffect(() => {
44 | const observer = new IntersectionObserver(
45 | (entries) => {
46 | entries.forEach((entry) => {
47 | if (entry.isIntersecting) {
48 | setActiveId(entry.target.id)
49 | }
50 | })
51 | },
52 | { rootMargin: `0% 0% -80% 0%` }
53 | )
54 |
55 | itemIds?.forEach((id) => {
56 | if (!id) {
57 | return
58 | }
59 |
60 | const element = document.getElementById(id)
61 | if (element) {
62 | observer.observe(element)
63 | }
64 | })
65 |
66 | return () => {
67 | itemIds?.forEach((id) => {
68 | if (!id) {
69 | return
70 | }
71 |
72 | const element = document.getElementById(id)
73 | if (element) {
74 | observer.unobserve(element)
75 | }
76 | })
77 | }
78 | }, [itemIds])
79 |
80 | return activeId
81 | }
82 |
83 | interface TreeProps {
84 | tree: TableOfContents
85 | level?: number
86 | activeItem?: string | null
87 | }
88 |
89 | function Tree({ tree, level = 1, activeItem }: TreeProps) {
90 | return tree?.items?.length && level < 3 ? (
91 |
92 | {tree.items.map((item, index) => {
93 | return (
94 | -
95 |
104 | {item.title}
105 |
106 | {item.items?.length ? (
107 |
108 | ) : null}
109 |
110 | )
111 | })}
112 |
113 | ) : null
114 | }
115 |
--------------------------------------------------------------------------------
/components/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "./ui/button"
2 | import { Icons } from "@/components/icons"
3 | import { ModeToggle } from "@/components/mode-toggle"
4 | import { siteConfig } from "@/config/site"
5 | import { cn } from "@/lib/utils"
6 | import Link from "next/link"
7 | import * as React from "react"
8 |
9 | export function SiteFooter({ className }: React.HTMLAttributes) {
10 | return (
11 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/app/(credits)/credits/page.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from "@/components/icons"
2 | import { DashboardShell } from "@/components/shell"
3 | import { StripePricingTable } from "@/components/stripe-pricing-table"
4 | import { authOptions } from "@/lib/auth"
5 | import { getCurrentUser } from "@/lib/session"
6 | import { redirect } from "next/navigation"
7 | import Script from "next/script"
8 |
9 | export const metadata = {
10 | title: "Buy credits",
11 | description: "Manage your account credits and purchase more",
12 | }
13 |
14 | export default async function CreditsPage() {
15 | const user = await getCurrentUser()
16 |
17 | if (!user) {
18 | redirect(authOptions?.pages?.signIn || "/login")
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | Purchase Credits
27 |
28 |
29 | Unlock all features and get unlimited access to all of our
30 | future updates.
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | What's included
41 |
42 |
43 | -
44 | Save
45 | generations
46 |
47 | -
48 | Premium
49 | support by email
50 |
51 |
52 | -
53 | Commercial
54 | usage of photos
55 |
56 | -
57 | Premium
58 | templates
59 |
60 | -
61 | Request new
62 | features
63 |
64 | -
65 | Early
66 | access to new features
67 |
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/app/api/feedback/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth"
2 | import { LOCALHOST_IP } from "@/lib/constants"
3 | import { db } from "@/lib/db"
4 | import { ratelimit } from "@/lib/upstash"
5 | import { GENERATIONSATISFCATION } from "@prisma/client"
6 | import { ipAddress } from "@vercel/edge"
7 | import { getServerSession } from "next-auth/next"
8 | import { z } from "zod"
9 |
10 | const feedbackBody = z.object({
11 | inferenceId: z.string(),
12 | modelId: z.string(),
13 | satisfaction: z.nativeEnum(GENERATIONSATISFCATION),
14 | comment: z.string().optional().nullable(),
15 | })
16 |
17 | export async function PUT(req: Request, context) {
18 | try {
19 | const session = await getServerSession(authOptions)
20 | if (!session?.user) {
21 | return new Response(null, { status: 403 })
22 | }
23 |
24 | const ip = ipAddress(req) || LOCALHOST_IP
25 | const { success } = await ratelimit().limit(ip)
26 |
27 | if (!success) {
28 | return new Response("Don't DDoS me pls 🥺", { status: 429 })
29 | }
30 | const body = await req.json()
31 |
32 | const { satisfaction, comment, inferenceId, modelId } =
33 | feedbackBody.parse(body)
34 |
35 | const userFeedbackCreditsGranted = await db.user.findUniqueOrThrow({
36 | where: {
37 | id: session.user.id,
38 | },
39 | select: {
40 | feedbackCreditsGranted: true,
41 | },
42 | })
43 |
44 | const generation = await db.generation.findUniqueOrThrow({
45 | where: {
46 | uniqueGeneration: {
47 | inferenceId,
48 | modelId,
49 | },
50 | },
51 | include: {
52 | feedback: true,
53 | },
54 | })
55 |
56 | const upsertGenerationFeedback = await db.generationFeedback.upsert({
57 | where: {
58 | generationId: generation.id,
59 | },
60 | include: {
61 | generation: true,
62 | },
63 | update: {
64 | satisfaction,
65 | comment: comment ?? "",
66 | },
67 | create: {
68 | generation: {
69 | connect: {
70 | id: generation.id,
71 | },
72 | },
73 | satisfaction,
74 | comment: comment ?? "",
75 | },
76 | })
77 |
78 | if (userFeedbackCreditsGranted?.feedbackCreditsGranted < 3 && comment) {
79 | await db.user.update({
80 | where: {
81 | id: session.user.id,
82 | },
83 | data: {
84 | feedbackCreditsGranted: {
85 | increment: 1,
86 | },
87 | credits: {
88 | increment: 1,
89 | },
90 | },
91 | })
92 | }
93 |
94 | return new Response(JSON.stringify(upsertGenerationFeedback), {
95 | status: 201,
96 | })
97 | } catch (error) {
98 | console.log(error)
99 | if (error instanceof z.ZodError) {
100 | return new Response(JSON.stringify(error.issues), { status: 422 })
101 | }
102 |
103 | return new Response(error, { status: 500 })
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/components/user-name-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useRouter } from "next/navigation"
5 | import { zodResolver } from "@hookform/resolvers/zod"
6 | import { User } from "@prisma/client"
7 | import { useForm } from "react-hook-form"
8 | import * as z from "zod"
9 |
10 | import { cn } from "@/lib/utils"
11 | import { userNameSchema } from "@/lib/validations/user"
12 | import { buttonVariants } from "@/components/ui/button"
13 | import {
14 | Card,
15 | CardContent,
16 | CardDescription,
17 | CardFooter,
18 | CardHeader,
19 | CardTitle,
20 | } from "@/components/ui/card"
21 | import { Input } from "@/components/ui/input"
22 | import { Label } from "@/components/ui/label"
23 | import { toast } from "@/components/ui/use-toast"
24 | import { Icons } from "@/components/icons"
25 |
26 | interface UserNameFormProps extends React.HTMLAttributes {
27 | user: Pick
28 | }
29 |
30 | type FormData = z.infer
31 |
32 | export function UserNameForm({ user, className, ...props }: UserNameFormProps) {
33 | const router = useRouter()
34 | const {
35 | handleSubmit,
36 | register,
37 | formState: { errors },
38 | } = useForm({
39 | resolver: zodResolver(userNameSchema),
40 | defaultValues: {
41 | name: user?.name || "",
42 | },
43 | })
44 | const [isSaving, setIsSaving] = React.useState(false)
45 |
46 | async function onSubmit(data: FormData) {
47 | setIsSaving(true)
48 |
49 | const response = await fetch(`/api/users/${user.id}`, {
50 | method: "PATCH",
51 | headers: {
52 | "Content-Type": "application/json",
53 | },
54 | body: JSON.stringify({
55 | name: data.name,
56 | }),
57 | })
58 |
59 | setIsSaving(false)
60 |
61 | if (!response?.ok) {
62 | return toast({
63 | title: "Something went wrong.",
64 | description: "Your name was not updated. Please try again.",
65 | variant: "destructive",
66 | })
67 | }
68 |
69 | toast({
70 | description: "Your name has been updated.",
71 | })
72 |
73 | router.refresh()
74 | }
75 |
76 | return (
77 |
120 | )
121 | }
122 |
--------------------------------------------------------------------------------