Showing preview only (451K chars total). Download the full file or copy to clipboard to get everything.
Repository: Sls0n/Prismify
Branch: main
Commit: 339ac7a56cf4
Files: 155
Total size: 412.6 KB
Directory structure:
gitextract_iv9ddlq0/
├── .eslintrc.json
├── .gitignore
├── .vscode/
│ └── settings.json
├── README.md
├── app/
│ ├── (routes)/
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── articles/
│ │ │ ├── [slug]/
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ └── sitemap.ts
│ │ └── layout.tsx
│ ├── admin/
│ │ ├── layout.tsx
│ │ └── write-article/
│ │ ├── blog-form.tsx
│ │ └── page.tsx
│ ├── api/
│ │ ├── article/
│ │ │ └── post/
│ │ │ └── route.ts
│ │ ├── auth/
│ │ │ └── [...nextauth]/
│ │ │ └── route.ts
│ │ └── user/
│ │ └── settings/
│ │ └── route.ts
│ ├── error.tsx
│ ├── layout.tsx
│ ├── loading.tsx
│ ├── manifest.json
│ ├── not-found.tsx
│ ├── page.tsx
│ ├── robots.ts
│ └── sitemap.ts
├── components/
│ ├── articles/
│ │ └── article-card.tsx
│ ├── auth-modal.tsx
│ ├── clarity-script.tsx
│ ├── color-picker.tsx
│ ├── editor/
│ │ ├── background-image-canvas.tsx
│ │ ├── background-options/
│ │ │ ├── custom-gradient-picker.tsx
│ │ │ ├── image-gradient-picker.tsx
│ │ │ ├── index.tsx
│ │ │ ├── noise-slider.tsx
│ │ │ ├── normal-gradient-picker.tsx
│ │ │ └── pattern-picker.tsx
│ │ ├── browser-frames.tsx
│ │ ├── canvas-area.tsx
│ │ ├── canvas-options/
│ │ │ ├── canvas-roundness-slider.tsx
│ │ │ ├── index.tsx
│ │ │ └── resolution-button.tsx
│ │ ├── frame-options/
│ │ │ ├── additional-frame-options.tsx
│ │ │ ├── frame-picker.tsx
│ │ │ └── index.tsx
│ │ ├── image-context-menu.tsx
│ │ ├── image-options/
│ │ │ ├── add-image-button.tsx
│ │ │ ├── index.tsx
│ │ │ ├── inset-option.tsx
│ │ │ ├── roundness-option.tsx
│ │ │ ├── scale-options.tsx
│ │ │ └── shadow-settings.tsx
│ │ ├── main-image-area.tsx
│ │ ├── mobile-view-image-options.tsx
│ │ ├── moveable-component.tsx
│ │ ├── noise.tsx
│ │ ├── perspective-options/
│ │ │ ├── index.tsx
│ │ │ └── rotate-options.tsx
│ │ ├── position-options/
│ │ │ ├── index.tsx
│ │ │ ├── position-control.tsx
│ │ │ └── translate-control.tsx
│ │ ├── selecto-component.tsx
│ │ ├── sidebar-buttons.tsx
│ │ ├── sidebar.tsx
│ │ ├── text-context-menu.tsx
│ │ ├── text-layers.tsx
│ │ ├── text-options/
│ │ │ ├── add-text-layer.tsx
│ │ │ ├── font-settings.tsx
│ │ │ └── index.tsx
│ │ ├── tiptap-moveable.tsx
│ │ └── undo-redo-buttons.tsx
│ ├── export-options.tsx
│ ├── footer.tsx
│ ├── icons/
│ │ ├── index.tsx
│ │ └── info.icon.tsx
│ ├── loader.tsx
│ ├── navbar.tsx
│ ├── navlinks.tsx
│ ├── popup-color-picker.tsx
│ ├── profile-dialog.tsx
│ ├── settings-dialog.tsx
│ ├── sign-in-form.tsx
│ ├── spinner/
│ │ ├── spinner.module.css
│ │ └── spinner.tsx
│ ├── ui/
│ │ ├── accordion.tsx
│ │ ├── back-button.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── circular-slider.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── gradient-text.tsx
│ │ ├── input.tsx
│ │ ├── popover.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── spotlight-button.tsx
│ │ ├── style/
│ │ │ └── checkbox.module.css
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── text-area.tsx
│ │ ├── text.tsx
│ │ ├── theme-button.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── tooltip.tsx
│ └── user-dropdown.tsx
├── components.json
├── docker-compose.yml
├── hooks/
│ ├── canvas-area-hooks/
│ │ ├── use-automatic-aspect-ratio-switcher.ts
│ │ ├── use-resize-observer.ts
│ │ └── use-screen-size-warning-toast.ts
│ ├── use-editor.ts
│ ├── use-event-listener.ts
│ ├── use-isomorphic-layout-effect.ts
│ ├── use-media-query.ts
│ ├── use-on-click-outside.ts
│ └── use-toast.ts
├── index.d.ts
├── libs/
│ ├── prismadb.ts
│ └── validators/
│ ├── article-post-validator.ts
│ └── user-settings-validator.ts
├── license.md
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prettier.config.js
├── prisma/
│ └── schema.prisma
├── providers/
│ └── index.tsx
├── store/
│ ├── use-active-index.ts
│ ├── use-background-options.ts
│ ├── use-color-extractor.ts
│ ├── use-frame-options.ts
│ ├── use-image-options.ts
│ ├── use-image-quality.ts
│ ├── use-moveable.ts
│ ├── use-resize-canvas.ts
│ └── use-tiptap.ts
├── styles/
│ └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── utils/
│ ├── auth-options.ts
│ ├── button-utils.ts
│ ├── helper-fns.ts
│ ├── presets/
│ │ ├── gradients.ts
│ │ ├── qualities.ts
│ │ ├── resolutions.ts
│ │ ├── shadows.ts
│ │ └── solid-colors.ts
│ └── tiptap-extensions.ts
└── workers/
└── background-removal.worker.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
Checkpoint.tsx
================================================
FILE: .vscode/settings.json
================================================
{
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"editor.fontLigatures": true,
"editor.renderWhitespace": "none",
"tailwindCSS.includeLanguages": {
"html": "html",
"javascript": "javascript",
"typescript": "typescript",
"css": "css"
},
"editor.quickSuggestions": {
"strings": true
},
"typescript.tsserver.experimental.enableProjectDiagnostics": true
}
================================================
FILE: README.md
================================================
## Prismify
Prismify is a web app that aims to revitalize & enhance boring images/screenshots. With prismify, you can effortlessly enhance your images/screenshots.

## Preview


## Tech Stacks
- Typescript
- Next
- React
- Tailwind
- Prisma
- Zustand
## Perfect lighthouse score

## Setup
Run the following command to install dependencies and generate the Prisma client:
```bash
pnpm run setup
```
================================================
FILE: app/(routes)/about/page.tsx
================================================
/* eslint-disable react/no-unescaped-entities */
import React from 'react'
import BackButton from '@/components/ui/back-button'
import { Text } from '@/components/ui/text'
import type { Metadata } from 'next'
import Image from 'next/image'
import { GradientText } from '@/components/ui/gradient-text'
export const metadata: Metadata = {
title: 'About - Prismify',
description: 'Read details about Prismify.',
openGraph: {
title: 'About - Prismify',
description: 'Read details about Prismify.',
},
alternates: {
canonical: 'https://prismify.vercel.app/about',
},
}
export default function page() {
return (
<section className="container flex w-full flex-col gap-8 pt-[72px] lg:gap-16">
<div className="mt-8">
<BackButton />
</div>
<article className="mx-auto h-fit w-full max-w-prose rounded-md">
<div className="mb-16 flex w-full items-start justify-between">
<div className="mx-auto flex flex-col gap-3">
{/* Title */}
<GradientText
as="h1"
variant="purple"
className="line-clamp-2 border-b-[3px] border-[#898aeb] text-center text-[2rem] font-semibold capitalize leading-tight md:text-[2.8rem]"
>
<span className="!font-medium">About</span> — Prismify
</GradientText>
</div>
</div>
<div className="prose prose-lg prose-neutral mb-16 dark:prose-invert prose-p:tracking-[0.002em] prose-p:text-dark prose-img:rounded-lg">
<figure className="">
<Image
width={1280}
height={720}
src="https://prismify.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F5262c091-eb0d-485e-8fdc-7a16b54935b4%2F01b77d12-90a7-42a9-87b3-d43ebb33aca3%2Fprofile-4TcKvlK3c7c8cg1PP9gwBEJrYaY2-128shots_so..png?table=block&id=17227f20-3d4b-4256-a2a7-54f8949a03f7&spaceId=5262c091-eb0d-485e-8fdc-7a16b54935b4&width=2000&userId=&cache=v2"
alt="About Prismify"
className="aspect-auto max-w-full object-cover"
/>
</figure>
<p>
Hey there, fellow entrepreneurs! Ever felt like banging your head
against the wall trying to create eye-catching designs for your
product or social media? I hear you! But guess what? Prismify's here
to save the day.
</p>{' '}
<h3 className="text-dark">
Let's dive into the struggles we all face:
</h3>
<ul>
<li>
<strong>Zero Design Skills:</strong> Who has time to become a
design pro? With Prismify, you don't need to! It's like having a
magic wand for your visuals.
</li>
<li>
<strong>Clock's Ticking:</strong> Time is money, they say.
Prismify respects that – swift, efficient, and no-nonsense design
tools.
</li>
<li>
<strong>Quality Matters:</strong> We all want our stuff to look
top-notch. Prismify keeps your visuals sharp and professional
without the designer price tag.
</li>
</ul>
<p>
Now, hold on to your hats! Here's how Prismify makes design a
cakewalk:
</p>
<ul>
<li>
Browser Frame Mockups: Pop your product into a fancy browser frame
in a snap!
</li>
<li>
Custom Gradients: Gradient backgrounds that'll make your graphics
pop like fireworks! With also adaptive gradients support!
</li>
<li>
Text Tricks & Notes: Jazz up your visuals with text enhancements
and handy annotations.
</li>
<li>
Color Magic & 3D Vibes: Prismify's color game and 3D effects?
Mind-blowing.
</li>
<li>
Hi-Res & Lightning-Speed Edits: Get crisp, high-res images without
the wait. Lightning-fast editing? You got it!
</li>
</ul>
<p>Perks for You:</p>
<ul>
<li>
Time Saver: Spend less time stressing over designs, more time
making things happen.
</li>
<li>
Pro-Level Designs: No more 'amateur hour' visuals. Prismify gives
you that polished, pro look.
</li>
<li>
Versatility Rules: Social media, websites, product shots –
Prismify's got your back for any visual need.
</li>
</ul>
<p>Feeling the design itch? Scratch it with Prismify!</p>
<blockquote>
"Transform Your Visuals Today! Try Prismify – Your Visual Upgrade
Shortcut."
</blockquote>
<p>
Wrapping it Up: Prismify's not just a tool; it's your secret weapon
for killer SaaS visuals and social media. Remember to show, not just
tell – demos and examples make all the difference. Get ready to rock
those visuals!
</p>
</div>
</article>
</section>
)
}
================================================
FILE: app/(routes)/articles/[slug]/loading.tsx
================================================
import Loader from '@/components/loader'
export default function Loading() {
return <Loader />
}
================================================
FILE: app/(routes)/articles/[slug]/page.tsx
================================================
import BackButton from '@/components/ui/back-button'
import { Badge } from '@/components/ui/badge'
import { Text } from '@/components/ui/text'
import prismadb from '@/libs/prismadb'
import {
generateBadgeVariant,
generateFormattedBlogDate,
separateCommas,
} from '@/utils/helper-fns'
import { Calendar } from 'lucide-react'
import type { Metadata } from 'next'
import Image from 'next/image'
import { notFound } from 'next/navigation'
import { cache } from 'react'
type ArticleProps = {
params: Promise<{
slug: string
}>
}
export const revalidate = 3600 // 1 hour
export async function generateStaticParams() {
const blogs = await prismadb.article.findMany({
select: {
slug: true,
},
})
return blogs.map((blog) => ({
slug: blog.slug,
}))
}
const getBlog = cache(async (slug: string) => {
const blog = await prismadb.article.findFirst({
where: {
slug,
},
})
return blog
})
export async function generateMetadata(props: ArticleProps) {
const params = await props.params;
const blog = await getBlog(params.slug)
if (!blog) {
return
}
const metadata: Metadata = {
title: `${blog.title} - Prismify`,
description: blog.summary,
openGraph: {
title: `${blog.title} - Prismify`,
description: blog?.summary || 'N/A',
locale: 'en_US',
url: `https://prismify.vercel.app/articles/${blog.slug}`,
type: 'article',
images: [
{
url:
blog.imageUrl ?? 'https://prismify.vercel.app/opengraph-image.jpg',
width: 1280,
height: 720,
alt: blog.title,
},
],
},
twitter: {
creator: '@xSls0n_007',
title: `${blog.title} - Prismify`,
description: blog.summary || 'N/A',
card: 'summary_large_image',
images: [
{
url:
blog.imageUrl ?? 'https://prismify.vercel.app/opengraph-image.jpg',
width: 1280,
height: 720,
alt: blog.title,
},
],
},
alternates: {
canonical: `https://prismify.vercel.app/articles/${blog.slug}`,
},
publisher: 'Prismify',
}
return metadata
}
export default async function ArticlePage(props: ArticleProps) {
const params = await props.params;
const blog = await getBlog(params.slug)
if (!blog) {
return notFound()
}
return (
<section className="container flex w-full flex-col gap-8 pt-[72px] lg:gap-16">
<div className="mt-8">
<BackButton />
</div>
<article className="mx-auto h-fit w-full max-w-prose rounded-md">
<div className="mb-16 flex w-full items-start justify-between">
<div className="flex flex-col gap-3">
{/* Time published/updated */}
<div className="flex items-center">
<Calendar className="mr-2 h-[1.15rem] w-[1.15rem] text-white/40" />
<time
className="text-base font-normal text-dark/60"
dateTime={
blog?.updatedAt.toISOString() ?? blog?.createdAt.toISOString()
}
>
{generateFormattedBlogDate(blog?.createdAt, blog?.updatedAt)}
</time>
</div>
{/* Title */}
<Text
variant="h1"
bold
className="text-[2rem] font-bold capitalize leading-tight text-dark md:text-[2.8rem]"
>
{blog.title ?? 'N/A'}
</Text>
{/* Category */}
{blog?.category && (
<div className="flex items-center gap-2">
{separateCommas(blog?.category)!.map((category) => (
<Badge
key={category}
variant={generateBadgeVariant(category)}
>
{category}
</Badge>
))}
</div>
)}
</div>
</div>
<div className="prose prose-lg prose-neutral mb-16 dark:prose-invert prose-p:tracking-[0.002em] prose-p:text-dark prose-a:cursor-pointer prose-a:text-purple">
{blog?.imageUrl && (
<figure className="h-fit overflow-hidden rounded-lg bg-formDark">
<Image
src={blog?.imageUrl ?? '/images/fallback.jpg'}
alt={'cover image'}
width={1280}
height={720}
priority
className="aspect-auto max-w-full object-cover"
/>
{/* <figcaption>
Picture
</figcaption> */}
</figure>
)}
{blog?.content && (
<div
dangerouslySetInnerHTML={{
__html: blog.content,
}}
></div>
)}
</div>
</article>
</section>
)
}
================================================
FILE: app/(routes)/articles/page.tsx
================================================
import prismadb from '@/libs/prismadb'
import { Text } from '@/components/ui/text'
import ArticleCard from '@/components/articles/article-card'
import { formatDate, separateCommas } from '@/utils/helper-fns'
import type { Metadata } from 'next'
import { unstable_cache as cache } from 'next/cache'
export const metadata: Metadata = {
title: 'Articles - Prismify',
description: 'Read latest articles from Prismify.',
openGraph: {
title: 'Articles - Prismify',
description: 'Read latest articles from Prismify.',
type: 'article',
url: 'https://prismify.vercel.app/articles',
},
alternates: {
canonical: 'https://prismify.vercel.app/articles',
},
}
const getCachedArticles = cache(
async () => {
return await prismadb.article.findMany({
orderBy: {
createdAt: 'desc',
},
})
},
['articles'],
{
revalidate: 60 * 60, // 1 hour
tags: ['articles'],
}
)
export default async function Article() {
const articles = await getCachedArticles()
return (
<section className="w-full flex-1 pt-[72px]">
<div className="mb-16 flex h-48 w-full flex-col items-center justify-center bg-black/10">
<Text
variant="h1"
bold
className="mb-2 line-clamp-2 text-[2rem] font-bold capitalize leading-tight tracking-normal text-dark md:text-[2.8rem]"
>
Articles
</Text>
<Text
variant="bodyMedium"
className="line-clamp-2 flex items-center gap-1 text-dark/70 "
>
<span>Read latest articles from</span>
<span className="inline-flex w-fit items-center gap-1 rounded-md bg-indigo-500/10 px-2 py-1 text-xs font-medium text-purple shadow-sm ring-1 ring-inset ring-indigo-500/20 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
Prismify
</span>
</Text>
</div>
<ul
className="container mt-5 grid grid-cols-1 grid-rows-2 gap-16 sm:mt-10 sm:grid-cols-2 md:mt-24 lg:grid-cols-3"
role="list"
>
{articles.length === 0 ? (
<div className="mb-24 text-center">
<Text variant="bodyMedium" className="text-dark/70">
— The list is empty. Go back to previous page
</Text>
</div>
) : (
articles.map((article) => {
return (
<div key={article.id} className="relative col-span-1 row-span-1">
<ArticleCard
title={article.title ?? 'N/A'}
image={article.imageUrl ?? '/images/fallback.jpg'}
date={formatDate(article.updatedAt) ?? 'N/A'}
href={`/articles/${article.slug}`}
category={
separateCommas(article?.category ?? 'Design')?.at(0) ?? ''
}
/>
</div>
)
})
)}
</ul>
</section>
)
}
================================================
FILE: app/(routes)/articles/sitemap.ts
================================================
import prismadb from '@/libs/prismadb'
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const response = await prismadb.article.findMany({
select: {
slug: true,
updatedAt: true,
},
})
const sitemapUrls: MetadataRoute.Sitemap = response?.map((p) => {
return {
url: `https://prismify.vercel.app/articles/${p.slug}`,
lastModified: new Date(p.updatedAt),
changeFrequency: 'monthly',
}
})
return sitemapUrls
}
================================================
FILE: app/(routes)/layout.tsx
================================================
import Footer from '@/components/footer'
import React from 'react'
export default function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<main className="flex h-full w-full flex-col">
{children}
<Footer />
</main>
)
}
================================================
FILE: app/admin/layout.tsx
================================================
import React from 'react'
export default function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<main className="container h-full w-full">
{children}
</main>
)
}
================================================
FILE: app/admin/write-article/blog-form.tsx
================================================
'use client'
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
import {
Bold,
Code,
Heading,
ImagePlus,
Italic,
Link,
List,
ListOrdered,
Quote,
Space,
Strikethrough,
Underline,
} from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
// import { FileUpload } from '@/components/file-upload'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { cn } from '@/utils/button-utils'
import { extensions } from '@/utils/tiptap-extensions'
import { useMutation } from '@tanstack/react-query'
import { Text } from '@/components/ui/text'
import { useTiptap } from '@/store/use-tiptap'
import axios, { AxiosError } from 'axios'
import { toast } from '@/hooks/use-toast'
import { Textarea } from '@/components/ui/text-area'
export default function BlogForm() {
const [blogOutput, setBlogOutput] = useState<string>('')
const [title, setTitle] = useState<string>('')
const [summary, setSummary] = useState<string>('')
const [category, setCategory] = useState<string>('')
const [slug, setSlug] = useState<string>('')
const [mainBlogImg, setMainBlogImg] = useState<string>('')
const { mutate: publishBlog, isLoading: isPublishing } = useMutation({
mutationFn: async () => {
const res = await axios.post('/api/article/post', {
title,
summary,
category,
slug,
imageUrl: mainBlogImg,
content: blogOutput,
})
return res
},
onSuccess: () => {
toast({
title: 'Blog published successfully',
})
},
onError: (err) => {
if (err instanceof AxiosError) {
if (err.response?.status === 400) {
toast({
title: err?.response?.data?.[0]?.message,
variant: 'destructive',
})
}
if (err.response?.status === 401) {
toast({
title: 'You are not authorized to publish this blog',
description: 'Please login to publish this blog',
variant: 'destructive',
})
}
if (err.response?.status === 409) {
toast({
title: 'Blog with this slug already exists',
description: 'Please modify the slug a bit.',
variant: 'destructive',
})
}
if (err.response?.status === 500) {
toast({
title: 'Something went wrong',
description: 'Please try again later',
variant: 'destructive',
})
}
}
console.log(err)
},
})
const publishBlogHandler = () => {
publishBlog()
}
const slugifyTitle = async (title: string) => {
const slugify = (await import('slugify')).default
setSlug(slugify(title, { lower: true, strict: true }))
}
return (
<>
<div className="mb-8 mt-8 space-y-6 rounded-lg border-[1.5px] border-border bg-[#151515] p-8">
<div className="space-y-2">
<Text className="text-dark/90" variant="bodyLarge" semibold>
Title
</Text>
<Input
placeholder="Write title for your blog..."
className="h-16 border-transparent bg-[#151515] p-0 placeholder:text-dark/50 focus-visible:ring-0 focus-visible:ring-transparent md:text-xl "
value={title}
onChange={(e) => {
setTitle(e.target.value)
slugifyTitle(e.target.value)
}}
required
/>
</div>
<div className="space-y-2">
<Text className="text-dark/90" variant="bodyLarge" semibold>
Summary
</Text>
<Textarea
placeholder="Summary of the blog..."
className="bg-transparent h-32 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
value={summary}
onChange={(e) => setSummary(e.target.value)}
/>
</div>
<div className="space-y-2">
<Text className="text-dark/90" variant="bodyLarge" semibold>
Category (Comma separated)
</Text>
<Input
placeholder="Eg: Design, Marketing"
className="h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
value={category}
onChange={(e) => setCategory(e.target.value)}
/>
</div>
<div className="space-y-2">
<Text className="text-dark/90" variant="bodyLarge" semibold>
Slug
</Text>
<Input
placeholder="Eg: blog-title-slug"
className="h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
value={slug}
onChange={(e) => setSlug(e.target.value)}
/>
</div>
<div className="space-y-2">
<Text className="text-dark/90" variant="bodyLarge" semibold>
Image URL
</Text>
<Input
placeholder="Eg: https://images.unsplash.com/photo/..."
className="h-16 border-transparent bg-transparent p-0 placeholder:text-dark/50 focus-visible:ring-transparent md:text-xl"
value={mainBlogImg}
onChange={(e) => setMainBlogImg(e.target.value)}
/>
</div>
</div>
<div className="mb-8 mt-4 w-full space-y-6 rounded-lg border-[1.5px] border-border bg-[#151515] p-8">
<div className="w-full ">
<Text variant="bodyXLarge" className="pb-4 text-dark/90" semibold>
Write your blog here
</Text>
<hr className="-mx-8 border-border pb-4" />
<EditorProvider
slotBefore={<MenuBar />}
extensions={extensions}
parseOptions={{
preserveWhitespace: true,
}}
editorProps={{
attributes: {
class:
'prose prose-md max-w-prose mx-auto prose-neutral mb-16 md:prose-lg prose-img:rounded-md focus:outline-none min-h-[30rem] dark:prose-invert',
},
}}
onUpdate={(content) =>
setBlogOutput(content?.editor?.getHTML())
}
/>
</div>
</div>
<Button
isLoading={isPublishing}
onClick={publishBlogHandler}
variant="default"
className="mb-24 max-w-[10rem]"
>
<Text variant="bodyMedium" semibold>
Publish blog
</Text>
</Button>
</>
)
}
const MenuBar = () => {
const { editor } = useCurrentEditor()
const addImage = useCallback(() => {
const url = window.prompt('URL')
if (url) {
editor?.chain().focus().setImage({ src: url }).run()
}
}, [editor])
const setLink = useCallback(() => {
const previousUrl = editor?.getAttributes('link').href as string
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
editor?.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}, [editor])
return (
<div className="sticky top-[4.5rem] z-50 bg-[#151515]">
<div className="z-50 flex flex-wrap gap-y-4 bg-[#151515] pb-5 pt-2 backdrop-blur-md">
<Button
variant="menuItem"
onClick={() =>
editor?.chain().focus().toggleHeading({ level: 3 }).run()
}
disabled={
!editor?.can().chain().focus().toggleHeading({ level: 3 }).run()
}
className={cn(
editor?.isActive('heading', { level: 3 })
? 'border border-indigo-400/10 bg-indigo-400/10'
: 'border border-transparent',
'mr-2 px-2.5 text-foreground/80 '
)}
>
<Heading className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleBold().run()}
disabled={!editor?.can().chain().focus().toggleBold().run()}
className={cn(
editor?.isActive('bold')
? 'border border-indigo-400/10 bg-indigo-400/10'
: 'border border-transparent',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Bold className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleItalic().run()}
disabled={!editor?.can().chain().focus().toggleItalic().run()}
className={cn(
editor?.isActive('italic')
? 'border border-indigo-400/10 bg-indigo-400/10'
: 'border border-transparent',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Italic className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleUnderline().run()}
disabled={!editor?.can().chain().focus().toggleUnderline().run()}
className={cn(
editor?.isActive('underline')
? 'border border-indigo-400/10 bg-indigo-400/10'
: 'border border-transparent',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Underline className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleStrike().run()}
disabled={!editor?.can().chain().focus().toggleStrike().run()}
className={cn(
editor?.isActive('strike')
? 'border border-indigo-400/10 bg-indigo-400/10'
: 'border border-transparent',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Strikethrough className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleCodeBlock().run()}
disabled={!editor?.can().chain().focus().toggleCodeBlock().run()}
className={cn(
editor?.isActive('codeBlock')
? 'border border-indigo-400/10 bg-indigo-400/10'
: 'border border-transparent',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Code className="h-5 w-5" />
</Button>
<div className="my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10"></div>
<Button
className="mr-2 px-2.5 text-foreground/80"
variant="menuItem"
onClick={addImage}
>
<ImagePlus className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => {
editor?.isActive('link')
? editor?.chain().focus().unsetLink().run()
: setLink()
}}
className={cn(
editor?.isActive('link')
? 'border border-indigo-400/10 bg-indigo-400/10'
: '',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Link className="h-5 w-5" />
</Button>
<div className="my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10"></div>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleBulletList().run()}
disabled={!editor?.can().chain().focus().toggleBulletList().run()}
className={cn(
editor?.isActive('bulletList')
? 'border border-indigo-400/10 bg-indigo-400/10'
: '',
'mr-2 px-2.5 text-foreground/80'
)}
>
<List className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleOrderedList().run()}
disabled={!editor?.can().chain().focus().toggleOrderedList().run()}
className={cn(
editor?.isActive('orderedList')
? 'border border-indigo-400/10 bg-indigo-400/10'
: '',
'mr-2 px-2.5 text-foreground/80'
)}
>
<ListOrdered className="h-5 w-5" />
</Button>
<div className="my-auto ml-2 mr-4 h-[1.5rem] w-[1.5px] bg-[#fff]/10"></div>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().toggleBlockquote().run()}
disabled={!editor?.can().chain().focus().toggleBlockquote().run()}
className={cn(
editor?.isActive('blockquote')
? 'border border-indigo-400/10 bg-indigo-400/10'
: '',
'mr-2 px-2.5 text-foreground/80'
)}
>
<Quote className="h-5 w-5" />
</Button>
<Button
variant="menuItem"
onClick={() => editor?.chain().focus().setHorizontalRule().run()}
className={cn('mr-2 px-2.5 text-foreground/80')}
>
<Space className="h-5 w-5" />
</Button>
</div>
<hr className="-mx-8 border-border pb-10" />
</div>
)
}
================================================
FILE: app/admin/write-article/page.tsx
================================================
import Loader from '@/components/loader'
import BackButton from '@/components/ui/back-button'
import dynamic from 'next/dynamic'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Admin - Write Article',
description: 'Write an article for the blog.',
}
const BlogForm = dynamic(() => import('./blog-form'), {
loading: () => <Loader />,
})
export default function page() {
return (
<section className="mx-auto mt-6 h-[100dvh] w-full max-w-prose pt-[72px] lg:mt-8">
<BackButton />
<BlogForm />
</section>
)
}
================================================
FILE: app/api/article/post/route.ts
================================================
import { z } from 'zod'
import { NextResponse } from 'next/server'
import prismadb from '@/libs/prismadb'
import { getCurrentSession } from '@/utils/auth-options'
import { postSchema } from '@/libs/validators/article-post-validator'
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
try {
const body = await request.json()
const { title, summary, category, slug, content, imageUrl } =
postSchema.parse(body)
const session = await getCurrentSession()
if (!session || !session.user.isCreator) {
return new NextResponse('Unauthorized', { status: 401 })
}
const slugExits = await prismadb.article.findFirst({
where: { slug },
})
if (slugExits) {
return new NextResponse('Article already exists', { status: 409 })
}
const blog = await prismadb.article.create({
data: {
title,
summary,
category,
slug,
content,
imageUrl,
},
})
revalidateTag('articles')
return new NextResponse('OK')
} catch (error) {
console.log(error)
console.log(error)
if (error instanceof z.ZodError) {
return new NextResponse(error.message, { status: 400 })
}
return new NextResponse('Something went wrong', { status: 500 })
}
}
================================================
FILE: app/api/auth/[...nextauth]/route.ts
================================================
import { AuthOptions } from 'next-auth'
import { authOptions } from '@/utils/auth-options'
import NextAuth from 'next-auth'
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
================================================
FILE: app/api/user/settings/route.ts
================================================
import { z } from 'zod'
import { NextResponse } from 'next/server'
import prismadb from '@/libs/prismadb'
import { getCurrentSession } from '@/utils/auth-options'
import { userSettingsSchema } from '@/libs/validators/user-settings-validator'
export async function GET() {
try {
const session = await getCurrentSession()
if (!session) {
return new NextResponse('Unauthorized', { status: 401 })
}
const dbUser = await prismadb.user.findUnique({
where: { id: session.user.id },
select: {
id: true,
name: true,
email: true,
image: true,
isCreator: true,
createdAt: true,
},
})
return NextResponse.json({
session: {
id: session.user.id,
name: session.user.name,
email: session.user.email,
image: session.user.image,
isCreator: session.user.isCreator,
},
database: dbUser,
})
} catch (error) {
return new NextResponse('Something went wrong', { status: 500 })
}
}
export async function PATCH(request: Request) {
try {
const session = await getCurrentSession()
if (!session) {
return new NextResponse('Unauthorized', { status: 401 })
}
const body = await request.json()
const { name, image } = userSettingsSchema.parse(body)
const updateData = {
name: name ?? undefined,
image: image ?? undefined,
}
await prismadb.user.update({
where: { id: session.user.id },
data: updateData,
})
return new NextResponse('OK')
} catch (error) {
if (error instanceof z.ZodError) {
return new NextResponse(error.message, { status: 400 })
}
return new NextResponse('Something went wrong', { status: 500 })
}
}
================================================
FILE: app/error.tsx
================================================
'use client'
/* eslint-disable react/no-unescaped-entities */
import { Button, buttonVariants } from '@/components/ui/button'
import { cn } from '@/utils/button-utils'
import { MailWarning, RotateCcw } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div className="flex flex-col items-center px-4 pt-40 text-center sm:px-6 lg:px-8">
<h1 className="block bg-gradient-to-br from-[#898AEB] via-[#898dd9]/80 to-[#8e8ece] bg-clip-text text-center text-4xl font-bold text-transparent sm:text-6xl ">
Something went amiss.
</h1>
<br />
<p className="max-w-xl text-center text-lg font-medium text-foreground opacity-90 md:text-xl">
<strong className="font-semibold">Sorry!</strong>{' '}
{error.message ? error.message : 'Looks like prismify needs glasses :('}
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-2 sm:flex-row sm:gap-3">
<Button onClick={reset} className="w-48 text-base sm:w-fit" variant="stylish">
<RotateCcw className="mr-2 h-5 w-5 text-foreground/80" />
Pray & Retry
</Button>
<Link
href="mailto:silson0072@gmail.com"
className={cn(
buttonVariants({ variant: 'default' }),
'w-48 px-4 text-base sm:w-fit'
)}
>
<MailWarning className="mr-2 h-5 w-5 text-foreground/80" />
Report
</Link>
</div>
</div>
)
}
================================================
FILE: app/layout.tsx
================================================
import ClarityScript from '@/components/clarity-script'
import Navbar from '@/components/navbar'
import { Toaster } from '@/components/ui/toaster'
import Providers from '@/providers'
import '@/styles/globals.css'
import { cn } from '@/utils/button-utils'
import { Analytics } from '@vercel/analytics/react'
import type { Metadata, Viewport } from 'next'
import { Plus_Jakarta_Sans } from 'next/font/google'
const Font = Plus_Jakarta_Sans({ subsets: ['latin'] })
export const metadata: Metadata = {
title:
'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
description:
'Easily make your SaaS/product shots and graphics design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.',
verification: {
google: 'cum1ckoCozAtSA3Xcn4UX_xR_FlfrlzKzQRa7nYQ2YM',
},
icons: {
icon: '/favicons/favicon.ico',
apple: '/favicons/apple-touch-icon.png',
},
applicationName: 'Prismify',
creator: 'Slson',
twitter: {
creator: '@xSls0n_007',
title:
'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
description:
'Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.',
card: 'summary_large_image',
images: [
{
url: 'https://prismify.vercel.app/opengraph-image.jpg',
width: 1200,
height: 630,
alt: 'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
},
],
},
openGraph: {
title:
'Create beautiful screenshots, graphics, designs for websites and social medias | Prismify',
description:
'Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics for websites, social media, and more. With Prismify, you get browser frames, gradient backgrounds, text, annotations.',
siteName: 'Prismify',
url: 'https://prismify.vercel.app',
locale: 'en_US',
type: 'website',
images: [
{
url: 'https://prismify.vercel.app/opengraph-image.jpg',
width: 1200,
height: 630,
alt: 'Prismify - Effortlessly Create Beautiful SaaS/Product Shots & Graphics',
},
],
},
category: 'Design',
alternates: {
canonical: 'https://prismify.vercel.app',
},
keywords: [
'SaaS product design',
'Create beautiful screenshots',
'Graphics design for social media',
'Add browser frames',
'Gradient backgrounds for graphics',
'Text annotations tool',
'Beautiful website graphics',
'Enhance social media visuals',
'Website demo image creator',
'Visual content enhancement',
'SaaS branding tool',
'Design beautiful image for products',
'Prismify graphics maker',
'SaaS graphics design',
'Website screenshot generator',
'SaaS marketing visuals',
],
metadataBase: new URL('https://prismify.vercel.app'),
robots: 'index, follow',
manifest: '/manifest.json',
}
export const viewport: Viewport = {
themeColor: '#151515',
colorScheme: 'dark',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
suppressHydrationWarning
className={cn('dark antialiased', Font.className)}
lang="en"
>
<body className="min-h-screen bg-[#111] text-dark">
<Toaster />
<Analytics />
<Providers>
<Navbar />
{children}
</Providers>
<ClarityScript />
</body>
</html>
)
}
================================================
FILE: app/loading.tsx
================================================
import Loader from '@/components/loader'
export default function Loading() {
return <Loader />
}
================================================
FILE: app/manifest.json
================================================
{
"name": "Prismify",
"short_name": "Prismify",
"start_url": "/",
"display": "standalone",
"background_color": "#111111",
"theme_color": "#151515",
"description": "Easily make your SaaS/product shots & design stand out. Create beautiful screenshots and graphics.",
"icons": [
{
"src": "/favicons/apple-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/favicons/favicon.ico",
"sizes": "48x48 64x64 96x96 128x128 256x256",
"type": "image/x-icon"
}
]
}
================================================
FILE: app/not-found.tsx
================================================
/* eslint-disable react/no-unescaped-entities */
'use client'
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/utils/button-utils'
import { MoveLeft } from 'lucide-react'
import Link from 'next/link'
export default function NotFound() {
return (
<div className="flex flex-col items-center px-4 pt-40 text-center sm:px-6 lg:px-8">
<h1 className="block bg-gradient-to-br from-[#898AEB] via-[#898dd9]/80 to-[#8e8ece] bg-clip-text text-center text-4xl font-bold text-transparent sm:text-6xl ">
Lost in the Pixels.
</h1>
<br />
<p className="max-w-xl text-center text-lg font-medium text-foreground opacity-90 md:text-xl">
<strong className="font-semibold">Not found!</strong> Looks like you've
wandered off the grid.
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-2 sm:flex-row sm:gap-3">
<Link
href="/"
className={cn(
buttonVariants({ variant: 'default' }),
'w-48 px-4 text-base sm:w-fit'
)}
>
<MoveLeft className="mr-2 h-5 w-5 text-foreground/80" />
Back to home
</Link>
</div>
</div>
)
}
================================================
FILE: app/page.tsx
================================================
import Sidebar from '@/components/editor/sidebar'
import Canvas from '@/components/editor/canvas-area'
export default function Home() {
return (
<main className="flex h-screen w-screen pt-[72px] sm:flex-row">
<Sidebar />
<Canvas />
</main>
)
}
================================================
FILE: app/robots.ts
================================================
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin/',
},
sitemap: [
'https://prismify.vercel.app/sitemap.xml',
'https://prismify.vercel.app/articles/sitemap.xml',
],
}
}
================================================
FILE: app/sitemap.ts
================================================
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
const sitemapUrls: MetadataRoute.Sitemap = allStaticRoutes.map((p) => {
return {
url: `https://prismify.vercel.app/${p}`,
lastModified: '2024-01-13T12:17:43.023Z',
changeFrequency: p === '' ? 'monthly' : 'weekly',
}
})
return sitemapUrls
}
const allStaticRoutes = ['', 'about', 'articles']
================================================
FILE: components/articles/article-card.tsx
================================================
import Image from 'next/image'
import Link from 'next/link'
type ArticleCardProps = {
title: string
category: string
image: string
date: string
href: string
}
export default function ArticleCard({
title,
image,
date,
category,
href,
}: ArticleCardProps) {
return (
<div className="group flex flex-col items-center text-dark">
<Link
href={href}
className="h-[300px] w-full overflow-hidden rounded-xl bg-formDark"
>
<Image
src={image}
alt={title}
width={500}
height={300}
className="aspect-[4/3] h-full w-full object-cover object-center transition-all will-change-auto duration-300 group-hover:scale-105"
/>
</Link>
<div className="mt-4 flex w-full flex-col">
<span className="text-xs font-semibold uppercase text-purple sm:text-sm">
{category}
</span>
<Link href={href} className="my-1.5 inline-block">
<h2 className="line-clamp-2 text-base font-semibold capitalize text-dark sm:text-lg">
{title}
</h2>
</Link>
<span className="dark:text-light/50 max-w-xs text-sm font-medium capitalize text-dark/60 sm:text-base">
{date}
</span>
</div>
</div>
)
}
================================================
FILE: components/auth-modal.tsx
================================================
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import { useMediaQuery } from '@/hooks/use-media-query'
import { LogIn } from 'lucide-react'
import SignInForm from './sign-in-form'
export function AuthModal() {
const [open, setOpen] = React.useState(false)
const isDesktop = useMediaQuery('(min-width: 768px)')
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
className="rounded-xl text-[13.6px]"
size="sm"
variant="default"
>
<p className="hidden md:block">Login</p>
<LogIn size={18} className="flex-center md:ml-2" />
</Button>
</DialogTrigger>
<DialogContent className="scale-110 p-16">
<SignInForm />
</DialogContent>
</Dialog>
)
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button
className="rounded-xl text-[13.6px]"
size="sm"
variant="default"
>
<p className="hidden md:block">Login</p>
<LogIn size={18} className="flex-center md:ml-2" />
</Button>
</DrawerTrigger>
<DrawerContent className="mb-6 rounded-xl bg-[#121212] px-6 py-2">
<div className="mt-8" />
<SignInForm />
<div className="mt-4" />
</DrawerContent>
</Drawer>
)
}
================================================
FILE: components/clarity-script.tsx
================================================
import Script from 'next/script'
const ClarityScript = () => (
<Script id="clarity-script" strategy="afterInteractive">
{`
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID}");
`}
</Script>
)
export default ClarityScript
================================================
FILE: components/color-picker.tsx
================================================
'use client'
import { useState } from 'react'
import {
HexAlphaColorPicker,
HexColorInput,
HexColorPicker,
} from 'react-colorful'
export default function ColorPicker({
onChange,
colorState,
shouldShowAlpha = true,
}: {
onChange: (color: string) => void
colorState: string
shouldShowAlpha?: boolean
}) {
const [color, setColor] = useState(colorState)
return (
<>
<div className="flex-center flex-col gap-2">
{shouldShowAlpha ? (
<HexAlphaColorPicker
color={color}
onChange={(color) => {
setColor(color)
onChange(color)
}}
/>
) : (
<HexColorPicker
color={color}
onChange={(color) => {
setColor(color)
onChange(color)
}}
/>
)}
<div className="flex-center relative h-full w-full rounded-md border border-[#22262b] bg-formDark text-center text-sm uppercase text-gray-100 md:text-sm">
<span className="absolute left-2 font-medium text-gray-400">#</span>
<HexColorInput
color={color}
onChange={(color) => {
setColor(color)
onChange(color)
}}
className="h-full w-full rounded-md border-[#22262b] bg-formDark px-3 py-3 text-center text-gray-100 focus:border-[#8e8ece] focus:outline-none focus:ring-1 focus:ring-[#8e8ece] md:text-sm"
/>
</div>
</div>
</>
)
}
================================================
FILE: components/editor/background-image-canvas.tsx
================================================
// this component shows background image & noise in the canvas
import { useBackgroundOptions } from '@/store/use-background-options'
import Noise from '@/components/editor/noise'
export default function BackgroundImageCanvas() {
const { imageBackground } = useBackgroundOptions()
return (
<>
{imageBackground && (
// eslint-disable-next-line @next/next/no-img-element
<img
draggable={false}
className={`pointer-events-none absolute z-[0] h-full w-full object-cover`}
src={imageBackground}
alt="background image"
/>
)}
<Noise />
</>
)
}
================================================
FILE: components/editor/background-options/custom-gradient-picker.tsx
================================================
'use client'
import { useCallback } from 'react'
import { solidColors } from '@/utils/presets/solid-colors'
import { Button } from '@/components/ui/button'
import PopupColorPicker from '@/components/popup-color-picker'
import { useBackgroundOptions } from '@/store/use-background-options'
export default function CustomGradientPicker() {
const {
setBackground,
background,
setBackgroundType,
solidColor,
setSolidColor,
setImageBackground,
imageBackground,
} = useBackgroundOptions()
const updateRootStyles = useCallback((color: string) => {
if (typeof window === 'undefined') return
document?.documentElement.style.setProperty('--solid-bg', color)
document?.documentElement.style.setProperty('--gradient-bg', color)
document?.documentElement.style.setProperty('--mesh-bg', color)
}, [])
const handleColorChange = useCallback(
(color: string) => {
setBackgroundType('solid')
setSolidColor(color)
setBackground(color)
setImageBackground(null)
updateRootStyles(color)
},
[
setSolidColor,
setBackground,
setBackgroundType,
updateRootStyles,
setImageBackground,
]
)
return (
<>
<div>
<h3 className="mb-3 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<span>Pick a color:</span>
</h3>
<PopupColorPicker onChange={handleColorChange} color={solidColor} />
</div>
<div>
<h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<span>Solid Colors:</span>
</h3>
<div className="mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid w-full">
{solidColors.slice(0, 1).map(({ background: solidBackground }) => {
return (
<Button
key={solidBackground}
className={`aspect-square overflow-hidden rounded-sm p-0 ${
background === solidBackground &&
!imageBackground &&
'outline-none ring-2 ring-ring ring-offset-2'
}`}
onClick={() => handleColorChange(solidBackground)}
style={{ background: solidBackground }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="h-full w-full scale-150"
src="/images/transparent.jpg"
alt="transparent background"
/>
</Button>
)
})}
{solidColors.slice(1).map(({ background: solidBackground }) => {
return (
<Button
key={solidBackground}
variant="secondary"
className={`h-[2.56rem] w-[2.56rem] rounded-md ${
background === solidBackground &&
!imageBackground &&
'outline-none ring-2 ring-ring ring-offset-2'
}`}
onClick={() => handleColorChange(solidBackground)}
style={{ background: solidBackground }}
/>
)
})}
</div>
</div>
</>
)
}
================================================
FILE: components/editor/background-options/image-gradient-picker.tsx
================================================
'use client'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Skeleton } from '@/components/ui/skeleton'
import { Switch } from '@/components/ui/switch'
import { toast } from '@/hooks/use-toast'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight, Settings2 } from 'lucide-react'
import { Key, useEffect, useState } from 'react'
export default function ImageGradientPicker() {
const {
setImageBackground,
imageBackground,
setAttribution,
setHighResBackground,
highResBackground,
setBackgroundType,
} = useBackgroundOptions()
const [currentPage, setCurrentPage] = useState(1)
const fetchUnsplashPictures = async (page: number) => {
const response = await fetch(
`https://api.unsplash.com/collections/5wgHcmn38m4/photos?page=${page}&per_page=30&q=100&fit=clip&w=1500&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`
)
const data = await response.json()
return data
}
const {
isLoading,
isError,
refetch,
data: unsplashData,
error,
} = useQuery({
queryKey: ['unsplash-gradients'],
queryFn: () => fetchUnsplashPictures(currentPage),
})
useEffect(
() => {
refetch()
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentPage]
)
if (isLoading) {
const skeletonLoaders = Array.from({ length: 30 }).map((_, index) => (
<li
className={`h-[2.56rem] w-[2.56rem] rounded-md`}
key={`skeleton-${index}`}
>
<Skeleton className="h-full w-full rounded-md" />
</li>
))
return (
<>
<h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<span>Images:</span>
</h3>
<ul className="mt-4 grid max-w-[18rem] auto-rows-auto grid-cols-5 gap-4">
{skeletonLoaders}
</ul>
</>
)
}
if (isError && error instanceof Error) {
toast({
title: 'Error',
description: error.message,
variant: 'destructive',
})
return <span>Error: {error.message}</span>
}
return (
<>
<h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<span>Images:</span>
<Popover>
<PopoverTrigger asChild>
<Settings2 size={20} />
</PopoverTrigger>
<PopoverContent className="flex w-fit flex-wrap gap-3">
<h1 className="text-[0.85rem]">High resolution background</h1>
<Switch
checked={highResBackground}
onCheckedChange={(checked) => {
setHighResBackground(checked)
}}
/>
</PopoverContent>
</Popover>
</h3>
<ul className="mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid">
{unsplashData.map(
(data: {
user: any
links: any
id: Key | null | undefined
urls: {
regular: string | null
small_s3: string | undefined
full: string | undefined
}
alt_description: string | undefined
}) => (
<li className={`h-[2.56rem] w-[2.56rem] rounded-md`} key={data.id}>
<button
className={`h-full w-full rounded-md ${
imageBackground ===
(highResBackground
? `${data.urls.full}`
: `${data.urls.regular}`) &&
'outline-none ring-2 ring-ring ring-offset-2'
}`}
onClick={() => {
setBackgroundType('gradient')
setImageBackground(
highResBackground
? `${data.urls.full}`
: `${data.urls.regular}`
)
setAttribution({
name: data.user.first_name,
link: data.user.username,
})
// Just triggering a download (Unsplash guideline)
fetch(
`${data.links.download_location}&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`
)
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="h-full w-full rounded-md object-cover"
src={data.urls.small_s3!}
alt={data.alt_description}
/>
</button>
</li>
)
)}
</ul>
<div className="flex justify-end mt-6 gap-2">
<Button
size="sm"
variant={'stylish'}
disabled={currentPage === 1}
className="text-sm flex-center h-9 px-2.5"
onClick={() => {
setCurrentPage((prevPage) => prevPage - 1)
}}
>
<ChevronLeft size={16} className="mr-1 translate-y-[1px]" />
<p>Back</p>
</Button>
<Button
variant={'stylish'}
disabled={currentPage === 3}
className="text-sm flex-center h-9 px-2.5"
onClick={() => {
setCurrentPage((prevPage) => prevPage + 1)
}}
>
<p>Next</p>
<ChevronRight size={16} className="ml-1 translate-y-[1px]" />
</Button>
</div>
</>
)
}
================================================
FILE: components/editor/background-options/index.tsx
================================================
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useBackgroundOptions } from '@/store/use-background-options'
import CustomGradientPicker from './custom-gradient-picker'
import NormalGradientPicker from './normal-gradient-picker'
import NoiseSlider from './noise-slider'
import PatternPicker from './pattern-picker'
import { useEffect } from 'react'
export default function BackgroundOptions() {
const { backgroundType, setIsBackgroundClicked } = useBackgroundOptions()
useEffect(() => {
setIsBackgroundClicked(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<Tabs
className="mt-4 !border-none !shadow-none !bg-transparent w-full"
defaultValue={
backgroundType === 'solid'
? 'customTab'
: backgroundType === 'pattern'
? 'patternsTab'
: 'gradientsTab'
}
>
<TabsList className="mb-4 bg-transparent border-none border-b border-border [&>*]:px-[1rem] [&>*]:border-b-2 [&>*]:border-border/40 [&>*]:pb-2 [&>*]:rounded-none [&>*[data-state=active]]:border-foreground [&>*[data-state=active]]:bg-transparent *:w-fit">
<TabsTrigger value="gradientsTab">Gradient</TabsTrigger>
<TabsTrigger value="patternsTab">Abstract</TabsTrigger>
<TabsTrigger className='hidden sm:flex' value="customTab">
Pick
</TabsTrigger>
</TabsList>
<NoiseSlider />
<TabsContent value="gradientsTab">
<NormalGradientPicker />
</TabsContent>
<TabsContent value="patternsTab">
<PatternPicker />
</TabsContent>
<TabsContent value="customTab">
<CustomGradientPicker />
</TabsContent>
</Tabs>
)
}
================================================
FILE: components/editor/background-options/noise-slider.tsx
================================================
import { Slider } from '@/components/ui/slider'
import { useBackgroundOptions } from '@/store/use-background-options'
import { Grip } from 'lucide-react'
export default function NoiseSlider() {
const { noise, setNoise } = useBackgroundOptions()
return (
<div>
<div className="mb-3 mt-4 flex items-center px-1">
<h3 className="mb-3 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<Grip size={18} />
<span>Noise</span>
</h3>
</div>
<div className="mb-3 flex gap-4 text-[0.85rem]">
<Slider
defaultValue={[0]}
max={0.8}
min={0}
step={0.05}
value={[noise]}
onValueChange={(value: number[]) => {
setNoise(value[0])
}}
onIncrement={() => {
if (noise >= 0.8) return
setNoise(noise + 0.05)
}}
onDecrement={() => {
if (noise <= 0) return
setNoise(noise - 0.05)
}}
/>
</div>
</div>
)
}
================================================
FILE: components/editor/background-options/normal-gradient-picker.tsx
================================================
'use client'
import { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import CircularSliderComp from '@/components/ui/circular-slider'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import SpotlightButton from '@/components/ui/spotlight-button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { gradients, type Gradient } from '@/utils/presets/gradients'
import ColorThief from 'colorthief'
import { Settings2, Sparkles, Zap, Palette, Wand2, Check, Rocket } from 'lucide-react'
import { useCallback } from 'react'
import ImageGradientPicker from './image-gradient-picker'
import { cn } from '@/utils/button-utils'
type Color = string
export default function NormalGradientPicker() {
const {
setBackground,
background: backgroundInStore,
setBackgroundType,
backgroundType,
setImageBackground,
imageBackground,
setAttribution,
setNoise,
} = useBackgroundOptions()
const { images, setImages } = useImageOptions()
const { selectedImage } = useSelectedLayers()
const [dominantColor, setDominantColor] = useState(null)
const [isGenerating, setIsGenerating] = useState(false)
const handleGradientClick = useCallback(
(gradient: Gradient, isMesh: boolean) => {
if (typeof window === 'undefined') return
document?.documentElement.style.setProperty(
'--gradient-bg',
gradient.gradient
)
document?.documentElement.style.setProperty(
'--mesh-bg',
isMesh ? gradient.background! : gradient.gradient
)
setBackground(gradient.gradient)
setBackgroundType(isMesh ? 'mesh' : 'gradient')
setImageBackground(null)
setAttribution({ name: null, link: null })
},
[setBackground, setBackgroundType, setImageBackground, setAttribution]
)
const extractDominantColor = async () => {
setIsGenerating(true)
const colorThief = new ColorThief()
const image = new Image()
image.crossOrigin = 'Anonymous'
image.src =
document.getElementById(`img-${selectedImage}`)?.getAttribute('src') ?? ''
image.onload = function () {
const palletes = colorThief.getPalette(image, 8)
const dominantColor = colorThief.getColor(image)
function generateGradients(
dominantColor: Color,
colors: Color[]
): { linearGradients: string[]; radialGradients: string[]; meshGradients: string[] } {
const getRandomColors = (limit: number): Color[] => {
const randomColors: Color[] = []
for (let i = 0; i < limit; i++) {
const randomIndex = Math.floor(Math.random() * colors.length)
randomColors.push(colors[randomIndex])
}
return randomColors
}
// Linear Gradients - Clean, directional gradients with 2-3 colors
const generateLinearGradients = (): string[] => {
const gradients: string[] = []
// Dominant to each palette color
colors.forEach((color: Color, index: number) => {
gradients.push(`linear-gradient(${135 + index * 30}deg, ${dominantColor} 0%, ${color} 100%)`)
})
// Two-color combinations from palette
for (let i = 0; i < 4; i++) {
const [color1, color2] = getRandomColors(2)
gradients.push(`linear-gradient(${45 + i * 45}deg, ${color1} 0%, ${color2} 100%)`)
}
// Three-color smooth transitions
for (let i = 0; i < 3; i++) {
const [color1, color2, color3] = getRandomColors(3)
gradients.push(`linear-gradient(${90 + i * 60}deg, ${color1} 0%, ${color2} 50%, ${color3} 100%)`)
}
return gradients.slice(0, 14)
}
// Radial Gradients - Circular, spotlight effects
const generateRadialGradients = (): string[] => {
const gradients: string[] = []
// Center spotlight with dominant color
colors.forEach((color: Color) => {
gradients.push(`radial-gradient(circle at 50% 50%, ${dominantColor} 0%, ${color} 100%)`)
})
// Off-center radials for dynamic feel
const positions = ['20% 30%', '80% 20%', '30% 80%', '70% 70%']
positions.forEach((pos, i) => {
const [color1, color2] = getRandomColors(2)
gradients.push(`radial-gradient(circle at ${pos}, ${color1} 0%, ${color2} 60%, transparent 100%)`)
})
// Elliptical radials
for (let i = 0; i < 3; i++) {
const [color1, color2] = getRandomColors(2)
gradients.push(`radial-gradient(ellipse at ${50 + i * 20}% 50%, ${color1} 0%, ${color2} 100%)`)
}
return gradients.slice(0, 14)
}
// Mesh Gradients - Complex, multi-layered beautiful gradients
const generateMeshGradients = (): string[] => {
const gradients: string[] = []
// Beautiful mesh gradients with multiple radial layers
for (let i = 0; i < 14; i++) {
const meshColors = getRandomColors(4)
const positions = [
{ x: 10 + Math.random() * 30, y: 10 + Math.random() * 30 },
{ x: 60 + Math.random() * 30, y: 10 + Math.random() * 30 },
{ x: 10 + Math.random() * 30, y: 60 + Math.random() * 30 },
{ x: 60 + Math.random() * 30, y: 60 + Math.random() * 30 }
]
const meshGradient = `
radial-gradient(circle at ${positions[0].x}% ${positions[0].y}%, ${meshColors[0]} 0%, transparent 50%),
radial-gradient(circle at ${positions[1].x}% ${positions[1].y}%, ${meshColors[1]} 0%, transparent 50%),
radial-gradient(circle at ${positions[2].x}% ${positions[2].y}%, ${meshColors[2]} 0%, transparent 50%),
radial-gradient(circle at ${positions[3].x}% ${positions[3].y}%, ${meshColors[3]} 0%, transparent 50%),
linear-gradient(${45 + i * 15}deg, ${dominantColor} 0%, ${meshColors[0]} 100%)
`.trim()
gradients.push(meshGradient)
}
return gradients
}
const linearGradients = generateLinearGradients()
const radialGradients = generateRadialGradients()
const meshGradients = generateMeshGradients()
setImages(
images.map((image, index) =>
index === images.length - 1
? {
...image,
dominantColor,
palletes,
linearGradients,
meshGradients,
radialGradients,
}
: image
)
)
setTimeout(() => setIsGenerating(false), 800)
return {
linearGradients,
radialGradients,
meshGradients,
}
}
generateGradients(
// @ts-ignore
`rgb(${dominantColor.join(',')})`,
// @ts-ignore
palletes.map((pallete) => `rgb(${pallete.join(',')})`)
)
}
}
const GradientSection = ({ title, gradients, icon: Icon, description }: {
title: string;
gradients: string[];
icon: any;
description: string;
}) => (
<div className="group relative">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-purple/50" />
<h4 className="text-sm font-semibold text-foreground">{title}</h4>
<Badge className="text-xs px-2 py-0.5 bg-primary/10 0 text-purple border border-primary/10">
{gradients?.length}
</Badge>
</div>
<span className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
{description}
</span>
</div>
<div className="grid grid-cols-7 gap-2">
{gradients.map((gradient: string, index: number) => (
<Button
key={gradient}
type="button"
variant="secondary"
className={cn(
"aspect-square h-[1.85rem] w-[1.85rem] overflow-hidden rounded-md p-[1px] transition-all duration-300",
"hover:scale-105 hover:shadow-lg hover:shadow-primary/25 hover:brightness-105",
" hover:ring-1 hover:ring-primary/20",
"transform-gpu active:scale-95 active:shadow-inner",
"group relative cursor-pointer",
gradient === backgroundInStore && !imageBackground &&
"ring-2 ring-primary ring-offset-2 ring-offset-background scale-105 shadow-lg shadow-primary/30 brightness-105"
)}
onClick={() =>
handleGradientClick(
{
gradient,
background: gradient,
type: title === 'Mesh' ? 'Mesh' : 'Normal',
},
title === 'Mesh'
)
}
style={{ background: gradient }}
>
{gradient === backgroundInStore &&
!imageBackground &&
backgroundType !== 'mesh' && (
<Rocket
className="h-4 w-4 text-white drop-shadow-lg transition-all duration-1000 repeat-infinite animate-pulse"
/>
)}
</Button>
))}
</div>
</div>
)
console.log(dominantColor)
return (
<div>
{/* Premium Header */}
<div className="relative mt-8 overflow-hidden rounded-2xl border border-border/50 bg-gradient-to-br from-background via-muted/20 to-muted/40 p-4 backdrop-blur-sm">
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 via-primary/10 to-primary/5" />
<div className="relative">
<h4 className="font-medium text-foreground mb-1">Adaptive Gradient</h4>
<p className="text-sm text-muted-foreground mb-4 max-w-md">
Generate stunning gradients matched to your image's color palette. {selectedImage ? (
<>
Click on the{' '}
<button
onClick={extractDominantColor}
disabled={!selectedImage || isGenerating}
className="font-medium text-purple hover:text-purple/80 underline decoration-purple/50 hover:decoration-purple transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Generate
</button>
{' '}button to generate.
</>
) : 'Please click on the image layer to generate.'}
</p>
<div
className="transition-all duration-300"
>
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<div className="inline-block">
<SpotlightButton
as={!selectedImage ? 'div' : 'button'}
onClick={extractDominantColor}
text={isGenerating ? "Analyzing..." : (selectedImage && images[selectedImage - 1]?.linearGradients ? "Regenerate" : "Generate")}
disabled={!selectedImage || isGenerating}
/>
</div>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-[10rem] z-[1000] text-center"
style={{
display: selectedImage ? 'none' : 'block',
}}
>
<div className="flex items-center text-left gap-2 mb-2">
<span className="font-medium">Image Required</span>
</div>
<p className="text-sm text-left">
Please click on the image layer on canvas first!
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isGenerating && (
<div className="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span>Analyzing color palette...</span>
</div>
)}
</div>
</div>
</div>
{/* Premium Gradient Collections */}
{selectedImage && images[selectedImage - 1]?.linearGradients && (
<div className="mt-8 space-y-8">
<GradientSection
title="Linear"
gradients={images[selectedImage - 1]?.linearGradients || []}
icon={Wand2}
description="Directional flow"
/>
<GradientSection
title="Radial"
gradients={images[selectedImage - 1]?.radialGradients || []}
icon={Sparkles}
description="Spotlight & circular"
/>
<GradientSection
title="Mesh"
gradients={images[selectedImage - 1]?.meshGradients || []}
icon={Palette}
description="Complex multi-layered"
/>
{/* Premium Tip */}
<div className="relative overflow-hidden rounded-xl border border-border/50 bg-gradient-to-r from-muted/30 to-muted/50 p-4">
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-primary/10" />
<div className="relative flex items-start gap-3">
<div>
<h4 className="font-medium text-foreground mb-1">Pro Tip</h4>
<p className="text-sm text-muted-foreground">
Mesh gradients work beautifully with{' '}
<button
onClick={() => setNoise(0.15)}
className="font-medium text-purple hover:text-purple/80 underline decoration-purple/50 hover:decoration-purple transition-colors"
>
15% noise
</button>
{' '}for that premium mesh effect.
</p>
</div>
</div>
</div>
</div>
)}
<h3 className="mt-12 flex items-center gap-2 text-xs font-medium uppercase text-muted-foreground">
<span>Standard Gradients</span>
</h3>
<div className="mt-4 flex w-full grid-cols-7 flex-wrap gap-[0.5rem] md:grid">
{gradients.map(({ gradient, background, type }: Gradient) => (
<Button
key={gradient}
type="button"
variant="secondary"
className={`h-[1.85rem] w-[1.85rem] overflow-hidden rounded-md p-[1px] ${
gradient === backgroundInStore &&
!imageBackground &&
'outline-none ring-2 ring-ring ring-offset-2'
}`}
onClick={() =>
handleGradientClick(
{
gradient,
background,
type: 'Normal',
},
type === 'Mesh' // will be true if its of type Mesh
)
}
style={
type === 'Normal'
? { background: gradient }
: { backgroundColor: background, backgroundImage: gradient }
}
>
{gradient === backgroundInStore &&
!imageBackground &&
backgroundType !== 'mesh' && (
<Popover>
<PopoverTrigger asChild>
<Settings2 className="flex-center" color="#333" size={20} />
</PopoverTrigger>
<PopoverContent className="flex w-[12rem] flex-col items-center gap-3">
<h1 className="text-[0.85rem]">Gradient angle</h1>
<div className={`circular-slider`}>
<CircularSliderComp />
</div>
</PopoverContent>
</Popover>
)}
</Button>
))}
</div>
<ImageGradientPicker />
</div>
)
}
================================================
FILE: components/editor/background-options/pattern-picker.tsx
================================================
'use client'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Skeleton } from '@/components/ui/skeleton'
import { Switch } from '@/components/ui/switch'
import { toast } from '@/hooks/use-toast'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useQuery } from '@tanstack/react-query'
import { Settings2 } from 'lucide-react'
import { Key, useState } from 'react'
export default function PatternPicker() {
const {
setImageBackground,
imageBackground,
setAttribution,
setHighResBackground,
highResBackground,
setBackgroundType,
} = useBackgroundOptions()
const [currentPage, setCurrentPage] = useState(1)
const fetchUnsplashPatterns = async (page: number) => {
const response = await fetch(
`https://api.unsplash.com/collections/W121KJsaTEs/photos?page=${page}&per_page=30&q=100&client_id=${process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY}`
)
const data = await response.json()
return data
}
const {
isLoading,
isError,
data: unsplashData,
error,
} = useQuery({
queryKey: ['unsplash-patterns', currentPage],
queryFn: () => fetchUnsplashPatterns(currentPage),
})
if (isLoading) {
const skeletonLoaders = Array.from({ length: 30 }).map((_, index) => (
<li
className={`h-[2.56rem] w-[2.56rem] rounded-md`}
key={`skeleton-${index}`}
>
<Skeleton className="h-full w-full rounded-md" />
</li>
))
return (
<>
<h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<span>Abstract:</span>
</h3>
<ul className="mt-4 grid auto-rows-auto grid-cols-4 gap-4 md:max-w-[18rem] md:grid-cols-5">
{skeletonLoaders}
</ul>
</>
)
}
if (isError && error instanceof Error) {
toast({
title: 'Error',
description: error.message,
variant: 'destructive',
})
return <span>Error: {error.message}</span>
}
return (
<>
<h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<span>Abstract:</span>
<Popover>
<PopoverTrigger asChild>
<Settings2 size={20} className="rotate-90" />
</PopoverTrigger>
<PopoverContent className="flex w-fit flex-col flex-wrap gap-3">
<div className="flex gap-3">
<h1 className="text-[0.85rem]">High resolution background</h1>
<Switch
checked={highResBackground}
onCheckedChange={(checked) => {
setHighResBackground(checked)
}}
/>
</div>
</PopoverContent>
</Popover>
</h3>
<ul className="mt-4 flex grid-cols-5 flex-wrap gap-x-2.5 gap-y-3 md:grid">
{unsplashData.map(
(data: {
user: any
links: any
id: Key | null | undefined
urls: {
regular: string | null
small_s3: string | undefined
full: string | undefined
}
alt_description: string | undefined
}) => (
<li className={`h-[2.56rem] w-[2.56rem] rounded-md`} key={data.id}>
<button
className={`h-full w-full rounded-md ${
imageBackground ===
(highResBackground
? `${data.urls.full}`
: `${data.urls.regular}`) &&
'outline-none ring-2 ring-ring ring-offset-2'
}`}
onClick={() => {
setBackgroundType('pattern')
setImageBackground(
highResBackground
? `${data.urls.full}`
: `${data.urls.regular}`
)
setAttribution({
name: data.user.first_name,
link: data.user.username,
})
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="h-full w-full rounded-md object-cover"
src={data.urls.small_s3!}
alt={data.alt_description}
/>
</button>
</li>
)
)}
</ul>
<div className="flex justify-end gap-2 md:max-w-[18rem]">
<Button
size="sm"
variant={'stylish'}
disabled={currentPage === 1}
className="mt-4 text-sm"
onClick={() => {
setCurrentPage((prevPage) => prevPage - 1)
}}
>
← Back
</Button>
<Button
size="sm"
variant={'stylish'}
disabled={currentPage === 2}
className="mt-4 text-sm"
onClick={() => {
setCurrentPage((prevPage) => prevPage + 1)
}}
>
Next →
</Button>
</div>
</>
)
}
================================================
FILE: components/editor/browser-frames.tsx
================================================
// This component is responsible for rendering the browser frame around the image layer.
'use client'
import { FrameTypes, useFrameOptions } from '@/store/use-frame-options'
import { cn } from '@/utils/button-utils'
const FrameButton = ({ color }: { color: string }) => (
<div
className={'rounded-full'}
style={{
backgroundColor: color,
}}
/>
)
const FrameButtons = ({
hasButtonColor,
frame,
}: {
hasButtonColor: boolean
frame: FrameTypes
}) => {
const { frameHeight } = useFrameOptions()
let colors: string[] = []
if (hasButtonColor && frame === 'MacOS Dark') {
colors = ['#f7645c', '#fbc341', '#3cc84a']
} else if (hasButtonColor && frame === 'MacOS Light') {
colors = ['#f7645ccc', '#fbc341d2', '#3cc84ac5']
} else if (!hasButtonColor && frame === 'MacOS Dark') {
colors = ['#ffffff33', '#ffffff33', '#ffffff33']
} else if (!hasButtonColor && frame === 'MacOS Light') {
colors = ['#00000033', '#00000033', '#00000033']
}
return (
<div
className={`mr-2 flex basis-[6%] ${
frameHeight === 'small'
? 'gap-[0.6vw] [&>*]:h-[0.7vw] [&>*]:w-[0.7vw]'
: frameHeight === 'medium'
? 'gap-[0.65vw] [&>*]:h-[0.8vw] [&>*]:w-[0.8vw]'
: 'gap-[0.7vw] [&>*]:h-[0.9vw] [&>*]:w-[0.9vw]'
}`}
>
{colors.map((color, index) => (
<FrameButton key={index} color={color} />
))}
</div>
)
}
const FrameSearchBar = ({ frame }: { frame: FrameTypes }) => (
<div
className={cn(
'flex h-[50%] w-full flex-1 items-center rounded-md px-2 opacity-5',
frame === 'MacOS Dark' ? 'bg-white' : 'bg-[#000]'
)}
/>
)
const FrameContainer = ({
frameHeight,
children,
style,
additionalClasses = '',
}: {
frameHeight: string
children: React.ReactNode
style: React.CSSProperties
additionalClasses?: string
}) => {
const heightClass =
frameHeight === 'small'
? 'h-[2.6vw] px-[1.6vw]'
: frameHeight === 'medium'
? 'h-[3vw] px-[1.8vw]'
: 'h-[3.4vw] px-[2vw]'
return (
<div
style={style}
className={`flex items-center gap-4 ${heightClass} ${additionalClasses}`}
>
{children}
</div>
)
}
export default function BrowserFrame({ frame }: { frame: FrameTypes }) {
const {
frameHeight,
showSearchBar,
macOsDarkColor,
macOsLightColor,
hideButtons,
hasButtonColor,
} = useFrameOptions()
const props = { frame }
const frameComponents = {
'MacOS Dark': (
<FrameContainer
style={{ background: macOsDarkColor }}
frameHeight={frameHeight}
>
{!hideButtons && (
<FrameButtons hasButtonColor={hasButtonColor} {...props} />
)}
{showSearchBar && <FrameSearchBar {...props} />}
</FrameContainer>
),
'MacOS Light': (
<FrameContainer
style={{ background: macOsLightColor }}
frameHeight={frameHeight}
additionalClasses="border-b border-[#00000010]"
>
{!hideButtons && (
<FrameButtons hasButtonColor={hasButtonColor} {...props} />
)}
{showSearchBar && <FrameSearchBar {...props} />}
</FrameContainer>
),
None: null,
Arc: null,
Shadow: null,
}
return frameComponents[frame] || null
}
================================================
FILE: components/editor/canvas-area.tsx
================================================
'use client'
import useAutomaticAspectRatioSwitcher from '@/hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher'
import useCanvasResizeObserver from '@/hooks/canvas-area-hooks/use-resize-observer'
import useScreenSizeWarningToast from '@/hooks/canvas-area-hooks/use-screen-size-warning-toast'
import { useEventListener } from '@/hooks/use-event-listener'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import dynamic from 'next/dynamic'
import React, { CSSProperties, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import BackgroundImageCanvas from './background-image-canvas'
import ImageUpload from './main-image-area'
import MobileViewImageOptions from './mobile-view-image-options'
import SelectoComponent from './selecto-component'
import TextLayers from './text-layers'
import TiptapMoveable from './tiptap-moveable'
const MoveableComponent = dynamic(
() => import('./moveable-component').then((mod) => mod.default),
{ ssr: false }
)
export default function Canvas() {
const { backgroundType } = useBackgroundOptions()
const {
resolution,
exactDomResolution,
scrollScale,
setScrollScale,
canvasRoundness,
} = useResizeCanvas()
const { scale } = useImageOptions()
const {
selectedImage,
selectedText,
setSelectedImage,
enableCrop,
setSelectedText,
} = useSelectedLayers()
const screenshotRef = useRef<HTMLDivElement | null>(null)
const parentRef = useRef<HTMLDivElement | null>(null)
const {
showControls,
setShowControls,
setShowTextControls,
setIsMultipleTargetSelected,
showTextControls,
isEditable,
setIsEditable,
isSelecting,
} = useMoveable()
const [width, height]: number[] = resolution.split('x').map(Number)
const aspectRatio = width / height
console.log(`Current DOM resoltion: ${exactDomResolution}`)
let style: CSSProperties = {
aspectRatio,
backgroundImage: `var(--gradient-bg)`,
borderRadius: `${canvasRoundness * 16}px`,
}
if (backgroundType === 'mesh') {
style = {
...style,
backgroundColor: `var(--mesh-bg)`,
}
}
if (backgroundType === 'solid') {
style = {
...style,
backgroundColor: `var(--solid-bg)`,
}
}
// This hook encapsulates the logic for observing changes in the size of the screenshot element & automatically sets the DOM resolution and scale factor based on the size changes of a provided ref element.
useCanvasResizeObserver(screenshotRef)
// This hook encapsulates the logic used to automatically switch the aspect ratio of a screenshot within a container. If the screenshot overflows the container, the aspect ratio is adjusted to fit within the container.
useAutomaticAspectRatioSwitcher({
containerRef: parentRef,
screenshotRef,
})
// This hook shows a warning toast if the screen size is less than 768px.
useScreenSizeWarningToast()
const handleScroll = (e: React.WheelEvent<HTMLDivElement>) => {
if (typeof window !== 'undefined' && window.innerWidth <= 768) {
return
}
if (enableCrop) return
if (e.deltaY < 0) {
// Scrolling up
if (scrollScale === 1) return
setScrollScale(scrollScale + 0.1) // Increment the scroll scale by 0.1
} else if (e.deltaY > 0) {
// Scrolling down
if (scrollScale <= 0.4) return
setScrollScale(scrollScale - 0.1) // Decrement the scroll scale by 0.1
}
}
let parentScaleStyle = {
scale: `${scrollScale}`,
}
// Close the image controls when the escape key is pressed
useHotkeys('Escape', () => {
if (showControls) {
setShowControls(false)
setIsMultipleTargetSelected(false)
}
})
// this hook listens for a click event on the canvas and hides the image controls/text controls if the user clicks outside the image. This just works idk how, I suggest you don't touch it.
useEventListener(
'click',
(e: any) => {
const isCanvasArea =
e?.target?.classList?.contains('canvas-container') ||
e?.target?.classList?.contains('selecto-area')
if (isCanvasArea) {
setSelectedText(null)
setShowTextControls(false)
setShowTextControls(false)
setIsEditable(false)
}
if (isSelecting || (!selectedImage && !showControls)) return
if (isCanvasArea) {
setSelectedImage(null)
setShowTextControls(false)
setShowControls(false)
setIsMultipleTargetSelected(false)
}
},
screenshotRef
)
return (
<>
<section
ref={parentRef}
className={`relative flex h-full w-full flex-col overflow-hidden bg-[#111] md:grid md:place-items-center ${
aspectRatio <= 1 ? 'p-4 md:p-8' : 'p-4 md:p-8'
}
`}
style={parentScaleStyle}
onWheel={handleScroll}
>
<div
className={`canvas-container relative flex items-center justify-center overflow-hidden ${
aspectRatio <= 1
? 'h-auto w-full lg:h-full lg:w-auto'
: 'h-auto w-full'
}`}
ref={screenshotRef}
id="canvas-container"
style={style}
>
<BackgroundImageCanvas />
{showControls && <MoveableComponent id={`${selectedImage}`} />}
{showTextControls && !isEditable && (
<TiptapMoveable id={`text-${selectedText}`} />
)}
<div
className="selecto-area relative flex h-full w-full place-items-center items-center justify-center"
style={{
scale,
}}
>
<ImageUpload />
<TextLayers />
</div>
</div>
<MobileViewImageOptions />
</section>
<SelectoComponent />
</>
)
}
================================================
FILE: components/editor/canvas-options/canvas-roundness-slider.tsx
================================================
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { RotateCcw } from 'lucide-react'
export default function CanvasRoundnessSlider() {
const { canvasRoundness, setCanvasRoundness } = useResizeCanvas()
return (
<>
<div className="mb-3 mt-4 flex items-center px-1 md:max-w-full">
<h1 className="text-[0.85rem]">Roundness</h1>
<p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
{`${Math.round((canvasRoundness / 3) * 100)} `}
</p>
<Button
aria-label="reset roundness"
variant="secondary"
size="sm"
className="ml-auto translate-x-2"
onClick={() => setCanvasRoundness(0)}
>
<RotateCcw size={15} className="text-dark/80" />
</Button>
</div>
<div className="flex gap-4 text-[0.85rem] md:max-w-full">
<Slider
defaultValue={[0]}
max={3}
min={0}
step={0.01}
value={[canvasRoundness]}
onValueChange={(value: number[]) => {
setCanvasRoundness(value[0])
}}
onDecrement={() => {
if (canvasRoundness <= 0) return
setCanvasRoundness(canvasRoundness - 0.03)
}}
onIncrement={() => {
if (canvasRoundness >= 3) return
setCanvasRoundness(canvasRoundness + 0.03)
}}
/>
</div>
</>
)
}
================================================
FILE: components/editor/canvas-options/index.tsx
================================================
'use client'
import {
Dribbble,
Facebook,
Instagram,
Linkedin,
Plus,
Minus,
Twitter,
Youtube,
ArrowRight,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { resolutions } from '@/utils/presets/resolutions'
import { Button } from '@/components/ui/button'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { Separator } from '@/components/ui/separator'
import { ResolutionButton } from './resolution-button'
import CanvasRoundnessSlider from './canvas-roundness-slider'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import {
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipContent,
} from '@/components/ui/tooltip'
import Icon from '@/components/icons'
const icons = {
Youtube: <Youtube size={18} />,
Instagram: <Instagram size={18} />,
Facebook: <Facebook size={18} />,
LinkedIn: <Linkedin size={18} />,
Twitter: <Twitter size={18} />,
Dribble: <Dribbble size={18} />,
ProductHunt: (
<div className="flex-center h-6 w-6 rounded-full bg-[#898aeb]/5">P</div>
),
}
const splitResolution = (resolution: string) => resolution.split('x')
export default function CanvasOptions() {
const {
setResolution,
domResolution,
scrollScale,
setScrollScale,
automaticResolution,
setAutomaticResolution,
} = useResizeCanvas()
const [width, height] = splitResolution(domResolution)
const [inputResolution, setInputResolution] = useState({
inputWidth: width,
inputHeight: height,
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setResolution(
`${inputResolution.inputWidth}x${inputResolution.inputHeight}`
)
}
useEffect(() => {
setInputResolution({
inputWidth: `${Math.round(+width)}`,
inputHeight: `${Math.round(+height)}`,
})
}, [height, width])
return (
<>
<div className="mt-4 flex w-full justify-between px-1">
<div className="flex-center">
<h1 className="mr-1 text-[0.85rem]">Auto resolution</h1>
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger>
<Icon variant="duotone" name="info" color="none" />
</TooltipTrigger>
<TooltipContent className="max-w-[12rem]">
<p>
When enabled, the canvas will automatically resize to fit your
image when you upload it.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Switch
checked={automaticResolution}
onCheckedChange={(checked) => {
setAutomaticResolution(checked)
}}
/>
</div>
<hr className="my-4 border-border" />
<h1 className="mb-3 mt-8 px-1 text-[0.85rem]">Custom Resolution</h1>
<form
onSubmit={handleSubmit}
className="flex w-full max-w-sm items-center space-x-2"
>
<Input
type="number"
value={inputResolution.inputWidth}
min={100}
max={5000}
onChange={(e) => {
setInputResolution({
...inputResolution,
inputWidth: e.target.value,
})
}}
className="rounded-lg text-sm"
/>
<span className="mx-2 my-auto">x</span>
<Input
type="number"
value={inputResolution.inputHeight}
min={100}
max={5000}
className="rounded-lg text-sm"
onChange={(e) => {
setInputResolution({
...inputResolution,
inputHeight: e.target.value,
})
}}
/>
<Button
type="submit"
variant="outline"
className="rounded-lg px-3 text-sm"
>
<ArrowRight size={18} />
</Button>
</form>
<h1 className="mb-3 mt-8 px-1 text-[0.85rem]">Resolutions</h1>
<div className="flex flex-wrap gap-3">
{resolutions.map((res, index) => (
<ResolutionButton
key={index}
resolutions={res?.resolutions}
name={res?.name}
icon={icons[res?.icon as keyof typeof icons]}
// variant={res.resolutions === resolution ? 'stylish' : 'outline'}
color={res.color}
variant="stylish"
className="rounded-lg"
/>
))}
</div>
<Separator className="mt-8 h-[0.1rem] w-full" />
<CanvasRoundnessSlider />
<Separator className="mt-8 h-[0.1rem] w-full" />
<h1 className="mb-3 mt-4 px-1 text-[0.85rem]">Preview scale</h1>
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
className="relative inline-flex items-center rounded-l-md px-2 py-2 ring-1 ring-inset ring-border focus:z-10 disabled:cursor-not-allowed bg-formDark text-dark"
disabled={scrollScale === 1}
onClick={() => {
if (scrollScale === 1) return
setScrollScale(scrollScale + 0.1)
}}
>
<span className="sr-only">Scale up</span>
<Plus className="h-5 w-5" aria-hidden="true" />
</button>
<button
type="button"
className="relative -ml-px inline-flex items-center rounded-r-md bg-formDark px-2 py-2 text-dark ring-1 ring-inset ring-border focus:z-10 disabled:cursor-not-allowed"
disabled={scrollScale <= 0.4}
onClick={() => {
if (scrollScale <= 0.4) return
setScrollScale(scrollScale - 0.1)
}}
>
<span className="sr-only">Scale down</span>
<Minus className="h-5 w-5" aria-hidden="true" />
</button>
</span>
</>
)
}
================================================
FILE: components/editor/canvas-options/resolution-button.tsx
================================================
import { useState } from 'react'
import { cn } from '@/utils/button-utils'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { PopoverArrow } from '@radix-ui/react-popover'
import { ChevronDown } from 'lucide-react'
import { calculateEqualCanvasSize } from '@/utils/helper-fns'
export function ResolutionButton({
resolutions,
name,
icon,
className,
color,
variant,
}: {
resolutions: any
name: string
icon?: React.ReactNode
className?: string
color?: string
variant: 'outline' | 'stylish'
}) {
const [isHovering, setIsHovering] = useState(false)
const { setResolution, setScaleFactor, domResolution } = useResizeCanvas()
const { images, setImages, initialImageUploaded } = useImageOptions()
const { selectedImage } = useSelectedLayers()
const [domWidth]: number[] = domResolution.split('x').map(Number)
const handleMouseOver = () => {
setIsHovering(true)
}
const handleMouseOut = () => {
setIsHovering(false)
}
const calculateCanvasSize = (
imgWidth: number,
imgHeight: number,
padding: number
) => {
const aspectRatio = imgWidth / imgHeight
let canvasWidth, canvasHeight
if (aspectRatio > 1) {
canvasWidth = imgWidth + 2 * padding
canvasHeight = canvasWidth / aspectRatio
} else {
canvasHeight = imgHeight + 2 * padding
canvasWidth = canvasHeight * aspectRatio
}
return `${canvasWidth}x${canvasHeight}`
}
if (name === 'Fit') {
return (
<Button
name="Fit"
size="sm"
className={cn('flex items-center gap-2 rounded-lg', className)}
variant={variant}
onClick={() => {
if (images.length === 0) return
const padding = 200
const img = new Image()
img.src = images[0].image
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const newResolution = calculateCanvasSize(
naturalWidth,
naturalHeight,
padding
)
setResolution(newResolution.toString())
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
imageSize: '0.75',
},
}
: image
)
)
}
}}
aria-label={name}
>
Fit image
</Button>
)
}
if (name === 'Equal padding') {
return (
<Button
name="Equal padding"
size="sm"
className={cn('flex items-center gap-2 rounded-lg', className)}
variant={variant}
onClick={() => {
if (images.length === 0) return
const padding = 250
const img = new Image()
img.src = images[0].image
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const newResolution = calculateEqualCanvasSize(
naturalWidth,
naturalHeight,
padding
)
setResolution(newResolution.toString())
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
imageSize: '0.75',
},
}
: image
)
)
}
}}
aria-label={name}
>
Equal padding
</Button>
)
}
return (
<Popover>
<PopoverTrigger asChild>
<Button
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
className={cn(
'flex items-center gap-1.5 rounded-lg transition-colors',
className
)}
style={{
backgroundColor: isHovering ? `${color}1A` : '',
color: isHovering ? `${color}` : '',
borderColor: isHovering ? `${color}33` : '',
}}
variant={variant}
size="sm"
>
{icon && <div>{icon}</div>}
<div className="sr-only">{name}</div>
<ChevronDown size={18} className="translate-y-[1.5px] text-inherit" />
</Button>
</PopoverTrigger>
<PopoverContent
avoidCollisions
className="grid w-[220px] grid-cols-1 gap-3"
>
<PopoverArrow
width={14}
height={7}
className="stroke fill-[#1A1C1F] stroke-border"
/>
{resolutions?.map((res: { resolution: string; preset: string }) => {
return (
<Button
onClick={() => {
const [outputWidth]: number[] = res.resolution
.split('x')
.map(Number)
if (!initialImageUploaded) return
setResolution(res.resolution)
setScaleFactor(outputWidth / domWidth)
}}
variant="stylish"
size={'sm'}
style={{
backgroundColor: `${color}1A`,
color: `${color}`,
borderColor: `${color}33`,
}}
key={res.resolution}
>
<p>{`${res.preset}`}</p>
<p>({res.resolution})</p>
</Button>
)
})}
</PopoverContent>
</Popover>
)
}
================================================
FILE: components/editor/frame-options/additional-frame-options.tsx
================================================
import PopupColorPicker from '@/components/popup-color-picker'
import { Switch } from '@/components/ui/switch'
import { useFrameOptions } from '@/store/use-frame-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { Settings2 } from 'lucide-react'
export default function AdditionalFrameOptions() {
const {
setShowSearchBar,
setShowStroke,
macOsDarkColor,
setMacOsDarkColor,
setMacOsLightColor,
macOsLightColor,
setArcDarkMode,
showStroke,
arcDarkMode,
hideButtons,
setHideButtons,
hasButtonColor,
setHasButtonColor,
} = useFrameOptions()
const { selectedImage } = useSelectedLayers()
const { images } = useImageOptions()
const browserFrame = selectedImage ? images[selectedImage - 1]?.frame : 'None'
if (browserFrame !== 'None')
return (
<div
className={`${selectedImage ? '' : 'pointer-events-none opacity-40'} `}
>
<h3 className="mb-6 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<Settings2 size={20} />
<span>Additional options</span>
</h3>
{(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
<div className="mb-6 flex items-center justify-between gap-4 px-1">
<h1 className="text-[0.85rem]">Show searchbar :</h1>
<Switch
defaultChecked={false}
onCheckedChange={(checked) => {
setShowSearchBar(checked)
}}
/>
</div>
)}
{(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
<div className="mb-6 flex items-center justify-between gap-4 px-1">
<h1 className="text-[0.85rem]">Colorful buttons :</h1>
<Switch
defaultChecked={true}
checked={hasButtonColor}
onCheckedChange={(checked) => {
setHasButtonColor(checked)
}}
/>
</div>
)}
{(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
<div className="mb-6 flex items-center justify-between gap-4 px-1">
<h1 className="text-[0.85rem]">Hide buttons :</h1>
<Switch
defaultChecked={false}
checked={hideButtons}
onCheckedChange={(checked) => {
setHideButtons(checked)
}}
/>
</div>
)}
{(browserFrame === 'MacOS Dark' || browserFrame === 'MacOS Light') && (
<div className="mb-6 flex items-center justify-between gap-4 px-1">
<h1 className="text-[0.85rem]">Frame color :</h1>
<PopupColorPicker
shouldShowDropdown={false}
shouldShowAlpha={true}
color={
browserFrame === 'MacOS Dark' ? macOsDarkColor : macOsLightColor
}
onChange={(color) => {
if (browserFrame === 'MacOS Dark') {
setMacOsDarkColor(color)
} else {
setMacOsLightColor(color)
}
}}
/>
</div>
)}
{browserFrame === 'Shadow' && (
<div className="mb-6 flex items-center gap-4 px-1 md:max-w-full">
<h1 className="text-[0.85rem]">Show outline :</h1>
<Switch
defaultChecked={true}
checked={showStroke}
onCheckedChange={(checked) => {
setShowStroke(checked)
}}
/>
</div>
)}
{browserFrame === 'Arc' && (
<div className="mb-6 flex items-center gap-4 px-1 md:max-w-full">
<h1 className="text-[0.85rem]">Dark mode :</h1>
<Switch
defaultChecked={false}
checked={arcDarkMode}
onCheckedChange={(checked) => {
setArcDarkMode(checked)
}}
/>
</div>
)}
</div>
)
}
================================================
FILE: components/editor/frame-options/frame-picker.tsx
================================================
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { FrameTypes, useFrameOptions } from '@/store/use-frame-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import { cn } from '@/utils/button-utils'
export default function FramePicker() {
const { setFrameHeight, frameHeight } = useFrameOptions()
const { selectedImage } = useSelectedLayers()
const { setImages, images } = useImageOptions()
const { setShowControls } = useMoveable()
const frameChangeHandler = (frame: FrameTypes) => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
frame,
style: {
...image.style,
imageRoundness:
frame === 'None' ? 0.4 : frame === 'Arc' ? 1.5 : 0.7,
},
}
: image
)
)
setShowControls(false)
}
return (
<>
<div
className={`mb-3 mt-4 flex items-center px-1 md:max-w-full
${selectedImage ? '' : 'pointer-events-none opacity-40'}
`}
>
<h1 className="text-[0.85rem]">Browser frames:</h1>
</div>
<div className="mt-2 grid w-full grid-cols-3 flex-wrap gap-x-2.5 gap-y-6">
<FrameContainer
text="None"
onClick={() => {
frameChangeHandler('None')
}}
>
<div className="flex h-full w-full flex-col justify-center overflow-hidden rounded-sm">
<div className="w-full flex-1 bg-primary/80" />
</div>
</FrameContainer>
<FrameContainer
text="MacOS Dark"
onClick={() => {
frameChangeHandler('MacOS Dark')
}}
>
<div className="flex h-full w-full flex-col justify-center overflow-hidden rounded-sm">
<div className="flex w-full basis-[30%] bg-[#454545] shadow-sm">
<div className={`flex-center basis-[50%] gap-0.5 `}>
<div className="h-1 w-1 rounded-full bg-[#f7645ccc]" />
<div className="h-1 w-1 rounded-full bg-[#fbc341d2]" />
<div className="h-1 w-1 rounded-full bg-[#3cc84ac5]" />
</div>
</div>
<div className="w-full flex-1 bg-primary/80" />
</div>
</FrameContainer>
<FrameContainer
text="MacOS Light"
onClick={() => {
frameChangeHandler('MacOS Light')
}}
>
<div className="flex h-full w-full flex-col justify-center overflow-hidden rounded-sm">
<div className="flex w-full basis-[30%] bg-[#E3E2E3] shadow-sm">
<div className={`flex-center basis-[50%] gap-0.5 `}>
<div className="h-1 w-1 rounded-full bg-[#f7645ccc]" />
<div className="h-1 w-1 rounded-full bg-[#fbc341d2]" />
<div className="h-1 w-1 rounded-full bg-[#3cc84ac5]" />
</div>
</div>
<div className="w-full flex-1 bg-primary/80" />
</div>
</FrameContainer>
<FrameContainer
text="Arc"
onClick={() => {
frameChangeHandler('Arc')
}}
>
<div className="flex-center h-[4.5rem] w-24 flex-col rounded-sm border border-[#fff]/20 bg-[#fff]/20 p-1 shadow-xl">
<div className="h-full w-full rounded-[2px] bg-primary shadow-sm" />
</div>
</FrameContainer>
<FrameContainer
text="Shadow"
onClick={() => {
frameChangeHandler('Shadow')
}}
className="translate-y-2"
>
<div className="flex-center h-[4.5rem] w-24 flex-col rounded-sm">
<div className="h-full w-full rounded-[2px] bg-primary/80" />
</div>
</FrameContainer>
</div>
{selectedImage &&
images[selectedImage - 1]?.frame !== 'Shadow' &&
images[selectedImage - 1]?.frame !== 'None' && (
<div
className={`mt-8 flex flex-col gap-3 px-1 md:max-w-full ${
selectedImage ? '' : 'pointer-events-none opacity-40'
}`}
>
<h1 className="text-[0.85rem]">Frame size</h1>
<Select
defaultValue={frameHeight}
onValueChange={(value) => setFrameHeight(value)}
>
<SelectTrigger className="w-[7rem]">
<SelectValue placeholder="Medium" />
</SelectTrigger>
<SelectContent className="w-[7rem]">
<SelectItem value="small">Small</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="large">Large</SelectItem>
</SelectContent>
</Select>
</div>
)}
</>
)
}
export function FrameContainer({
children,
text,
onClick,
className,
}: {
children: React.ReactNode
text: FrameTypes
onClick?: () => void
className?: string
}) {
const { selectedImage } = useSelectedLayers()
const { images } = useImageOptions()
return (
<div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
<button
onClick={onClick}
className={`relative h-[3.55rem] w-[4.6rem] overflow-hidden whitespace-nowrap rounded-lg border border-border/80 bg-gray-300 ring-offset-background transition-colors focus:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ${
selectedImage && images[selectedImage - 1]?.frame === text
? 'ring-2 ring-ring ring-offset-2'
: ''
}`}
>
<div
className={cn(
'absolute bottom-0 h-2/3 w-2/3 translate-x-1/2 translate-y-1 scale-150 overflow-hidden rounded-sm',
className
)}
style={{
boxShadow: `0px 10px 40px #000${
text === 'Shadow' ? ',-4px -3.5px rgba(0,0,0,0.8)' : ''
}`,
}}
>
{children}
</div>
</button>
<p className="mt-2 text-center text-[0.75rem] font-medium text-dark">
{text.replace('MacOS', 'Mac')}
</p>
</div>
)
}
================================================
FILE: components/editor/frame-options/index.tsx
================================================
import RoundnessOption from '../image-options/roundness-option'
import FramePicker from './frame-picker'
import AdditionalFrameOptions from './additional-frame-options'
import { Separator } from '@/components/ui/separator'
export default function FrameOptions() {
return (
<>
<FramePicker />
<RoundnessOption />
<Separator className="mt-8 h-[0.1rem] w-full" />
<AdditionalFrameOptions />
</>
)
}
================================================
FILE: components/editor/image-context-menu.tsx
================================================
import { Button } from '@/components/ui/button'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { useColorExtractor } from '@/store/use-color-extractor'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import {
BringToFront,
CropIcon,
ImagePlus,
SendToBack,
Trash,
Wand,
} from 'lucide-react'
import dynamic from 'next/dynamic'
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { type Crop } from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
import Loader from '../loader'
const DynamicCropComponent = dynamic(() =>
import('react-image-crop').then((mod) => mod.ReactCrop)
)
export default function ContextMenuImage({
children,
}: {
children: React.ReactNode
}) {
const [crop, setCrop] = useState<Crop>({
unit: '%', // Can be 'px' or '%'
x: 25,
y: 25,
width: 50,
height: 50,
})
const imgRef = useRef<HTMLImageElement>(null)
const { setImages, images } = useImageOptions()
const [isRemovingBackground, setIsRemovingBackground] = useState(false)
const [isBgRemovalDialogOpen, setIsBgRemovalDialogOpen] = useState(false)
const [isProcessingBackground, setIsProcessingBackground] = useState(false)
const [bgRemovalError, setBgRemovalError] = useState<string | null>(null)
const [processedImageUrl, setProcessedImageUrl] = useState<string | null>(
null
)
const { selectedImage, setSelectedImage, setEnableCrop, enableCrop } =
useSelectedLayers()
const { showControls, setShowControls } = useMoveable()
const workerRef = useRef<Worker | null>(null)
const handleImageDelete = (id: number) => {
if (images.length === 1) {
setImages([])
return
}
if (selectedImage) {
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
image: '',
}
: image
)
)
}
setSelectedImage(null)
}
const bringToFrontOrBack = (direction: 'front' | 'back') => {
if (selectedImage) {
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
zIndex:
direction === 'front'
? image.style.zIndex + 1
: image.style.zIndex - 1,
},
}
: image
)
)
}
}
useEffect(() => {
// Terminate existing worker if component re-renders or dependencies change
workerRef.current?.terminate()
if (isBgRemovalDialogOpen && selectedImage) {
const currentImage = images[selectedImage - 1]
if (!currentImage || !currentImage.image) return
setProcessedImageUrl(null) // Reset image URL first
setBgRemovalError(null) // Reset error
setIsProcessingBackground(true)
// Create and configure the worker
workerRef.current = new Worker(
new URL('@/workers/background-removal.worker.ts', import.meta.url)
)
workerRef.current.onmessage = (
event: MessageEvent<
{ type: 'success'; url: string } | { type: 'error'; error: string }
>
) => {
if (event.data.type === 'success') {
setProcessedImageUrl(event.data.url)
setBgRemovalError(null)
} else if (event.data.type === 'error') {
console.error('Background removal worker error:', event.data.error)
setBgRemovalError(event.data.error)
// Keep dialog open to show error or close?
// setIsBgRemovalDialogOpen(false);
}
setIsProcessingBackground(false)
}
workerRef.current.onerror = (error) => {
console.error('Unhandled worker error:', error)
setBgRemovalError('An unexpected worker error occurred.')
setIsProcessingBackground(false)
// setIsBgRemovalDialogOpen(false);
}
// Send image source to worker
workerRef.current.postMessage({ src: currentImage.image })
} else {
// If dialog is closed, ensure worker is terminated
workerRef.current?.terminate()
workerRef.current = null
}
// Cleanup function to terminate worker on unmount or when dialog closes
return () => {
workerRef.current?.terminate()
workerRef.current = null
}
// Rerun effect when dialog opens/closes or selected image changes
}, [isBgRemovalDialogOpen, selectedImage, images]) // Added images dependency
useHotkeys(['Delete', 'Backspace'], () => {
if (selectedImage)
if (showControls) {
handleImageDelete(selectedImage)
setShowControls(false)
setSelectedImage(null)
}
})
const cropImageNow = () => {
const canvas = document.createElement('canvas')
const image = imgRef.current
if (!image) return
const scaleX = image.naturalWidth / image.width
const scaleY = image.naturalHeight / image.height
canvas.width = crop.width
canvas.height = crop.height
const ctx: any = canvas.getContext('2d')
const pixelRatio = window.devicePixelRatio
canvas.width = crop.width * pixelRatio * scaleX
canvas.height = crop.height * pixelRatio * scaleY
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
ctx.imageSmoothingQuality = 'high'
ctx.drawImage(
image,
crop.x * scaleX,
crop.y * scaleY,
crop.width * scaleX,
crop.height * scaleY,
0,
0,
crop.width * scaleX,
crop.height * scaleY
)
const base64Image = canvas.toDataURL('image/png')
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
image: base64Image,
}
: image
)
)
}
return (
<Dialog
open={enableCrop}
onOpenChange={(open) => {
if (open === false) setEnableCrop(false)
setEnableCrop(open)
}}
>
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuItem
inset
onClick={() => {
bringToFrontOrBack('back')
}}
// disabled={
// !selectedImage || images[selectedImage - 1].style.zIndex === 2
// }
>
Send back
<ContextMenuShortcut>
<BringToFront size={19} className="opacity-80" />
</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem
inset
onClick={() => {
bringToFrontOrBack('front')
}}
>
Bring forward
<ContextMenuShortcut>
<SendToBack size={19} className="opacity-80" />
</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
<ReplaceImage />
<div className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
<div
onClick={() => setIsBgRemovalDialogOpen(true)}
className="ml-6 cursor-pointer"
>
Remove background
</div>
<span className="ml-auto text-xs tracking-widest text-muted-foreground">
<Wand size={19} className="opacity-80" />
</span>
</div>
<DialogTrigger asChild>
<div className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
<div className="ml-6 cursor-pointer">Crop</div>
<span className="ml-auto text-xs tracking-widest text-muted-foreground">
<CropIcon size={19} className="opacity-80" />
</span>
</div>
</DialogTrigger>
{/* <ContextMenuSub>
<ContextMenuSubTrigger inset>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
</ContextMenuSubContent>
</ContextMenuSub> */}
<ContextMenuSeparator />
<ContextMenuItem
inset
onClick={() => {
selectedImage && handleImageDelete(selectedImage)
}}
className="text-[#F46567]/70 focus:text-[#f46567]/80"
>
Delete
<ContextMenuShortcut>
<Trash size={19} className="text-[#F46567]/70 opacity-80" />
</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<DialogContent className="flex h-fit max-h-[95vh] w-1/2 flex-col gap-4">
<DialogHeader className="mb-4">
<DialogTitle>Crop image</DialogTitle>
</DialogHeader>
<div className="mb-4 h-full w-full flex-1 overflow-hidden overflow-y-auto">
{selectedImage && (
<DynamicCropComponent
crop={crop}
onChange={(c) => setCrop(c)}
disabled={!enableCrop || !selectedImage}
onComplete={(c) => {
console.log(c)
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={imgRef}
src={images[selectedImage - 1].image}
alt="Crop selected image"
className="h-full w-full object-cover"
/>
</DynamicCropComponent>
)}
</div>
<DialogFooter className="mt-auto flex items-center gap-1.5">
<Button
variant="outline"
onClick={() => {
setEnableCrop(false)
}}
>
Cancel
</Button>
<Button
onClick={() => {
setEnableCrop(false)
cropImageNow()
}}
className="flex-center gap-1.5"
>
<span>Done</span>
<CropIcon size={19} className="opacity-80" />
</Button>
</DialogFooter>
</DialogContent>
<Dialog
open={isBgRemovalDialogOpen}
onOpenChange={setIsBgRemovalDialogOpen}
>
<DialogContent className="flex h-fit max-h-[95vh] w-1/2 flex-col gap-4">
<DialogHeader>
<DialogTitle className="mb-4 flex items-center gap-1.5">
<Wand size={19} className="opacity-80" />
<span>Remove Background</span>
</DialogTitle>
</DialogHeader>
<div
className="relative mb-4 flex h-full w-full flex-1 items-center justify-center overflow-hidden overflow-y-auto"
style={{
// Apply checkered background only when done processing, successful, and image is ready
backgroundImage:
!isProcessingBackground && processedImageUrl && !bgRemovalError
? 'linear-gradient(45deg, rgba(204,204,204,0.05) 25%, transparent 25%), linear-gradient(-45deg, rgba(204,204,204,0.05) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(204,204,204,0.05) 75%), linear-gradient(-45deg, transparent 75%, rgba(204,204,204,0.05) 75%)'
: 'none',
backgroundSize:
!isProcessingBackground && processedImageUrl && !bgRemovalError
? '20px 20px'
: 'auto',
backgroundPosition:
!isProcessingBackground && processedImageUrl && !bgRemovalError
? '0 0, 0 10px, 10px -10px, -10px 0px'
: 'initial',
// Keep a fallback bg or use theme background
backgroundColor:
!isProcessingBackground && processedImageUrl && !bgRemovalError
? 'hsl(var(--background))' // Use theme background
: 'hsl(var(--muted) / 0.1)', // Original dim background
}}
>
{isProcessingBackground && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-black/60 ">
<Loader />
</div>
)}
{/* Display Error Message */}
{!isProcessingBackground && bgRemovalError && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-destructive/60 p-4 text-center">
<p className="font-semibold text-white">
Error Removing Background
</p>
<p className="mt-1 text-xs text-white/80">{bgRemovalError}</p>
</div>
)}
{selectedImage && !processedImageUrl && (
<img
src={images[selectedImage - 1]?.image}
alt="Original image"
className="h-full w-full object-cover"
// Dim image slightly if there's an error overlay
style={{ opacity: bgRemovalError ? 0.5 : 1 }}
/>
)}
{processedImageUrl && (
<img
src={processedImageUrl}
alt="Image with background removed"
className="h-full w-full object-cover" // Show processed image even if error occurred previously
/>
)}
</div>
<DialogFooter className="mt-auto flex items-center gap-1.5">
<Button
variant="outline"
onClick={() => {
setIsBgRemovalDialogOpen(false)
setProcessedImageUrl(null)
setBgRemovalError(null)
setIsProcessingBackground(false) // Ensure loading state is reset
}}
>
Cancel
</Button>
<Button
onClick={() => {
if (selectedImage && processedImageUrl) {
setImages(
images.map((img, index) =>
index === selectedImage - 1
? {
...img,
image: processedImageUrl,
style: {
...img.style,
shadowName: 'None',
imageShadow: '0 0 0 0',
},
}
: img
)
)
}
setIsBgRemovalDialogOpen(false)
setProcessedImageUrl(null)
}}
disabled={
isProcessingBackground || !processedImageUrl || !!bgRemovalError
} // Disable if processing, no result, or error
className="flex-center gap-1.5"
>
Done
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
)
}
function ReplaceImage() {
const { setImages, images } = useImageOptions()
const { selectedImage, setSelectedImage } = useSelectedLayers()
const { setImagesCheck, imagesCheck } = useColorExtractor()
const onDrop = async (file: any) => {
const analyze = (await import('rgbaster')).default
if (file) {
const imageUrl = URL.createObjectURL(file)
const result = await analyze(imageUrl, {
scale: 0.3,
})
const extractedColors = result.slice(0, 12)
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
image: imageUrl,
colors: extractedColors,
}
: image
)
)
setImagesCheck([...imagesCheck, imageUrl])
}
}
return (
<div className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
<label className="ml-6" htmlFor="file-replace">
Replace image
</label>
<input
id="file-replace"
name="file-replace"
type="file"
onChange={(e: ChangeEvent<HTMLInputElement>) => {
onDrop(e.target.files?.[0])
}}
accept="image/*"
className="sr-only"
/>
<span className="ml-auto text-xs tracking-widest text-muted-foreground">
<ImagePlus size={19} className="opacity-80" />
</span>
</div>
)
}
================================================
FILE: components/editor/image-options/add-image-button.tsx
================================================
import { ChangeEvent, useRef, useState } from 'react'
import { X, Plus, Upload } from 'lucide-react'
import { useImageOptions } from '@/store/use-image-options'
import { useColorExtractor } from '@/store/use-color-extractor'
import { calculateEqualCanvasSize } from '@/utils/helper-fns'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import Dropzone from 'react-dropzone'
type AddImageButtonProps = {}
export default function AddImageButton({}: AddImageButtonProps) {
const { setImages, images, defaultStyle } = useImageOptions()
const { imagesCheck, setImagesCheck } = useColorExtractor()
const uploadRef = useRef<HTMLInputElement>(null)
const { automaticResolution, setResolution } = useResizeCanvas()
const [isDragging, setIsDragging] = useState<boolean>(false)
const handleImageUpload = (file: File) => {
const imageUrl = URL.createObjectURL(file)
setImages([
...images,
{
image: imageUrl,
id: images.length + 1,
style:
images.length < 1
? defaultStyle
: {
...defaultStyle,
imageSize: '0.5',
},
},
])
setImagesCheck([...imagesCheck, imageUrl])
if (images.length > 0) return
if (automaticResolution) {
const padding = 200
const img = new Image()
img.src = imageUrl
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const newResolution = calculateEqualCanvasSize(
naturalWidth,
naturalHeight,
padding
)
setResolution(newResolution.toString())
}
}
}
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
handleImageUpload(file)
}
}
const handleDrop = (acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
handleImageUpload(acceptedFiles[0])
}
setIsDragging(false)
}
return (
<div className="mb-4 mt-2 h-[8rem] w-full px-1 text-sm">
<Dropzone
multiple={false}
onDrop={handleDrop}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
accept={{ 'image/*': [] }}
noClick
>
{({ getRootProps, getInputProps, open }) => (
<div
{...getRootProps()}
className={`relative flex h-full w-full flex-col rounded-xl border-2 p-1 transition-all duration-300 ${
isDragging
? 'scale-[1.02] border-[#898aeb] bg-[#898aeb]/10'
: 'border-[#898aeb]/20 hover:border-[#898aeb]/60'
}`}
>
<div
className="group relative flex h-full w-full cursor-pointer items-center justify-center overflow-hidden rounded-lg"
onClick={open}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
open()
}
}}
>
{isDragging ? (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-gradient-to-br from-[#898aeb]/20 to-[#d8b9e3]/20">
<div className="flex flex-col items-center">
<Upload
className="mb-2 animate-bounce text-[#898aeb]"
size={24}
/>
<span className="text-sm font-medium text-[#898aeb]">
Drop here
</span>
</div>
</div>
) : (
<div className="flex flex-col items-center">
<Plus
className="mb-2 cursor-pointer text-purple/60 transition-transform focus:ring-1 group-hover:scale-110 group-hover:text-purple/80"
size={26}
/>
<span className="text-sm font-medium text-purple/60">
Click or drag image
</span>
</div>
)}
</div>
<input
{...getInputProps()}
/>
</div>
)}
</Dropzone>
</div>
)
}
================================================
FILE: components/editor/image-options/index.tsx
================================================
'use client'
import { Focus, GalleryVerticalEnd } from 'lucide-react'
import SizeOption from './scale-options'
import RoundnessOption from './roundness-option'
import InsetOption from './inset-option'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { useImageOptions } from '@/store/use-image-options'
import AddImageButton from './add-image-button'
import ShadowSettings from './shadow-settings'
export default function ImageOptions() {
const { accordionOpen, setAccordionOpen } = useImageOptions()
return (
<>
<AddImageButton />
<Accordion
type="single"
collapsible
defaultValue={accordionOpen.appearanceOpen ? 'appearance' : ''}
className="mt-4 w-full"
>
<AccordionItem value="appearance">
<AccordionTrigger
onClick={() =>
setAccordionOpen({
...accordionOpen,
appearanceOpen: !accordionOpen.appearanceOpen,
})
}
>
<h3 className="flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<Focus size={20} />
<span>Appearance</span>
</h3>
</AccordionTrigger>
<AccordionContent>
<SizeOption />
<InsetOption />
<RoundnessOption />
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion
type="single"
collapsible
defaultValue={accordionOpen.shadowOpen ? 'shadow' : ''}
className="mt-2 w-full"
>
<AccordionItem value="shadow">
<AccordionTrigger
onClick={() =>
setAccordionOpen({
...accordionOpen,
shadowOpen: !accordionOpen.shadowOpen,
})
}
>
<h3 className="flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<GalleryVerticalEnd size={20} className="rotate-90" />
<span>Shadow</span>
</h3>
</AccordionTrigger>
<AccordionContent>
<ShadowSettings />
</AccordionContent>
</AccordionItem>
</Accordion>
</>
)
}
================================================
FILE: components/editor/image-options/inset-option.tsx
================================================
'use client'
import { Slider } from '@/components/ui/slider'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
export default function InsetOption() {
const { images, updateImageStyle, getImage } = useImageOptions()
const { setShowControls, showControls } = useMoveable()
const { selectedImage } = useSelectedLayers()
return (
<div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
<div className="mb-3 mt-6 flex items-center px-1 md:max-w-full">
<h1 className="text-[0.85rem]">Inset</h1>
{images.length !== 0 && (
<Popover>
<PopoverTrigger asChild>
<Button
className="ml-2 h-5 w-5 rounded-md"
style={{
backgroundColor: selectedImage
? getImage(selectedImage)?.style.insetColor
: '#fff',
}}
variant="outline"
/>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-[250px] rounded-lg bg-formDark p-4"
>
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="pb-1 text-sm font-medium tracking-tight">
Detected colors
</h4>
<hr className="border-border pt-2" />
<div className="flex flex-wrap gap-2">
{images[selectedImage! - 1]?.extractedColors?.map(
(color) => (
<button
key={color.color}
className={`h-7 w-7 rounded-sm ${
images[selectedImage! - 1]?.style.insetColor ===
color.color
? 'ring-2 ring-ring ring-offset-2'
: ''
}`}
style={{ backgroundColor: color.color }}
onClick={() => {
selectedImage &&
updateImageStyle(selectedImage, {
insetColor: color.color,
})
}}
/>
)
)}
</div>
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
<div className="flex gap-4 px-1 text-[0.85rem] md:max-w-full">
<Slider
defaultValue={[0]}
max={150}
min={0}
step={0.02}
onValueChange={(value: number[]) => {
setShowControls(false)
selectedImage &&
updateImageStyle(selectedImage, {
insetSize: value[0].toString(),
})
}}
value={
images.length !== 0 && selectedImage
? [+(getImage(selectedImage)?.style.insetSize ?? 0)]
: [10]
}
onValueCommit={() => setShowControls(true)}
onIncrement={() => {
setShowControls(false)
selectedImage &&
updateImageStyle(selectedImage, {
insetSize:
+(getImage(selectedImage)?.style.insetSize ?? 0) <= 149
? (
+(getImage(selectedImage)?.style.insetSize ?? 0) + 4
).toString()
: '150',
})
}}
onDecrement={() => {
setShowControls(false)
selectedImage &&
updateImageStyle(selectedImage, {
insetSize:
+(getImage(selectedImage)?.style.insetSize ?? 0) >= 0
? (
+(getImage(selectedImage)?.style.insetSize ?? 0) - 4
).toString()
: '0',
})
}}
/>
</div>
</div>
)
}
================================================
FILE: components/editor/image-options/roundness-option.tsx
================================================
'use client'
import { Slider } from '@/components/ui/slider'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
export default function RoundnessOption() {
const { images, updateImageStyle, getImage } = useImageOptions()
const { setShowControls } = useMoveable()
const { selectedImage } = useSelectedLayers()
const browserFrame = selectedImage ? getImage(selectedImage)?.frame : 'None'
return (
<div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
<div className="mb-3 mt-6 flex items-center px-1 md:max-w-full">
<h1 className="text-[0.85rem]">Roundness</h1>
<p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
{`${Math.round(
Number(
selectedImage
? getImage(selectedImage)?.style.imageRoundness
: 0.2
) * 10
)} `}
</p>
</div>
<div className="flex gap-4 px-1 text-[0.85rem] md:max-w-full">
<Slider
defaultValue={[0.7]}
max={browserFrame !== 'None' && browserFrame !== 'Arc' ? 1.6 : 5}
min={0}
step={0.05}
onValueChange={(value) => {
setShowControls(false)
selectedImage &&
updateImageStyle(selectedImage, { imageRoundness: value[0] })
}}
value={
images.length !== 0 && selectedImage
? [+(getImage(selectedImage)?.style.imageRoundness ?? 1)]
: [1]
}
onValueCommit={() => setShowControls(true)}
onIncrement={() => {
if (images.length === 0 || !selectedImage) return
if (Number(getImage(selectedImage)?.style.imageRoundness) >= 5)
return
setShowControls(false)
updateImageStyle(selectedImage, {
imageRoundness:
Number(getImage(selectedImage)?.style.imageRoundness ?? 0) +
0.1,
})
}}
onDecrement={() => {
if (images.length === 0 || !selectedImage) return
if (Number(getImage(selectedImage)?.style.imageRoundness) <= 0)
return
setShowControls(false)
updateImageStyle(selectedImage, {
imageRoundness:
Number(getImage(selectedImage)?.style.imageRoundness ?? 0) -
0.1,
})
}}
/>
</div>
</div>
)
}
================================================
FILE: components/editor/image-options/scale-options.tsx
================================================
import { Slider } from '@/components/ui/slider'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
type SizeOptionProps = {
text?: string
}
export default function SizeOption({ text = 'Scale' }: SizeOptionProps) {
const { images, setImages, scale, setScale } = useImageOptions()
const { selectedImage } = useSelectedLayers()
const { setShowControls } = useMoveable()
return (
<>
<div className="mb-3 mt-2 flex items-center px-1 md:md:max-w-full">
<h1 className="text-[0.85rem]">{text}</h1>
<p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
{Math.round(scale * 100)}%
</p>
</div>
<div className="flex gap-4 px-1 text-[0.85rem] md:md:max-w-full">
<Slider
defaultValue={[1]}
max={3}
min={0.25}
step={0.01}
onValueChange={(value: number[]) => {
setShowControls(false)
setScale(value[0])
}}
onValueCommit={() => setShowControls(true)}
value={[scale ?? 1]}
onIncrement={() => {
if (scale >= 3) return
setScale(scale + 0.05)
}}
onDecrement={() => {
if (scale <= 0.25) return
setScale(scale - 0.05)
}}
/>
</div>
</>
)
}
================================================
FILE: components/editor/image-options/shadow-settings.tsx
================================================
'use client'
import PopupColorPicker from '@/components/popup-color-picker'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { shadows } from '@/utils/presets/shadows'
import { ChevronDown } from 'lucide-react'
import { Slider } from '@/components/ui/slider'
import { useMoveable } from '@/store/use-moveable'
export default function ShadowSettings() {
const { images, setImages, defaultStyle } = useImageOptions()
const { showControls } = useMoveable()
const { selectedImage } = useSelectedLayers()
const { backgroundType } = useBackgroundOptions()
const boxShadowStyle = {
boxShadow: selectedImage
? images[selectedImage - 1]?.style.imageShadow
: '',
}
const boxShadowPreview = {
boxShadow: selectedImage
? images[selectedImage - 1]?.style.shadowPreview
: '',
}
const backgroundStyle = {
backgroundImage: `var(--gradient-bg)`,
backgroundColor:
backgroundType === 'mesh' ? `var(--mesh-bg)` : 'var(--solid-bg)',
}
const handleShadowButtonClick = (shadow: {
shadow: string
fullName: string
preview: string
}) => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
imageShadow: shadow.shadow,
shadowName: shadow.fullName,
shadowPreview: shadow.preview,
},
}
: image
)
)
}
const handleColorChange = (color: string) => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
shadowColor: color,
imageShadow:
shadows.find(
(shadow) =>
shadow.fullName ===
(images[selectedImage - 1]?.style.shadowName ?? '')
)?.shadow ?? '',
},
}
: image
)
)
}
return (
<div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
<Popover>
<PopoverTrigger className="relative mt-2 flex h-14 w-full items-center overflow-hidden rounded-lg border border-border/80 bg-[#898beb05]">
<div
style={backgroundStyle}
className="flex-center h-full basis-[23%]"
>
<div
className="flex-center h-1/2 w-1/2 rounded-md bg-white"
style={boxShadowPreview}
></div>
</div>
<div className="flex h-full w-full flex-1 items-center justify-between px-4">
<div className="flex w-full flex-col items-start">
<p className="text-[0.85rem] font-medium text-dark/70">
{selectedImage
? images[selectedImage - 1]?.style.shadowName
: 'None'}
</p>
<p className="text-[0.7rem] font-bold text-dark/50">
{selectedImage
? images[selectedImage - 1]?.style.shadowColor.slice(0, 7)
: '#000'}
</p>
</div>
<ChevronDown size={18} className="text-dark/80" />
</div>
</PopoverTrigger>
<PopoverContent
align="start"
className="grid w-[350px] grid-cols-3 gap-4 rounded-lg bg-formDark p-4"
>
{/* Inside popup */}
{shadows.map((shadow) => (
<Button
variant="secondary"
key={shadow.name}
onClick={() => {
handleShadowButtonClick(shadow)
}}
className={`flex-center relative h-20 w-24 cursor-pointer rounded-md ${
shadow.shadow ===
images[selectedImage! - 1]?.style.imageShadow &&
'outline-none ring-2 ring-ring ring-offset-2'
}`}
style={backgroundStyle}
>
<div
className="flex-center h-[75%] w-[95%] rounded-md bg-white text-xs text-[#333]"
style={{ boxShadow: `${shadow.preview}` }}
>
{shadow.name}
</div>
</Button>
))}
</PopoverContent>
</Popover>
<div className="mb-3 mt-8 flex items-center px-1">
<h1 className="text-[0.85rem]">Opacity</h1>
<p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
{Math.round(
Number(
selectedImage
? images[selectedImage - 1]?.style.shadowOpacity ?? 0.5
: 0.5
) * 100
)}
%
</p>
</div>
<div className="flex gap-4 px-1 text-[0.85rem] md:max-w-full">
<Slider
defaultValue={[0.5]}
min={0}
max={1}
step={0.01}
onValueChange={(value) => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
shadowOpacity: value[0],
},
}
: image
)
)
}}
value={
images.length !== 0 && selectedImage
? [+images[selectedImage - 1]?.style.shadowOpacity]
: [1]
}
onIncrement={() => {
if (images.length === 0 || !selectedImage) return
if (Number(images[selectedImage - 1]?.style.shadowOpacity) >= 1)
return
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
shadowOpacity: Number(image.style.shadowOpacity) + 0.01,
},
}
: image
)
)
}}
onDecrement={() => {
if (images.length === 0 || !selectedImage) return
if (Number(images[selectedImage - 1]?.style.shadowOpacity) <= 0)
return
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
shadowOpacity: Number(image.style.shadowOpacity) - 0.01,
},
}
: image
)
)
}}
/>
</div>
<div className="mb-3 mt-8 flex items-center px-1">
<h1 className="text-[0.85rem]">Shadow color</h1>
</div>
<PopupColorPicker
shouldShowAlpha={false}
color={
selectedImage ? images[selectedImage - 1]?.style.shadowColor : '#000'
}
onChange={handleColorChange}
/>
</div>
)
}
================================================
FILE: components/editor/main-image-area.tsx
================================================
/* eslint-disable @next/next/no-img-element */
'use client'
import { useOnClickOutside } from '@/hooks/use-on-click-outside'
import demoImage from '@/public/images/demo-tweet.png'
import { useBackgroundOptions } from '@/store/use-background-options'
import { useColorExtractor } from '@/store/use-color-extractor'
import { useFrameOptions } from '@/store/use-frame-options'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import {
calculateEqualCanvasSize,
convertHexToRgba,
splitWidthHeight,
} from '@/utils/helper-fns'
import { ImageIcon, Upload } from 'lucide-react'
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'
import Dropzone from 'react-dropzone'
import { Button } from '../ui/button'
import BrowserFrame from './browser-frames'
import ContextMenuImage from './image-context-menu'
const ImageUpload = () => {
const targetRef = useRef<HTMLDivElement>(null)
const {
images,
addImage,
updateImage,
updateImageStyle,
setInitialImageUploaded,
initialImageUploaded,
} = useImageOptions()
const { selectedImage, setSelectedImage, setSelectedText } = useSelectedLayers()
const { setShowControls, isSelecting, isMultipleTargetSelected } =
useMoveable()
const { exactDomResolution } = useResizeCanvas()
const { width: exactDomWidth, height: exactDomHeight } =
splitWidthHeight(exactDomResolution)
const { frameHeight, showStroke, arcDarkMode } = useFrameOptions()
const { imagesCheck } = useColorExtractor()
useEffect(() => {
if (images.length === 0) {
return
}
setInitialImageUploaded(true)
const extractColors = async () => {
const analyze = (await import('rgbaster')).default
const result = await analyze(images[images.length - 1].image, {
scale: 0.5,
})
const extractedColors = result.slice(0, 12)
updateImage(images.length, { extractedColors })
updateImageStyle(images.length, {
insetColor: extractedColors[0].color,
})
}
extractColors()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [imagesCheck])
console.log(images)
useOnClickOutside(targetRef, () => {
if (isMultipleTargetSelected) return
// setShowControls(false)
})
// useOnClickOutside(multipleTargetRef, () => {
// setShowControls(false)
// setIsSelecting(false)
// })
// const {
// imageSize,
// imageRoundness,
// imageShadow,
// borderSize,
// borderColor,
// rotate,
// } = images[selectedImage - 1]?.style || {}
return (
<>
{!initialImageUploaded && <LoadAImage />}
{images && (
<>
{images.map((image, index) => {
if (image.image !== '')
return (
<ContextMenuImage key={image.id + index}>
<div
className={`image image-check absolute z-[2] flex-1 overflow-hidden ${
isSelecting ? 'selectable' : ''
} ${selectedImage ? '' : ''}`}
ref={
!isMultipleTargetSelected
? image.id === selectedImage
? targetRef
: null
: targetRef
}
style={{
// transition:
// 'box-shadow 0.8s cubic-bezier(0.6, 0.6, 0, 1)',
transformStyle: 'preserve-3d',
transformOrigin: `50% 50%`,
// rotate: `${image.style.rotate}deg`,
// transform: `scale(${image.style.imageSize}) rotateX(${image.style.rotateX}deg) rotateY(${image.style.rotateY}deg) rotateZ(${image.style.rotateZ}deg) `,
transform: `perspective(${image.style.perspective}px) translate(${image.style.translateX}%, ${image.style.translateY}%) scale(${image.style.imageSize}) rotate(${image.style.rotate}deg) rotateX(${image.style.rotateX}deg) rotateY(${image.style.rotateY}deg) rotateZ(${image.style.rotateZ}deg)`,
borderRadius: `${image.style.imageRoundness}rem`,
boxShadow:
image.style.shadowName !== 'Medium'
? `${image.style.imageShadow} ${convertHexToRgba(
image.style.shadowColor,
image.style.shadowOpacity
)}${
image.frame === 'Shadow'
? ',11px 11px rgba(0,0,0,0.8)'
: ''
}`
: `0px 18px 88px -4px ${convertHexToRgba(
image.style.shadowColor,
image.style.shadowOpacity
)}, 0px 8px 28px -6px ${convertHexToRgba(
image.style.shadowColor,
image.style.shadowOpacity
)}${
image.frame === 'Shadow'
? ',11px 11px rgba(0,0,0,0.8)'
: ''
}`,
padding:
image.frame !== 'None'
? image.frame === 'Arc'
? frameHeight === 'small'
? '10px'
: frameHeight === 'medium'
? '13px'
: '15px'
: ''
: `${image.style.insetSize}px`,
backgroundColor:
image.style.insetSize !== '0' && image.frame === 'None'
? `${image?.style.insetColor}`
: image.frame === 'Arc'
? arcDarkMode
? '#00000050'
: '#ffffff50'
: image.frame === 'Shadow'
? 'rgba(0,0,0,0.8)'
: 'transparent',
border:
image.frame === 'Arc'
? arcDarkMode
? '1px solid #00000020'
: '1px solid #ffffff60'
: image.frame === 'Shadow'
? showStroke
? '3px solid rgba(0,0,0,0.8)'
: ''
: '',
zIndex: `${image.style.zIndex}`,
}}
id={`${image.id}`}
onClick={() => {
setShowControls(true)
setSelectedImage(image.id)
}}
// on right click too do the same
onContextMenu={(e) => {
setShowControls(true)
setSelectedImage(image.id)
}}
>
<BrowserFrame frame={image.frame || 'None'} />
<img
draggable={false}
className={`pointer-events-none h-full w-full shrink-0 ${
image.frame === 'Arc' ? 'shadow-md' : ''
}`}
id={`img-${image.id}`}
src={image.image}
alt="Uploaded image"
style={{
borderRadius:
image.frame !== 'None'
? image.frame === 'Arc'
? `calc(${image.style.imageRoundness}rem - 9px)`
: ''
: `calc(${image.style.imageRoundness}rem - ${image.style.insetSize}px)`,
padding:
image.frame === 'None'
? ''
: `${image.style.insetSize}px`,
backgroundColor:
image.style.insetSize !== '0' &&
image.frame !== 'None'
? `${image?.style.insetColor}`
: '',
}}
/>
</div>
{/* Trying layout feature! */}
{/* <div
className={`flex flex-col image image-check absolute flex-1 z-[2] ${
isSelecting ? 'selectable' : ''
} ${selectedImage ? '' : ''}`}
style={{
// width: '65%',
// maxHeight: '35%',
width: `${+image.style.imageSize * +exactDomWidth}px`,
maxHeight: `${+image.style.imageSize * +exactDomHeight}px`,
// transform: `translate(35%,50%) rotateX(40deg) rotate(50deg) scale(1.3) skew(8deg, 0deg)`,
borderRadius: `${image.style.imageRoundness}rem`,
boxShadow:
image.style.shadowName !== 'Medium'
? `${image.style.imageShadow} ${convertHexToRgba(
image.style.shadowColor,
image.style.shadowOpacity
)}${
image.frame === 'Shadow'
? ',11px 11px rgba(0,0,0,0.8)'
: ''
}`
: `0px 18px 88px -4px ${convertHexToRgba(
image.style.shadowColor,
image.style.shadowOpacity
)}, 0px 8px 28px -6px ${convertHexToRgba(
image.style.shadowColor,
image.style.shadowOpacity
)}${
image.frame === 'Shadow'
? ',11px 11px rgba(0,0,0,0.8)'
: ''
}`,
}}
ref={
!isMultipleTargetSelected
? image.id === selectedImage
? targetRef
: null
: targetRef
}
id={`${image.id}`}
onClick={() => {
setShowControls(true)
setSelectedImage(image.id)
}}
onContextMenu={(e) => {
setShowControls(true)
setSelectedImage(image.id)
}}
>
<BrowserFrame frame={image.frame || 'None'} />
<img
draggable={false}
className={`pointer-events-none h-full w-full shrink-0 object-cover object-center ${
image.frame === 'Arc' ? 'shadow-md' : ''
}`}
id={`img-${image.id}`}
src={image.image}
alt="Uploaded image"
style={{
borderRadius:
image.frame !== 'None'
? image.frame === 'Arc'
? `calc(${image.style.imageRoundness}rem - 9px)`
: ''
: `calc(${image.style.imageRoundness}rem - ${image.style.insetSize}px)`,
padding:
image.frame === 'None'
? ''
: `${image.style.insetSize}px`,
backgroundColor:
image.style.insetSize !== '0' &&
image.frame !== 'None'
? `${image?.style.insetColor}`
: '',
}}
/>
</div> */}
</ContextMenuImage>
)
})}
</>
)}
</>
)
}
export default ImageUpload
function LoadAImage() {
const {
images,
addImage,
updateImage,
defaultStyle,
setInitialImageUploaded,
} = useImageOptions()
const { setSelectedImage } = useSelectedLayers()
const { imagesCheck, setImagesCheck } = useColorExtractor()
const { setResolution, automaticResolution } = useResizeCanvas()
const { setBackground } = useBackgroundOptions()
const [isDragging, setIsDragging] = useState<boolean>(false)
useEffect(() => {
const handlePaste = async (event: ClipboardEvent) => {
const items = event.clipboardData?.items
if (!items) return
const itemsArray = Array.from(items)
for (const item of itemsArray) {
if (item.type.indexOf('image') === 0) {
const file = item.getAsFile()
if (file) {
const imageUrl = URL.createObjectURL(file)
setInitialImageUploaded(true)
setImagesCheck([...imagesCheck, imageUrl])
addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })
setSelectedImage(images.length + 1)
if (images.length === 0 && automaticResolution) {
const padding = 250
const img = new Image()
img.src = imageUrl
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const newResolution = calculateEqualCanvasSize(
naturalWidth,
naturalHeight,
padding
)
setResolution(newResolution.toString())
}
}
}
}
}
}
document.addEventListener('paste', handlePaste)
return () => document.removeEventListener('paste', handlePaste)
}, [images, imagesCheck, addImage, setImagesCheck, setInitialImageUploaded, setSelectedImage, defaultStyle, automaticResolution, setResolution])
const handleImageLoad = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const imageUrl = URL.createObjectURL(file)
setInitialImageUploaded(true)
setImagesCheck([...imagesCheck, imageUrl])
addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })
setSelectedImage(images.length + 1)
if (images.length > 0) return
if (automaticResolution) {
const padding = 200
const img = new Image()
img.src = imageUrl
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const newResolution = calculateEqualCanvasSize(
naturalWidth,
naturalHeight,
padding
)
setResolution(newResolution.toString())
}
}
}
},
[
setInitialImageUploaded,
setImagesCheck,
imagesCheck,
images,
defaultStyle,
setSelectedImage,
automaticResolution,
setResolution,
]
)
const handleImageChange = useCallback(
(file: any) => {
// const file = event.target.files?.[0]
if (file) {
const imageUrl = URL.createObjectURL(file)
setInitialImageUploaded(true)
setImagesCheck([...imagesCheck, imageUrl])
addImage({ image: imageUrl, id: images.length + 1, style: defaultStyle })
setSelectedImage(images.length + 1)
if (images.length > 0) return
if (automaticResolution) {
const padding = 250
const img = new Image()
img.src = imageUrl
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const newResolution = calculateEqualCanvasSize(
naturalWidth,
naturalHeight,
padding
)
setResolution(newResolution.toString())
}
}
}
},
[
setInitialImageUploaded,
setImagesCheck,
imagesCheck,
images,
defaultStyle,
setSelectedImage,
automaticResolution,
setResolution,
]
)
const loadDemoImage = () => {
if (typeof window === 'undefined') return
setBackground('linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)')
document?.documentElement.style.setProperty(
'--gradient-bg',
' linear-gradient(var(--gradient-angle), #898aeb, #d8b9e3)'
)
addImage({
image: demoImage.src,
id: 1,
style: {
...defaultStyle,
borderSize: '15',
imageRoundness: 0.7,
imageSize: '0.78',
insetSize: '10',
},
})
setImagesCheck([...imagesCheck, demoImage.src])
setResolution('1920x1080')
}
return (
<Dropzone
multiple={false}
onDrop={(acceptedFiles) => {
handleImageChange(acceptedFiles[0])
}}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
noClick
>
{({ getRootProps, getInputProps }) => (
<div
{...getRootProps()}
className="h-25 absolute-center w-4/5 xl:w-2/5"
>
<div className="flex flex-col gap-4 rounded-xl text-center md:shadow-2xl">
<div className="flex-center flex-col rounded-xl px-4 py-10 md:bg-[#f1f1f1]">
<Upload
style={{
transition: 'all 0.8s cubic-bezier(0.6, 0.6, 0, 1)',
}}
className={`mx-auto mb-2 hidden h-10 w-10 text-[#332]/80 sm:block ${
isDragging ? 'rotate-180' : 'rotate-0'
}`}
aria-hidden="true"
/>
<div className="flex-center mt-4 text-base leading-6 text-gray-400">
<label
htmlFor="file-upload"
className="focus-within:ring-purple relative cursor-pointer rounded-md font-bold text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 hover:text-purple"
>
<span>Load a image</span>
</label>
<input {...getInputProps()} />
<input
id="file-upload"
name="file-upload"
type="file"
onChange={handleImageLoad}
accept="image/*"
className="sr-only"
/>
<p className="hidden pl-1 font-medium text-[#333]/80 lg:block">
or drag and drop
</p>
</div>
<p className="mt-4 hidden text-sm font-extrabold leading-5 text-[#555]/80 sm:block">
OR
</p>
<Button
onClick={loadDemoImage}
className="z-[120] mt-4 hidden rounded-md border-[#898aeb]/40 bg-[#898aeb]/30 font-semibold text-[#6264aa] shadow-sm sm:inline-flex"
variant="stylish"
>
Try with a demo image
<ImageIcon className="ml-2" size={19} />
</Button>
</div>
</div>
</div>
)}
</Dropzone>
)
}
================================================
FILE: components/editor/mobile-view-image-options.tsx
================================================
import { ScrollArea } from '@/components/ui/scroll-area'
import BackgroundOptions from '@/components/editor/background-options'
import CanvasOptions from '@/components/editor/canvas-options'
import FrameOptions from '@/components/editor/frame-options'
import ImageOptions from '@/components/editor/image-options'
import PerspectiveOptions from '@/components/editor/perspective-options'
import PositionOptions from '@/components/editor/position-options'
import TextOptions from '@/components/editor/text-options'
import { useActiveIndexStore } from '@/store/use-active-index'
export default function MobileViewImageOptions() {
const { activeIndex } = useActiveIndexStore()
return (
<ScrollArea className="mt-6 w-full md:hidden" type="auto">
<div className="w-full max-w-[90%] md:hidden">
{activeIndex === 0 && <CanvasOptions />}
{activeIndex === 1 && <ImageOptions />}
{activeIndex === 2 && <BackgroundOptions />}
{activeIndex === 3 && <FrameOptions />}
{activeIndex === 4 && <TextOptions />}
{activeIndex === 5 && <PerspectiveOptions />}
{activeIndex === 6 && <PositionOptions />}
</div>
</ScrollArea>
)
}
================================================
FILE: components/editor/moveable-component.tsx
================================================
'use client'
import React from 'react'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useResizeCanvas } from '@/store/use-resize-canvas'
import { useImageQualityStore } from '@/store/use-image-quality'
import { useMoveable } from '@/store/use-moveable'
import { splitWidthHeight } from '@/utils/helper-fns'
import {
Draggable,
DraggableProps,
GroupableProps,
Rotatable,
RotatableProps,
Scalable,
ScalableProps,
Snappable,
SnappableProps,
makeMoveable,
} from 'react-moveable'
const Moveable = makeMoveable<
DraggableProps &
ScalableProps &
RotatableProps &
SnappableProps &
GroupableProps
// @ts-ignore
>([Draggable, Scalable, Rotatable, Snappable])
export default function MoveableComponent({ id }: { id: string }) {
const { quality } = useImageQualityStore()
const { domResolution, scaleFactor, exactDomResolution } = useResizeCanvas()
const { setImages, images } = useImageOptions()
const { selectedImage } = useSelectedLayers()
const moveableRef = React.useRef<any>(null)
const { width, height } = splitWidthHeight(exactDomResolution)
const { isMultipleTargetSelected } = useMoveable()
const otherImages = images.filter((image) => image.id !== selectedImage)
const elementGuidelines = otherImages.map((image) => ({
element:
typeof document !== 'undefined'
? document?.getElementById(`${image.id}`)
: '',
}))
const [domWidth, domHeight]: number[] = domResolution.split('x').map(Number)
return (
<Moveable
ref={moveableRef}
target={
isMultipleTargetSelected
? '.selected'
: typeof document !== 'undefined'
? document?.getElementById(id)
: ''
}
hideChildMoveableDefaultLines={true}
draggable={true}
onDrag={({ target, beforeTranslate }) => {
const perspective = target.style.transform.match(/perspective\((.*?)\)/)
// @ts-expect-error
const xPerc = (beforeTranslate[0] / target?.offsetWidth) * 100
// @ts-expect-error
const yPerc = (beforeTranslate[1] / target?.offsetHeight) * 100
const scale = target.style.transform.match(/scale\((.*?)\)/)
const rotate = target.style.transform.match(/rotate\((.*?)\)/)
const rotateX = target.style.transform.match(/rotateX\((.*?)\)/)
const rotateY = target.style.transform.match(/rotateY\((.*?)\)/)
const rotateZ = target.style.transform.match(/rotateZ\((.*?)\)/)
target.style.transform = `${
perspective ? perspective[0] : ''
} translate(${xPerc}%, ${yPerc}%) ${scale ? scale[0] : ''} ${
rotate ? rotate[0] : ''
} ${rotateX ? rotateX[0] : ''} ${rotateY ? rotateY[0] : ''} ${
rotateZ ? rotateZ[0] : ''
} `
}}
onDragEnd={({ target, lastEvent }) => {
// @ts-expect-error
const xPerc = (lastEvent?.translate[0] / target?.offsetWidth) * 100
// @ts-expect-error
const yPerc = (lastEvent?.translate[1] / target?.offsetHeight) * 100
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
translateX: xPerc,
translateY: yPerc,
},
}
: image
)
)
}}
scalable={true}
keepRatio={true}
onScale={({ scale, target, drag }) => {
const perspective = target.style.transform.match(/perspective\((.*?)\)/)
const rotateX = target.style.transform.match(/rotateX\((.*?)\)/)
const scaleX = scale[0]
const scaleY = scale[1]
const rotate = target.style.transform.match(/rotate\((.*?)\)/)
const rotateY = target.style.transform.match(/rotateY\((.*?)\)/)
const rotateZ = target.style.transform.match(/rotateZ\((.*?)\)/)
// @ts-expect-error
const xPerc = (drag.beforeTranslate[0] / target.offsetWidth) * 100
// @ts-expect-error
const yPerc = (drag.beforeTranslate[1] / target.offsetHeight) * 100
target.style.transform = `${
perspective ? perspective[0] : ''
} translate(${xPerc}%, ${yPerc}%) scale(${scaleX}, ${scaleY}) ${
rotate ? rotate[0] : ''
} ${rotateX ? rotateX[0] : ''} ${rotateY ? rotateY[0] : ''} ${
rotateZ ? rotateZ[0] : ''
}`
}}
onScaleEnd={({ target, lastEvent }) => {
const scaleX = lastEvent.scale[0]
// @ts-expect-error
const xPerc = (lastEvent?.drag?.translate[0] / target.offsetWidth) * 100
const yPerc =
// @ts-expect-error
(lastEvent?.drag?.translate[1] / target.offsetHeight) * 100
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
translateX: xPerc,
translateY: yPerc,
imageSize: `${scaleX}`,
},
}
: image
)
)
}}
rotatable={!isMultipleTargetSelected}
rotationPosition={'top'}
onRotate={({ target, beforeRotate }) => {
const scale = target.style.transform.match(/scale\((.*?)\)/)
const translate = target.style.transform.match(/translate\((.*?)\)/)
const perspective = target.style.transform.match(/perspective\((.*?)\)/)
const rotateX = target.style.transform.match(/rotateX\((.*?)\)/)
const rotateY = target.style.transform.match(/rotateY\((.*?)\)/)
const rotateZ = target.style.transform.match(/rotateZ\((.*?)\)/)
const rotate = beforeRotate || ''
target.style.transform = `${perspective ? perspective[0] : ''} ${
translate ? translate[0] : ''
} ${scale ? scale[0] : ''} ${rotate ? `rotate(${rotate}deg)` : ''} ${
rotateX ? rotateX[0] : ''
} ${rotateY ? rotateY[0] : ''} ${rotateZ ? rotateZ[0] : ''}`
}}
onRotateEnd={({ target, lastEvent }) => {
const rotate = lastEvent.rotate
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
rotate: rotate,
},
}
: image
)
)
}}
snapRotationThreshold={2}
snapRotationDegrees={[0, 90, 180, 270]}
snappable={true}
snapDirections={{
center: true,
middle: true,
left: true,
top: true,
right: true,
bottom: true,
}}
snapThreshold={7}
horizontalGuidelines={[
domHeight / 2 / scaleFactor / quality,
domHeight / 1 / scaleFactor / quality,
0,
]}
verticalGuidelines={[
domWidth / 2 / scaleFactor / quality,
domWidth / 1 / scaleFactor / quality,
0,
]}
elementSnapDirections={{
top: true,
left: true,
bottom: true,
right: true,
center: true,
middle: true,
}}
elementGuidelines={!isMultipleTargetSelected ? elementGuidelines : []}
onRenderGroup={({ targets, events }) => {
if (!isMultipleTargetSelected) return
events.forEach((ev) => {
ev.target.style.transform = ev.transform
})
}}
onRenderGroupEnd={({ targets, events }) => {
if (isMultipleTargetSelected) {
const updatedImages = images.map((image, index) => {
const targetIndex = events.findIndex(
(ev: any) => ev.target.id === `${image.id}`
)
const updatedEvent = events[targetIndex]
if (targetIndex !== -1) {
const updatedEvent = events[targetIndex]
const xPerc =
(updatedEvent?.transformObject?.translate[0] /
// @ts-expect-error
targets[targetIndex]?.offsetWidth) *
100
const yPerc =
(updatedEvent?.transformObject?.translate[1] /
// @ts-expect-error
targets[targetIndex]?.offsetHeight) *
100
return {
...image,
style: {
...image.style,
translateX: xPerc,
translateY: yPerc,
imageSize: `${updatedEvent.transformObject.scale[0]}`,
},
}
}
// Return the original image if the target is not found
return image
})
setImages(updatedImages)
}
}}
/>
)
}
================================================
FILE: components/editor/noise.tsx
================================================
import { useBackgroundOptions } from '@/store/use-background-options'
export default function Noise() {
const { noise, isBackgroundClicked } = useBackgroundOptions()
return (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
{isBackgroundClicked && <img
draggable={false}
className={`pointer-events-none absolute z-[0] h-full w-full object-cover`}
style={{
opacity: noise,
}}
src={'/images/Noise.svg'}
alt="noise"
loading="lazy"
/>}
</>
)
}
================================================
FILE: components/editor/perspective-options/index.tsx
================================================
import { JoystickIcon, Rotate3d } from 'lucide-react'
import RotateOptions from './rotate-options'
import { Joystick } from 'react-joystick-component'
import { IJoystickUpdateEvent } from 'react-joystick-component/build/lib/Joystick'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
import { useMoveable } from '@/store/use-moveable'
export default function PerspectiveOptions() {
const { images, setImages } = useImageOptions()
const { selectedImage } = useSelectedLayers()
const { setShowControls } = useMoveable()
return (
<div className={`${selectedImage ? '' : 'pointer-events-none opacity-40'}`}>
<h3 className="mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<Rotate3d size={20} />
<span>Custom options</span>
</h3>
<RotateOptions />
<hr className="my-8" />
<h3 className="mb-6 mt-8 flex items-center gap-2 text-xs font-medium uppercase text-dark/70">
<JoystickIcon size={20} />
<span>Or with a controller</span>
</h3>
<Joystick
size={40}
stickColor="#898aeb"
baseColor="#898aeb40"
move={(event: IJoystickUpdateEvent) => {
const { type, x, y } = event
if (type === 'move') {
setShowControls(false)
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
rotateX: y! * 20,
rotateY: x! * 20,
},
}
: image
)
)
} else if (type === 'stop') {
setShowControls(true)
}
}}
></Joystick>
</div>
)
}
================================================
FILE: components/editor/perspective-options/rotate-options.tsx
================================================
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { useMoveable } from '@/store/use-moveable'
import { RotateCcw } from 'lucide-react'
import { useImageOptions, useSelectedLayers } from '@/store/use-image-options'
export default function RotateOptions() {
const { images, setImages } = useImageOptions()
const { selectedImage } = useSelectedLayers()
const { setShowControls } = useMoveable()
return (
<>
{/* Perspective */}
<div
className={`mb-3 mt-8 flex items-center px-1 md:max-w-full ${
selectedImage ? '' : 'pointer-events-none opacity-40'
}`}
>
<h1 className="text-[0.85rem]">3D Depth</h1>
<p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
{`${Math.round(
selectedImage ? images[selectedImage - 1]?.style.perspective : 0
)}px`}
</p>
<Button
aria-label="reset rotate x"
variant="secondary"
size="sm"
className="ml-auto translate-x-2"
onClick={() =>
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
perspective: 2000,
},
}
: image
)
)
}
>
<RotateCcw size={15} className="text-dark/80" />
</Button>
</div>
<div className="mb-3 flex gap-4 text-[0.85rem] md:max-w-full">
<Slider
defaultValue={[0]}
max={6500}
min={0}
step={0.0001}
value={
selectedImage
? [images[selectedImage - 1]?.style.perspective]
: [2000]
}
onValueChange={(value: number[]) => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
perspective: value[0],
},
}
: image
)
)
setShowControls(false)
}}
onValueCommit={() => setShowControls(true)}
onIncrement={() => {
if (selectedImage) {
if (images[selectedImage - 1]?.style.perspective >= 6500) return
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
perspective: Number(image.style.perspective) + 500,
},
}
: image
)
)
}
}}
onDecrement={() => {
if (selectedImage) {
if (images[selectedImage - 1]?.style.perspective <= 0) return
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
perspective: Number(image.style.perspective) - 500,
},
}
: image
)
)
}
}}
/>
</div>
<hr className="my-6" />
{/* RotateX */}
<div className="mb-3 flex items-center px-1 md:max-w-full">
<h1 className="text-[0.85rem]">Rotate X</h1>
<p className="ml-2 rounded-md bg-formDark p-[0.4rem] text-[0.8rem] text-dark/70">
{`${Math.round(
selectedImage ? images[selectedImage - 1]?.style.rotateX : 0
)}px`}
</p>
<Button
aria-label="reset rotate x"
variant="secondary"
size="sm"
className="ml-auto translate-x-2"
onClick={() => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
rotateX: 0.0001,
},
}
: image
)
)
}}
>
<RotateCcw size={15} className="text-dark/80" />
</Button>
</div>
<div className="mb-3 flex gap-4 text-[0.85rem] md:max-w-full">
<Slider
defaultValue={[0]}
max={180}
min={-180}
step={0.0001}
value={
selectedImage ? [images[selectedImage - 1]?.style.rotateX] : [0]
}
onValueChange={(value: number[]) => {
selectedImage &&
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
...image.style,
rotateX: value[0],
},
}
: image
)
)
setShowControls(false)
}}
onValueCommit={() => setShowControls(true)}
onIncrement={() => {
if (selectedImage) {
if (images[selectedImage - 1]?.style.rotateX >= 180) return
setImages(
images.map((image, index) =>
index === selectedImage - 1
? {
...image,
style: {
gitextract_iv9ddlq0/
├── .eslintrc.json
├── .gitignore
├── .vscode/
│ └── settings.json
├── README.md
├── app/
│ ├── (routes)/
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── articles/
│ │ │ ├── [slug]/
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ └── sitemap.ts
│ │ └── layout.tsx
│ ├── admin/
│ │ ├── layout.tsx
│ │ └── write-article/
│ │ ├── blog-form.tsx
│ │ └── page.tsx
│ ├── api/
│ │ ├── article/
│ │ │ └── post/
│ │ │ └── route.ts
│ │ ├── auth/
│ │ │ └── [...nextauth]/
│ │ │ └── route.ts
│ │ └── user/
│ │ └── settings/
│ │ └── route.ts
│ ├── error.tsx
│ ├── layout.tsx
│ ├── loading.tsx
│ ├── manifest.json
│ ├── not-found.tsx
│ ├── page.tsx
│ ├── robots.ts
│ └── sitemap.ts
├── components/
│ ├── articles/
│ │ └── article-card.tsx
│ ├── auth-modal.tsx
│ ├── clarity-script.tsx
│ ├── color-picker.tsx
│ ├── editor/
│ │ ├── background-image-canvas.tsx
│ │ ├── background-options/
│ │ │ ├── custom-gradient-picker.tsx
│ │ │ ├── image-gradient-picker.tsx
│ │ │ ├── index.tsx
│ │ │ ├── noise-slider.tsx
│ │ │ ├── normal-gradient-picker.tsx
│ │ │ └── pattern-picker.tsx
│ │ ├── browser-frames.tsx
│ │ ├── canvas-area.tsx
│ │ ├── canvas-options/
│ │ │ ├── canvas-roundness-slider.tsx
│ │ │ ├── index.tsx
│ │ │ └── resolution-button.tsx
│ │ ├── frame-options/
│ │ │ ├── additional-frame-options.tsx
│ │ │ ├── frame-picker.tsx
│ │ │ └── index.tsx
│ │ ├── image-context-menu.tsx
│ │ ├── image-options/
│ │ │ ├── add-image-button.tsx
│ │ │ ├── index.tsx
│ │ │ ├── inset-option.tsx
│ │ │ ├── roundness-option.tsx
│ │ │ ├── scale-options.tsx
│ │ │ └── shadow-settings.tsx
│ │ ├── main-image-area.tsx
│ │ ├── mobile-view-image-options.tsx
│ │ ├── moveable-component.tsx
│ │ ├── noise.tsx
│ │ ├── perspective-options/
│ │ │ ├── index.tsx
│ │ │ └── rotate-options.tsx
│ │ ├── position-options/
│ │ │ ├── index.tsx
│ │ │ ├── position-control.tsx
│ │ │ └── translate-control.tsx
│ │ ├── selecto-component.tsx
│ │ ├── sidebar-buttons.tsx
│ │ ├── sidebar.tsx
│ │ ├── text-context-menu.tsx
│ │ ├── text-layers.tsx
│ │ ├── text-options/
│ │ │ ├── add-text-layer.tsx
│ │ │ ├── font-settings.tsx
│ │ │ └── index.tsx
│ │ ├── tiptap-moveable.tsx
│ │ └── undo-redo-buttons.tsx
│ ├── export-options.tsx
│ ├── footer.tsx
│ ├── icons/
│ │ ├── index.tsx
│ │ └── info.icon.tsx
│ ├── loader.tsx
│ ├── navbar.tsx
│ ├── navlinks.tsx
│ ├── popup-color-picker.tsx
│ ├── profile-dialog.tsx
│ ├── settings-dialog.tsx
│ ├── sign-in-form.tsx
│ ├── spinner/
│ │ ├── spinner.module.css
│ │ └── spinner.tsx
│ ├── ui/
│ │ ├── accordion.tsx
│ │ ├── back-button.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── circular-slider.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── gradient-text.tsx
│ │ ├── input.tsx
│ │ ├── popover.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── spotlight-button.tsx
│ │ ├── style/
│ │ │ └── checkbox.module.css
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── text-area.tsx
│ │ ├── text.tsx
│ │ ├── theme-button.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── tooltip.tsx
│ └── user-dropdown.tsx
├── components.json
├── docker-compose.yml
├── hooks/
│ ├── canvas-area-hooks/
│ │ ├── use-automatic-aspect-ratio-switcher.ts
│ │ ├── use-resize-observer.ts
│ │ └── use-screen-size-warning-toast.ts
│ ├── use-editor.ts
│ ├── use-event-listener.ts
│ ├── use-isomorphic-layout-effect.ts
│ ├── use-media-query.ts
│ ├── use-on-click-outside.ts
│ └── use-toast.ts
├── index.d.ts
├── libs/
│ ├── prismadb.ts
│ └── validators/
│ ├── article-post-validator.ts
│ └── user-settings-validator.ts
├── license.md
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prettier.config.js
├── prisma/
│ └── schema.prisma
├── providers/
│ └── index.tsx
├── store/
│ ├── use-active-index.ts
│ ├── use-background-options.ts
│ ├── use-color-extractor.ts
│ ├── use-frame-options.ts
│ ├── use-image-options.ts
│ ├── use-image-quality.ts
│ ├── use-moveable.ts
│ ├── use-resize-canvas.ts
│ └── use-tiptap.ts
├── styles/
│ └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── utils/
│ ├── auth-options.ts
│ ├── button-utils.ts
│ ├── helper-fns.ts
│ ├── presets/
│ │ ├── gradients.ts
│ │ ├── qualities.ts
│ │ ├── resolutions.ts
│ │ ├── shadows.ts
│ │ └── solid-colors.ts
│ └── tiptap-extensions.ts
└── workers/
└── background-removal.worker.ts
SYMBOL INDEX (167 symbols across 113 files)
FILE: app/(routes)/about/page.tsx
function page (line 21) | function page() {
FILE: app/(routes)/articles/[slug]/loading.tsx
function Loading (line 3) | function Loading() {
FILE: app/(routes)/articles/[slug]/page.tsx
type ArticleProps (line 16) | type ArticleProps = {
function generateStaticParams (line 24) | async function generateStaticParams() {
function generateMetadata (line 46) | async function generateMetadata(props: ArticleProps) {
function ArticlePage (line 97) | async function ArticlePage(props: ArticleProps) {
FILE: app/(routes)/articles/page.tsx
function Article (line 37) | async function Article() {
FILE: app/(routes)/articles/sitemap.ts
function sitemap (line 4) | async function sitemap(): Promise<MetadataRoute.Sitemap> {
FILE: app/(routes)/layout.tsx
function AdminLayout (line 4) | function AdminLayout({
FILE: app/admin/layout.tsx
function AdminLayout (line 3) | function AdminLayout({
FILE: app/admin/write-article/blog-form.tsx
function BlogForm (line 31) | function BlogForm() {
FILE: app/admin/write-article/page.tsx
function page (line 15) | function page() {
FILE: app/api/article/post/route.ts
function POST (line 8) | async function POST(request: Request) {
FILE: app/api/user/settings/route.ts
function GET (line 7) | async function GET() {
function PATCH (line 41) | async function PATCH(request: Request) {
FILE: app/error.tsx
function Error (line 10) | function Error({
FILE: app/layout.tsx
function RootLayout (line 93) | function RootLayout({
FILE: app/loading.tsx
function Loading (line 3) | function Loading() {
FILE: app/not-found.tsx
function NotFound (line 9) | function NotFound() {
FILE: app/page.tsx
function Home (line 4) | function Home() {
FILE: app/robots.ts
function robots (line 3) | function robots(): MetadataRoute.Robots {
FILE: app/sitemap.ts
function sitemap (line 3) | function sitemap(): MetadataRoute.Sitemap {
FILE: components/articles/article-card.tsx
type ArticleCardProps (line 4) | type ArticleCardProps = {
function ArticleCard (line 12) | function ArticleCard({
FILE: components/auth-modal.tsx
function AuthModal (line 10) | function AuthModal() {
FILE: components/color-picker.tsx
function ColorPicker (line 10) | function ColorPicker({
FILE: components/editor/background-image-canvas.tsx
function BackgroundImageCanvas (line 6) | function BackgroundImageCanvas() {
FILE: components/editor/background-options/custom-gradient-picker.tsx
function CustomGradientPicker (line 9) | function CustomGradientPicker() {
FILE: components/editor/background-options/image-gradient-picker.tsx
function ImageGradientPicker (line 17) | function ImageGradientPicker() {
FILE: components/editor/background-options/index.tsx
function BackgroundOptions (line 9) | function BackgroundOptions() {
FILE: components/editor/background-options/noise-slider.tsx
function NoiseSlider (line 5) | function NoiseSlider() {
FILE: components/editor/background-options/normal-gradient-picker.tsx
type Color (line 29) | type Color = string
function NormalGradientPicker (line 31) | function NormalGradientPicker() {
FILE: components/editor/background-options/pattern-picker.tsx
function PatternPicker (line 17) | function PatternPicker() {
FILE: components/editor/browser-frames.tsx
function BrowserFrame (line 91) | function BrowserFrame({ frame }: { frame: FrameTypes }) {
FILE: components/editor/canvas-area.tsx
function Canvas (line 26) | function Canvas() {
FILE: components/editor/canvas-options/canvas-roundness-slider.tsx
function CanvasRoundnessSlider (line 6) | function CanvasRoundnessSlider() {
FILE: components/editor/canvas-options/index.tsx
function CanvasOptions (line 45) | function CanvasOptions() {
FILE: components/editor/canvas-options/resolution-button.tsx
function ResolutionButton (line 15) | function ResolutionButton({
FILE: components/editor/frame-options/additional-frame-options.tsx
function AdditionalFrameOptions (line 7) | function AdditionalFrameOptions() {
FILE: components/editor/frame-options/frame-picker.tsx
function FramePicker (line 13) | function FramePicker() {
function FrameContainer (line 149) | function FrameContainer({
FILE: components/editor/frame-options/index.tsx
function FrameOptions (line 6) | function FrameOptions() {
FILE: components/editor/image-context-menu.tsx
function ContextMenuImage (line 40) | function ContextMenuImage({
function ReplaceImage (line 465) | function ReplaceImage() {
FILE: components/editor/image-options/add-image-button.tsx
type AddImageButtonProps (line 9) | type AddImageButtonProps = {}
function AddImageButton (line 11) | function AddImageButton({}: AddImageButtonProps) {
FILE: components/editor/image-options/index.tsx
function ImageOptions (line 18) | function ImageOptions() {
FILE: components/editor/image-options/inset-option.tsx
function InsetOption (line 13) | function InsetOption() {
FILE: components/editor/image-options/roundness-option.tsx
function RoundnessOption (line 7) | function RoundnessOption() {
FILE: components/editor/image-options/scale-options.tsx
type SizeOptionProps (line 5) | type SizeOptionProps = {
function SizeOption (line 9) | function SizeOption({ text = 'Scale' }: SizeOptionProps) {
FILE: components/editor/image-options/shadow-settings.tsx
function ShadowSettings (line 17) | function ShadowSettings() {
FILE: components/editor/main-image-area.tsx
function LoadAImage (line 310) | function LoadAImage() {
FILE: components/editor/mobile-view-image-options.tsx
function MobileViewImageOptions (line 11) | function MobileViewImageOptions() {
FILE: components/editor/moveable-component.tsx
function MoveableComponent (line 32) | function MoveableComponent({ id }: { id: string }) {
FILE: components/editor/noise.tsx
function Noise (line 3) | function Noise() {
FILE: components/editor/perspective-options/index.tsx
function PerspectiveOptions (line 8) | function PerspectiveOptions() {
FILE: components/editor/perspective-options/rotate-options.tsx
function RotateOptions (line 7) | function RotateOptions() {
FILE: components/editor/position-options/index.tsx
function PositionOptions (line 7) | function PositionOptions() {
FILE: components/editor/position-options/position-control.tsx
function PositionControl (line 17) | function PositionControl() {
FILE: components/editor/position-options/translate-control.tsx
function TranslateOption (line 7) | function TranslateOption() {
FILE: components/editor/selecto-component.tsx
function SelectoComponent (line 13) | function SelectoComponent() {
FILE: components/editor/sidebar-buttons.tsx
function SidebarButton (line 6) | function SidebarButton({
FILE: components/editor/sidebar.tsx
type SidebarSection (line 25) | type SidebarSection =
type SidebarButton (line 34) | interface SidebarButton {
function useSidebarButtons (line 41) | function useSidebarButtons() {
function Sidebar (line 71) | function Sidebar() {
type SidebarImageSettingsProps (line 101) | interface SidebarImageSettingsProps {
function SidebarImageSettings (line 105) | function SidebarImageSettings({
FILE: components/editor/text-context-menu.tsx
function ContextMenuText (line 15) | function ContextMenuText({
FILE: components/editor/text-layers.tsx
type MenuBarProps (line 11) | type MenuBarProps = {
function TipTapEditor (line 77) | function TipTapEditor() {
function TextLayers (line 103) | function TextLayers() {
FILE: components/editor/text-options/add-text-layer.tsx
function AddTextLayer (line 7) | function AddTextLayer() {
FILE: components/editor/text-options/font-settings.tsx
function FontSettings (line 31) | function FontSettings() {
FILE: components/editor/text-options/index.tsx
function TextOptions (line 6) | function TextOptions() {
FILE: components/editor/tiptap-moveable.tsx
function TiptapMoveable (line 25) | function TiptapMoveable({ id }: { id: string }) {
FILE: components/editor/undo-redo-buttons.tsx
function useUndoRedoHotkeys (line 9) | function useUndoRedoHotkeys() {
function UndoRedoButtons (line 34) | function UndoRedoButtons() {
FILE: components/export-options.tsx
type ExportOptionsProps (line 18) | interface ExportOptionsProps {
function ExportOptions (line 22) | function ExportOptions({ isLoggedIn }: ExportOptionsProps) {
FILE: components/footer.tsx
function Footer (line 6) | function Footer() {
FILE: components/icons/index.tsx
type IconType (line 3) | type IconType = {
function Icon (line 12) | function Icon({
FILE: components/icons/info.icon.tsx
function InfoIcon (line 3) | function InfoIcon({
FILE: components/loader.tsx
function Loader (line 3) | function Loader() {
FILE: components/navbar.tsx
function Navbar (line 10) | function Navbar() {
FILE: components/navlinks.tsx
function NavLinks (line 32) | function NavLinks() {
FILE: components/popup-color-picker.tsx
function PopupColorPicker (line 7) | function PopupColorPicker({
FILE: components/profile-dialog.tsx
type ProfileDialogProps (line 21) | interface ProfileDialogProps {
function ProfileDialog (line 26) | function ProfileDialog({
FILE: components/settings-dialog.tsx
type SettingsDialogProps (line 30) | interface SettingsDialogProps {
function SettingsDialog (line 35) | function SettingsDialog({
FILE: components/sign-in-form.tsx
type SignInFormProps (line 12) | type SignInFormProps = {
function SignInForm (line 16) | function SignInForm({ authenticated }: SignInFormProps) {
FILE: components/spinner/spinner.tsx
function Spinner (line 3) | function Spinner() {
FILE: components/ui/back-button.tsx
type BackButtonProps (line 10) | type BackButtonProps = {
function BackButton (line 16) | function BackButton({
FILE: components/ui/badge.tsx
type BadgeProps (line 22) | interface BadgeProps
function Badge (line 26) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: components/ui/button.tsx
type ButtonProps (line 53) | interface ButtonProps
FILE: components/ui/gradient-text.tsx
type TextElement (line 17) | type TextElement = HTMLParagraphElement | HTMLHeadingElement | HTMLSpanE...
type TextProps (line 19) | interface TextProps
FILE: components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({
FILE: components/ui/spotlight-button.tsx
type Props (line 6) | type Props = {
function SpotlightButton (line 14) | function SpotlightButton({
FILE: components/ui/text-area.tsx
type TextareaProps (line 5) | interface TextareaProps
FILE: components/ui/text.tsx
type TextElement (line 24) | type TextElement = HTMLHeadingElement | HTMLParagraphElement
type TextProps (line 26) | interface TextProps
FILE: components/ui/theme-button.tsx
function ModeToggle (line 15) | function ModeToggle() {
FILE: components/ui/toast.tsx
type ToastProps (line 112) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement (line 114) | type ToastActionElement = React.ReactElement<typeof ToastAction>
FILE: components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: components/user-dropdown.tsx
type MenuItem (line 22) | interface MenuItem {
FILE: hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher.ts
function useAutomaticAspectRatioSwitcher (line 16) | function useAutomaticAspectRatioSwitcher({
FILE: hooks/canvas-area-hooks/use-resize-observer.ts
function useCanvasResizeObserver (line 12) | function useCanvasResizeObserver(
FILE: hooks/canvas-area-hooks/use-screen-size-warning-toast.ts
function useScreenSizeWarningToast (line 8) | function useScreenSizeWarningToast() {
FILE: hooks/use-editor.ts
function useTiptapEditor (line 6) | function useTiptapEditor() {
FILE: hooks/use-event-listener.ts
function useEventListener (line 40) | function useEventListener<
FILE: hooks/use-media-query.ts
function useMediaQuery (line 4) | function useMediaQuery(query: string): boolean {
FILE: hooks/use-on-click-outside.ts
type Handler (line 5) | type Handler = (event: MouseEvent) => void
function useOnClickOutside (line 7) | function useOnClickOutside<T extends HTMLElement = HTMLElement>(
FILE: hooks/use-toast.ts
constant TOAST_LIMIT (line 6) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 7) | const TOAST_REMOVE_DELAY = 1000000
type ToasterToast (line 9) | type ToasterToast = ToastProps & {
function genId (line 25) | function genId() {
type ActionType (line 30) | type ActionType = typeof actionTypes
type Action (line 32) | type Action =
type State (line 50) | interface State {
function dispatch (line 131) | function dispatch(action: Action) {
type Toast (line 138) | type Toast = Omit<ToasterToast, 'id'>
function toast (line 140) | function toast({ ...props }: Toast) {
function useToast (line 169) | function useToast() {
FILE: index.d.ts
type Maybe (line 4) | type Maybe = null | undefined
class ColorThief (line 23) | class ColorThief {
FILE: providers/index.tsx
type ProviderProps (line 8) | type ProviderProps = {
function Providers (line 12) | function Providers({ children }: ProviderProps) {
FILE: store/use-active-index.ts
type ActiveIndexState (line 3) | interface ActiveIndexState {
FILE: store/use-background-options.ts
type BackgroundOptionsState (line 3) | interface BackgroundOptionsState {
FILE: store/use-color-extractor.ts
type ActiveIndexState (line 3) | interface ActiveIndexState {
FILE: store/use-frame-options.ts
type FrameTypes (line 3) | type FrameTypes =
type FrameOptionsState (line 10) | interface FrameOptionsState {
FILE: store/use-image-options.ts
type ImageStyle (line 6) | interface ImageStyle {
type ImageItem (line 29) | interface ImageItem {
type ImageOptionsState (line 42) | interface ImageOptionsState {
type SelectedLayerState (line 257) | interface SelectedLayerState {
FILE: store/use-image-quality.ts
type ImageQualityState (line 3) | interface ImageQualityState {
FILE: store/use-moveable.ts
type MoveableState (line 3) | interface MoveableState {
FILE: store/use-resize-canvas.ts
type ResizeCanvasState (line 3) | interface ResizeCanvasState {
FILE: store/use-tiptap.ts
type TiptapState (line 3) | interface TiptapState {
FILE: utils/auth-options.ts
type Session (line 12) | interface Session extends DefaultSession {
type JWT (line 22) | interface JWT {
method jwt (line 46) | async jwt({ token, user, session, trigger }) {
method session (line 87) | async session({ session, token }) {
FILE: utils/button-utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: utils/helper-fns.ts
function splitWidthHeight (line 1) | function splitWidthHeight(resolution: string) {
function convertHexToRgba (line 6) | function convertHexToRgba(hexCode: string, opacity = 1) {
function calculateEqualCanvasSize (line 25) | function calculateEqualCanvasSize(
function capitalize (line 44) | function capitalize(str: string) {
function formatDate (line 89) | function formatDate(date: Date | string | number | undefined): string {
function generateFormattedBlogDate (line 104) | function generateFormattedBlogDate(
FILE: utils/presets/gradients.ts
type Gradient (line 1) | interface Gradient {
FILE: utils/presets/qualities.ts
type Quality (line 1) | interface Quality {
FILE: utils/presets/resolutions.ts
type Resolution (line 1) | interface Resolution {
FILE: utils/presets/shadows.ts
type Shadow (line 1) | interface Shadow {
Condensed preview — 155 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (448K chars).
[
{
"path": ".eslintrc.json",
"chars": 40,
"preview": "{\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": ".gitignore",
"chars": 388,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": ".vscode/settings.json",
"chars": 498,
"preview": "{\n \"tailwindCSS.experimental.classRegex\": [\n [\"cva\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*).*?[\\\"'`]\"],\n [\"cx\\\\(([^)]*)\\\\"
},
{
"path": "README.md",
"chars": 832,
"preview": "## Prismify\n\nPrismify is a web app that aims to revitalize & enhance boring images/screenshots. With prismify, you can e"
},
{
"path": "app/(routes)/about/page.tsx",
"chars": 5311,
"preview": "/* eslint-disable react/no-unescaped-entities */\nimport React from 'react'\nimport BackButton from '@/components/ui/back-"
},
{
"path": "app/(routes)/articles/[slug]/loading.tsx",
"chars": 100,
"preview": "import Loader from '@/components/loader'\n\nexport default function Loading() {\n return <Loader />\n}\n"
},
{
"path": "app/(routes)/articles/[slug]/page.tsx",
"chars": 4877,
"preview": "import BackButton from '@/components/ui/back-button'\nimport { Badge } from '@/components/ui/badge'\nimport { Text } from "
},
{
"path": "app/(routes)/articles/page.tsx",
"chars": 2975,
"preview": "import prismadb from '@/libs/prismadb'\nimport { Text } from '@/components/ui/text'\nimport ArticleCard from '@/components"
},
{
"path": "app/(routes)/articles/sitemap.ts",
"chars": 526,
"preview": "import prismadb from '@/libs/prismadb'\nimport { MetadataRoute } from 'next'\n\nexport default async function sitemap(): Pr"
},
{
"path": "app/(routes)/layout.tsx",
"chars": 271,
"preview": "import Footer from '@/components/footer'\nimport React from 'react'\n\nexport default function AdminLayout({\n children,\n}:"
},
{
"path": "app/admin/layout.tsx",
"chars": 208,
"preview": "import React from 'react'\n\nexport default function AdminLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n ret"
},
{
"path": "app/admin/write-article/blog-form.tsx",
"chars": 13179,
"preview": "'use client'\n\nimport { EditorProvider, useCurrentEditor } from '@tiptap/react'\nimport {\n Bold,\n Code,\n Heading,\n Ima"
},
{
"path": "app/admin/write-article/page.tsx",
"chars": 570,
"preview": "import Loader from '@/components/loader'\nimport BackButton from '@/components/ui/back-button'\nimport dynamic from 'next/"
},
{
"path": "app/api/article/post/route.ts",
"chars": 1311,
"preview": "import { z } from 'zod'\nimport { NextResponse } from 'next/server'\nimport prismadb from '@/libs/prismadb'\nimport { getCu"
},
{
"path": "app/api/auth/[...nextauth]/route.ts",
"chars": 206,
"preview": "import { AuthOptions } from 'next-auth'\nimport { authOptions } from '@/utils/auth-options'\nimport NextAuth from 'next-au"
},
{
"path": "app/api/user/settings/route.ts",
"chars": 1754,
"preview": "import { z } from 'zod'\nimport { NextResponse } from 'next/server'\nimport prismadb from '@/libs/prismadb'\nimport { getCu"
},
{
"path": "app/error.tsx",
"chars": 1671,
"preview": "'use client'\n\n/* eslint-disable react/no-unescaped-entities */\nimport { Button, buttonVariants } from '@/components/ui/b"
},
{
"path": "app/layout.tsx",
"chars": 3779,
"preview": "import ClarityScript from '@/components/clarity-script'\nimport Navbar from '@/components/navbar'\nimport { Toaster } from"
},
{
"path": "app/loading.tsx",
"chars": 100,
"preview": "import Loader from '@/components/loader'\n\nexport default function Loading() {\n return <Loader />\n}\n"
},
{
"path": "app/manifest.json",
"chars": 535,
"preview": "{\n \"name\": \"Prismify\",\n \"short_name\": \"Prismify\",\n \"start_url\": \"/\",\n \"display\": \"standalone\",\n \"background_color\":"
},
{
"path": "app/not-found.tsx",
"chars": 1221,
"preview": "/* eslint-disable react/no-unescaped-entities */\n'use client'\n\nimport { buttonVariants } from '@/components/ui/button'\ni"
},
{
"path": "app/page.tsx",
"chars": 269,
"preview": "import Sidebar from '@/components/editor/sidebar'\nimport Canvas from '@/components/editor/canvas-area'\n\nexport default f"
},
{
"path": "app/robots.ts",
"chars": 328,
"preview": "import { MetadataRoute } from 'next'\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: {\n "
},
{
"path": "app/sitemap.ts",
"chars": 421,
"preview": "import { MetadataRoute } from 'next'\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n const sitemapUrls: Me"
},
{
"path": "components/articles/article-card.tsx",
"chars": 1297,
"preview": "import Image from 'next/image'\nimport Link from 'next/link'\n\ntype ArticleCardProps = {\n title: string\n category: strin"
},
{
"path": "components/auth-modal.tsx",
"chars": 1609,
"preview": "import * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { Dialog, DialogContent, DialogTr"
},
{
"path": "components/clarity-script.tsx",
"chars": 570,
"preview": "import Script from 'next/script'\n\nconst ClarityScript = () => (\n <Script id=\"clarity-script\" strategy=\"afterInteractive"
},
{
"path": "components/color-picker.tsx",
"chars": 1511,
"preview": "'use client'\n\nimport { useState } from 'react'\nimport {\n HexAlphaColorPicker,\n HexColorInput,\n HexColorPicker,\n} from"
},
{
"path": "components/editor/background-image-canvas.tsx",
"chars": 635,
"preview": "// this component shows background image & noise in the canvas\n\nimport { useBackgroundOptions } from '@/store/use-backgr"
},
{
"path": "components/editor/background-options/custom-gradient-picker.tsx",
"chars": 3230,
"preview": "'use client'\n\nimport { useCallback } from 'react'\nimport { solidColors } from '@/utils/presets/solid-colors'\nimport { Bu"
},
{
"path": "components/editor/background-options/image-gradient-picker.tsx",
"chars": 5583,
"preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} "
},
{
"path": "components/editor/background-options/index.tsx",
"chars": 1745,
"preview": "import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport { useBackgroundOptions } from '@/"
},
{
"path": "components/editor/background-options/noise-slider.tsx",
"chars": 1059,
"preview": "import { Slider } from '@/components/ui/slider'\nimport { useBackgroundOptions } from '@/store/use-background-options'\nim"
},
{
"path": "components/editor/background-options/normal-gradient-picker.tsx",
"chars": 16544,
"preview": "'use client'\n\nimport { useState } from 'react'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/"
},
{
"path": "components/editor/background-options/pattern-picker.tsx",
"chars": 5144,
"preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} "
},
{
"path": "components/editor/browser-frames.tsx",
"chars": 3291,
"preview": "// This component is responsible for rendering the browser frame around the image layer.\n\n'use client'\n\nimport { FrameTy"
},
{
"path": "components/editor/canvas-area.tsx",
"chars": 5985,
"preview": "'use client'\n\nimport useAutomaticAspectRatioSwitcher from '@/hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher"
},
{
"path": "components/editor/canvas-options/canvas-roundness-slider.tsx",
"chars": 1549,
"preview": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useResizeCanvas"
},
{
"path": "components/editor/canvas-options/index.tsx",
"chars": 5830,
"preview": "'use client'\n\nimport {\n Dribbble,\n Facebook,\n Instagram,\n Linkedin,\n Plus,\n Minus,\n Twitter,\n Youtube,\n ArrowRi"
},
{
"path": "components/editor/canvas-options/resolution-button.tsx",
"chars": 5943,
"preview": "import { useState } from 'react'\nimport { cn } from '@/utils/button-utils'\nimport { useImageOptions, useSelectedLayers }"
},
{
"path": "components/editor/frame-options/additional-frame-options.tsx",
"chars": 4082,
"preview": "import PopupColorPicker from '@/components/popup-color-picker'\nimport { Switch } from '@/components/ui/switch'\nimport { "
},
{
"path": "components/editor/frame-options/frame-picker.tsx",
"chars": 6471,
"preview": "import {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@/components/ui/select'\nimport"
},
{
"path": "components/editor/frame-options/index.tsx",
"chars": 436,
"preview": "import RoundnessOption from '../image-options/roundness-option'\nimport FramePicker from './frame-picker'\nimport Addition"
},
{
"path": "components/editor/image-context-menu.tsx",
"chars": 17243,
"preview": "import { Button } from '@/components/ui/button'\nimport {\n ContextMenu,\n ContextMenuContent,\n ContextMenuItem,\n Conte"
},
{
"path": "components/editor/image-options/add-image-button.tsx",
"chars": 4290,
"preview": "import { ChangeEvent, useRef, useState } from 'react'\nimport { X, Plus, Upload } from 'lucide-react'\nimport { useImageOp"
},
{
"path": "components/editor/image-options/index.tsx",
"chars": 2287,
"preview": "'use client'\n\nimport { Focus, GalleryVerticalEnd } from 'lucide-react'\nimport SizeOption from './scale-options'\nimport R"
},
{
"path": "components/editor/image-options/inset-option.tsx",
"chars": 4252,
"preview": "'use client'\n\nimport { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/stor"
},
{
"path": "components/editor/image-options/roundness-option.tsx",
"chars": 2549,
"preview": "'use client'\n\nimport { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/stor"
},
{
"path": "components/editor/image-options/scale-options.tsx",
"chars": 1413,
"preview": "import { Slider } from '@/components/ui/slider'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-op"
},
{
"path": "components/editor/image-options/shadow-settings.tsx",
"chars": 7585,
"preview": "'use client'\n\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { Button } from '@/components/ui/but"
},
{
"path": "components/editor/main-image-area.tsx",
"chars": 19622,
"preview": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { useOnClickOutside } from '@/hooks/use-on-click-out"
},
{
"path": "components/editor/mobile-view-image-options.tsx",
"chars": 1190,
"preview": "import { ScrollArea } from '@/components/ui/scroll-area'\nimport BackgroundOptions from '@/components/editor/background-o"
},
{
"path": "components/editor/moveable-component.tsx",
"chars": 9078,
"preview": "'use client'\n\nimport React from 'react'\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\ni"
},
{
"path": "components/editor/noise.tsx",
"chars": 551,
"preview": "import { useBackgroundOptions } from '@/store/use-background-options'\n\nexport default function Noise() {\n const { noise"
},
{
"path": "components/editor/perspective-options/index.tsx",
"chars": 1926,
"preview": "import { JoystickIcon, Rotate3d } from 'lucide-react'\nimport RotateOptions from './rotate-options'\nimport { Joystick } f"
},
{
"path": "components/editor/perspective-options/rotate-options.tsx",
"chars": 13307,
"preview": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } f"
},
{
"path": "components/editor/position-options/index.tsx",
"chars": 611,
"preview": "'use client'\n\nimport SizeOption from '../image-options/scale-options'\nimport PositionControl from './position-control'\ni"
},
{
"path": "components/editor/position-options/position-control.tsx",
"chars": 4385,
"preview": "'use client'\n\nimport {\n CircleDot,\n ArrowDown,\n ArrowDownLeft,\n ArrowDownRight,\n ArrowLeft,\n ArrowRight,\n ArrowUp"
},
{
"path": "components/editor/position-options/translate-control.tsx",
"chars": 6888,
"preview": "import { Button } from '@/components/ui/button'\nimport { Slider } from '@/components/ui/slider'\nimport { useMoveable } f"
},
{
"path": "components/editor/selecto-component.tsx",
"chars": 1370,
"preview": "import React from 'react'\nimport { useSelectedLayers } from '@/store/use-image-options'\nimport { useMoveable } from '@/s"
},
{
"path": "components/editor/sidebar-buttons.tsx",
"chars": 1035,
"preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { useActiveIndexStore } from '@/store/use-active-in"
},
{
"path": "components/editor/sidebar.tsx",
"chars": 3768,
"preview": "'use client'\n\nimport BackgroundOptions from '@/components/editor/background-options'\nimport { ScrollArea } from '@/compo"
},
{
"path": "components/editor/text-context-menu.tsx",
"chars": 3377,
"preview": "import {\n ContextMenu,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n Contex"
},
{
"path": "components/editor/text-layers.tsx",
"chars": 4800,
"preview": "'use client'\n\nimport useTiptapEditor from '@/hooks/use-editor'\nimport { useImageOptions, useSelectedLayers } from '@/sto"
},
{
"path": "components/editor/text-options/add-text-layer.tsx",
"chars": 735,
"preview": "'use client'\n\nimport { Plus } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { useImageOptio"
},
{
"path": "components/editor/text-options/font-settings.tsx",
"chars": 12797,
"preview": "'use client'\n\nimport PopupColorPicker from '@/components/popup-color-picker'\nimport { Button } from '@/components/ui/but"
},
{
"path": "components/editor/text-options/index.tsx",
"chars": 673,
"preview": "import { Type } from 'lucide-react'\nimport AddTextLayer from './add-text-layer'\nimport FontSettings from './font-setting"
},
{
"path": "components/editor/tiptap-moveable.tsx",
"chars": 3295,
"preview": "'use client'\n\nimport { useImageOptions, useSelectedLayers } from '@/store/use-image-options'\nimport { useResizeCanvas } "
},
{
"path": "components/editor/undo-redo-buttons.tsx",
"chars": 1328,
"preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { useTemporalStore } from '@/store/use-image-option"
},
{
"path": "components/export-options.tsx",
"chars": 8212,
"preview": "'use client'\n\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@/components/ui/popover'\nimport { toast } "
},
{
"path": "components/footer.tsx",
"chars": 7285,
"preview": "import { cn } from '@/utils/button-utils'\nimport Link from 'next/link'\nimport React from 'react'\nimport { buttonVariants"
},
{
"path": "components/icons/index.tsx",
"chars": 757,
"preview": "import InfoIcon from './info.icon'\n\ntype IconType = {\n name: 'info'\n size?: number\n color?: string\n className?: stri"
},
{
"path": "components/icons/info.icon.tsx",
"chars": 2466,
"preview": "import React from 'react'\n\nexport default function InfoIcon({\n variant,\n}: {\n variant?: 'default' | 'solid' | 'duotone"
},
{
"path": "components/loader.tsx",
"chars": 1799,
"preview": "// Reusable loading spinner component to be used as suspense fallback on server components.\n\nexport default function Loa"
},
{
"path": "components/navbar.tsx",
"chars": 1451,
"preview": "'use client'\n\nimport { AuthModal } from '@/components/auth-modal'\nimport ExportOptions from '@/components/export-options"
},
{
"path": "components/navlinks.tsx",
"chars": 3780,
"preview": "// Links in the navbar\n\nimport { Badge } from '@/components/ui/badge'\nimport { buttonVariants } from '@/components/ui/bu"
},
{
"path": "components/popup-color-picker.tsx",
"chars": 1624,
"preview": "// Formatted color picker component which opens a popover with a color picker.\n\nimport { Popover, PopoverContent, Popove"
},
{
"path": "components/profile-dialog.tsx",
"chars": 2470,
"preview": "'use client'\n\nimport { useMemo } from 'react'\nimport { useSession } from 'next-auth/react'\nimport { User as UserIcon } f"
},
{
"path": "components/settings-dialog.tsx",
"chars": 5050,
"preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\nimport {\n Dialog,"
},
{
"path": "components/sign-in-form.tsx",
"chars": 3481,
"preview": "/* eslint-disable @next/next/no-img-element */\n'use client'\n\nimport { useState } from 'react'\n\nimport { Button } from '@"
},
{
"path": "components/spinner/spinner.module.css",
"chars": 722,
"preview": ".ring {\n --uib-size: 25px;\n --uib-speed: 2s;\n --uib-color: #fefefe;\n \n height: var(--uib-size);\n width: var(--uib-"
},
{
"path": "components/spinner/spinner.tsx",
"chars": 242,
"preview": "import classes from './spinner.module.css'\n\nexport default function Spinner() {\n return (\n <>\n <svg className={"
},
{
"path": "components/ui/accordion.tsx",
"chars": 2024,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { Ch"
},
{
"path": "components/ui/back-button.tsx",
"chars": 983,
"preview": "'use client'\n\nimport React from 'react'\nimport { Text } from '@/components/ui/text'\nimport { Button } from '@/components"
},
{
"path": "components/ui/badge.tsx",
"chars": 1059,
"preview": "import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@/"
},
{
"path": "components/ui/button.tsx",
"chars": 3253,
"preview": "import { cn } from '@/utils/button-utils'\nimport Spinner from '@/components/spinner/spinner'\nimport { VariantProps, cva "
},
{
"path": "components/ui/circular-slider.tsx",
"chars": 1116,
"preview": "'use client'\n\nimport CircularSlider, {\n CircularSliderProps,\n} from '@fseehawer/react-circular-slider'\nimport { useBack"
},
{
"path": "components/ui/context-menu.tsx",
"chars": 7269,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport"
},
{
"path": "components/ui/dialog.tsx",
"chars": 4027,
"preview": "// @ts-nocheck (for no reason build fails with this file)\n'use client'\n\nimport * as React from 'react'\nimport * as Dialo"
},
{
"path": "components/ui/drawer.tsx",
"chars": 3029,
"preview": "'use client'\n\nimport * as React from 'react'\nimport { Drawer as DrawerPrimitive } from 'vaul'\n\nimport { cn } from '@/uti"
},
{
"path": "components/ui/dropdown-menu.tsx",
"chars": 7300,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimpo"
},
{
"path": "components/ui/gradient-text.tsx",
"chars": 1114,
"preview": "import { cn } from '@/utils/button-utils'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport { typ"
},
{
"path": "components/ui/input.tsx",
"chars": 818,
"preview": "import * as React from 'react'\n\nimport { cn } from '@/utils/button-utils'\n\nexport interface InputProps\n extends React.I"
},
{
"path": "components/ui/popover.tsx",
"chars": 1301,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\n\nimport { cn } "
},
{
"path": "components/ui/scroll-area.tsx",
"chars": 1656,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport "
},
{
"path": "components/ui/select.tsx",
"chars": 4439,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SelectPrimitive from '@radix-ui/react-select'\nimport { Check, C"
},
{
"path": "components/ui/separator.tsx",
"chars": 779,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\n\nimport { c"
},
{
"path": "components/ui/skeleton.tsx",
"chars": 270,
"preview": "import { cn } from \"@/utils/button-utils\"\n\nfunction Skeleton({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivEl"
},
{
"path": "components/ui/slider.tsx",
"chars": 2066,
"preview": "'use client'\n\nimport * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\n\nimport { cn } fr"
},
{
"path": "components/ui/spotlight-button.tsx",
"chars": 6893,
"preview": "'use client'\n\nimport { cn } from '@/utils/button-utils'\nimport { Sparkles, Wand, Wand2 } from 'lucide-react'\n\ntype Props"
},
{
"path": "components/ui/style/checkbox.module.css",
"chars": 658,
"preview": ".container {\n display: inline-block;\n vertical-align: middle;\n margin-right: 10px;\n cursor: pointer;\n position: rel"
},
{
"path": "components/ui/switch.tsx",
"chars": 1212,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } f"
},
{
"path": "components/ui/tabs.tsx",
"chars": 1941,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
},
{
"path": "components/ui/text-area.tsx",
"chars": 741,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/utils/button-utils\"\n\nexport interface TextareaProps\n extends Reac"
},
{
"path": "components/ui/text.tsx",
"chars": 3088,
"preview": "import { cn } from '@/utils/button-utils'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport { typ"
},
{
"path": "components/ui/theme-button.tsx",
"chars": 1454,
"preview": "'use client'\n\nimport * as React from 'react'\nimport { MoonStar, SunMoon } from 'lucide-react'\nimport { useTheme } from '"
},
{
"path": "components/ui/toast.tsx",
"chars": 4817,
"preview": "import * as React from 'react'\nimport * as ToastPrimitives from '@radix-ui/react-toast'\nimport { cva, type VariantProps "
},
{
"path": "components/ui/toaster.tsx",
"chars": 786,
"preview": "'use client'\n\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastProvider,\n ToastTitle,\n ToastViewport,\n} from"
},
{
"path": "components/ui/tooltip.tsx",
"chars": 1168,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } "
},
{
"path": "components/user-dropdown.tsx",
"chars": 4952,
"preview": "// dropdown menu for user profile when logged in\n\n'use client'\n\nimport { Button } from '@/components/ui/button'\nimport S"
},
{
"path": "components.json",
"chars": 334,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"default\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n"
},
{
"path": "docker-compose.yml",
"chars": 507,
"preview": "version: '3.8'\n\nservices:\n postgres:\n image: postgres:15-alpine\n container_name: prismify-db\n restart: unless-"
},
{
"path": "hooks/canvas-area-hooks/use-automatic-aspect-ratio-switcher.ts",
"chars": 2479,
"preview": "import { useColorExtractor } from '@/store/use-color-extractor'\nimport { useImageQualityStore } from '@/store/use-image-"
},
{
"path": "hooks/canvas-area-hooks/use-resize-observer.ts",
"chars": 1617,
"preview": "\nimport { useImageQualityStore } from '@/store/use-image-quality'\nimport { useResizeCanvas } from '@/store/use-resize-ca"
},
{
"path": "hooks/canvas-area-hooks/use-screen-size-warning-toast.ts",
"chars": 634,
"preview": "import React, { useEffect } from 'react'\nimport { toast } from '../use-toast'\n\n/**\n * This hook shows a warning toast if"
},
{
"path": "hooks/use-editor.ts",
"chars": 375,
"preview": "import { useEditor } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport { Color } from '@tiptap/ex"
},
{
"path": "hooks/use-event-listener.ts",
"chars": 2489,
"preview": "import { RefObject, useEffect, useRef } from 'react'\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout"
},
{
"path": "hooks/use-isomorphic-layout-effect.ts",
"chars": 155,
"preview": "import { useEffect, useLayoutEffect } from 'react'\n\nexport const useIsomorphicLayoutEffect =\n typeof window !== 'undefi"
},
{
"path": "hooks/use-media-query.ts",
"chars": 1060,
"preview": "\nimport { useEffect, useState } from 'react'\n\nexport function useMediaQuery(query: string): boolean {\n const getMatches"
},
{
"path": "hooks/use-on-click-outside.ts",
"chars": 556,
"preview": "import { RefObject } from 'react'\n\nimport { useEventListener } from './use-event-listener'\n\ntype Handler = (event: Mouse"
},
{
"path": "hooks/use-toast.ts",
"chars": 3922,
"preview": "// Inspired by react-hot-toast library\nimport * as React from 'react'\n\nimport type { ToastActionElement, ToastProps } fr"
},
{
"path": "index.d.ts",
"chars": 1855,
"preview": "/**\n * Represents a type that can be null or undefined.\n */\ntype Maybe = null | undefined\n\ndeclare module 'rgbaster' {\n "
},
{
"path": "libs/prismadb.ts",
"chars": 260,
"preview": "import { PrismaClient } from '@prisma/client'\n\ndeclare global {\n var prisma: PrismaClient | undefined\n}\n\nconst prismadb"
},
{
"path": "libs/validators/article-post-validator.ts",
"chars": 936,
"preview": "import { z } from 'zod'\nimport sanitizeHtml from 'sanitize-html'\n\nexport const postSchema = z\n .object({\n title: z\n "
},
{
"path": "libs/validators/user-settings-validator.ts",
"chars": 211,
"preview": "import { z } from 'zod'\n\nexport const userSettingsSchema = z.object({\n name: z.string().min(1).max(50).optional().or(z."
},
{
"path": "license.md",
"chars": 546,
"preview": "1. You are free to use Prismify for any purpose, commercial or non-commercial. While not required, I would appreciate it"
},
{
"path": "middleware.ts",
"chars": 292,
"preview": "import { withAuth } from 'next-auth/middleware'\n\nexport default withAuth(\n function middleware(req) {\n console.log('"
},
{
"path": "next.config.js",
"chars": 819,
"preview": "/** @type {import('next').NextConfig} */\n\nconst nextConfig = {\n images: {\n // domains: ['lh3.googleusercontent.com',"
},
{
"path": "package.json",
"chars": 3712,
"preview": "{\n \"name\": \"prismify\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"author\": {\n \"name\": \"Silson\"\n },\n \"repository\":"
},
{
"path": "postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "prettier.config.js",
"chars": 126,
"preview": "module.exports = {\n plugins: [require(\"prettier-plugin-tailwindcss\")],\n singleQuote: true,\n semi: false,\n tabWidth: "
},
{
"path": "prisma/schema.prisma",
"chars": 1816,
"preview": "generator client {\n provider = \"prisma-client-js\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env"
},
{
"path": "providers/index.tsx",
"chars": 802,
"preview": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { Provider } from 'react-w"
},
{
"path": "store/use-active-index.ts",
"chars": 282,
"preview": "import { create } from 'zustand'\n\ninterface ActiveIndexState {\n activeIndex: number\n setActiveIndex: (index: number) ="
},
{
"path": "store/use-background-options.ts",
"chars": 1921,
"preview": "import { create } from 'zustand'\n\ninterface BackgroundOptionsState {\n background: string\n setBackground: (background: "
},
{
"path": "store/use-color-extractor.ts",
"chars": 469,
"preview": "import { create } from 'zustand'\n\ninterface ActiveIndexState {\n extractedColor: {}\n setExtractedColor: (extractedColor"
},
{
"path": "store/use-frame-options.ts",
"chars": 1547,
"preview": "import { create } from 'zustand'\n\nexport type FrameTypes =\n | 'Arc'\n | 'MacOS Dark'\n | 'MacOS Light'\n | 'Shadow'\n |"
},
{
"path": "store/use-image-options.ts",
"chars": 6915,
"preview": "import { create, useStore } from 'zustand'\nimport { temporal, TemporalState } from 'zundo'\nimport throttle from 'just-th"
},
{
"path": "store/use-image-quality.ts",
"chars": 477,
"preview": "import { create } from 'zustand'\n\ninterface ImageQualityState {\n quality: number\n setQuality: (quality: number) => voi"
},
{
"path": "store/use-moveable.ts",
"chars": 1023,
"preview": "import { create } from 'zustand'\n\ninterface MoveableState {\n showControls: boolean\n setShowControls: (showControls: bo"
},
{
"path": "store/use-resize-canvas.ts",
"chars": 1471,
"preview": "import { create } from 'zustand'\n\ninterface ResizeCanvasState {\n resolution: string\n setResolution: (res: string) => v"
},
{
"path": "store/use-tiptap.ts",
"chars": 268,
"preview": "import { create } from 'zustand'\n\ninterface TiptapState {\n shouldShow: boolean\n setShouldShow: (shouldShow: boolean) ="
},
{
"path": "styles/globals.css",
"chars": 5674,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* A reusable h-dynamic-screen which has a fallback of vh if"
},
{
"path": "tailwind.config.js",
"chars": 2685,
"preview": "const { fontFamily } = require('tailwindcss/defaultTheme')\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports ="
},
{
"path": "tsconfig.json",
"chars": 705,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"sk"
},
{
"path": "utils/auth-options.ts",
"chars": 2857,
"preview": "import { PrismaAdapter } from '@next-auth/prisma-adapter'\nimport {\n type NextAuthOptions,\n type DefaultSession,\n getS"
},
{
"path": "utils/button-utils.ts",
"chars": 166,
"preview": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: Cla"
},
{
"path": "utils/helper-fns.ts",
"chars": 3438,
"preview": "export function splitWidthHeight(resolution: string) {\n const [width, height] = resolution.split('x')\n return { width,"
},
{
"path": "utils/presets/gradients.ts",
"chars": 7949,
"preview": "export interface Gradient {\n type: 'Normal' | 'Mesh'\n gradient: string\n background?: string\n}\n\nexport const gradients"
},
{
"path": "utils/presets/qualities.ts",
"chars": 240,
"preview": "export interface Quality {\n quality: string\n value: number\n}\n\nexport const qualities = [\n { quality: '0.5x Low', valu"
},
{
"path": "utils/presets/resolutions.ts",
"chars": 2169,
"preview": "export interface Resolution {\n name: string\n resolutions?: Array<{\n preset: string\n resolution: string\n }>\n ic"
},
{
"path": "utils/presets/shadows.ts",
"chars": 825,
"preview": "export interface Shadow {\n name: string\n fullName: string\n shadow: string\n preview: string\n}\n\nexport const shadows: "
},
{
"path": "utils/presets/solid-colors.ts",
"chars": 395,
"preview": "export const solidColors = [\n {\n background: 'transparent',\n },\n {\n background: '#cac2ff',\n },\n {\n backgro"
},
{
"path": "utils/tiptap-extensions.ts",
"chars": 867,
"preview": "import StarterKit from '@tiptap/starter-kit'\nimport Image from '@tiptap/extension-image'\nimport { Underline } from '@tip"
},
{
"path": "workers/background-removal.worker.ts",
"chars": 798,
"preview": "self.onmessage = async (event: MessageEvent<{ src: string }>) => {\n const { src } = event.data\n\n if (!src) {\n self."
}
]
About this extraction
This page contains the full source code of the Sls0n/Prismify GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 155 files (412.6 KB), approximately 109.8k tokens, and a symbol index with 167 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.