From 80c81fa30644f9a0268a90f22ad9c1352dc98d51 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Tue, 11 Nov 2025 18:07:19 +0530 Subject: [PATCH] added background image URL feature and integrated Prettier --- .prettierignore | 4 + .prettierrc | 7 + ARCHITECTURE.md | 38 +- CONTRIBUTING.md | 31 +- README.md | 20 +- app/api/cleanup-cache/route.ts | 14 +- app/api/screenshot/invalidate/route.ts | 41 +- app/api/screenshot/route.ts | 59 +- app/globals.css | 59 +- app/home/page.tsx | 8 +- app/landing/page.tsx | 19 +- app/layout.tsx | 105 +- app/page.tsx | 4 +- app/sitemap.ts | 3 +- components/ErrorBoundary.tsx | 32 +- components/SponsorButton.tsx | 93 +- .../aspect-ratio/aspect-ratio-dropdown.tsx | 29 +- .../aspect-ratio/aspect-ratio-picker.tsx | 86 +- components/aspect-ratio/index.ts | 5 +- components/canvas/CanvasContext.tsx | 846 ++++++----- components/canvas/ClientCanvas.tsx | 706 ++++----- components/canvas/EditorCanvas.tsx | 1 - components/canvas/EditorStoreSync.tsx | 10 +- components/canvas/dialogs/ExportDialog.tsx | 52 +- .../canvas/dialogs/SaveDesignDialog.tsx | 113 +- components/controls/BackgroundEffects.tsx | 34 +- components/controls/BorderControls.tsx | 174 +-- components/controls/BorderStyleSelector.tsx | 45 +- components/controls/Perspective3DControls.tsx | 73 +- components/controls/ShadowControls.tsx | 105 +- components/controls/UploadDropzone.tsx | 15 +- .../controls/WebsiteScreenshotInput.tsx | 50 +- components/editor/EditorContent.tsx | 17 +- components/editor/EditorHeader.tsx | 42 +- components/editor/EditorLayout.tsx | 46 +- components/editor/UploadArea.tsx | 118 +- components/editor/editor-bottom-bar.tsx | 55 +- components/editor/editor-left-panel.tsx | 94 +- components/editor/editor-right-panel.tsx | 413 ++--- components/editor/sidebar-left.tsx | 49 +- components/editor/style-tabs.tsx | 168 ++- components/export/FormatSelector.tsx | 13 +- components/export/QualitySlider.tsx | 25 +- components/export/ScaleSlider.tsx | 25 +- components/export/index.ts | 7 +- components/image-render/index.ts | 4 +- .../image-render/text-overlay-renderer.tsx | 22 +- components/landing/FAQ.tsx | 81 +- components/landing/Features.tsx | 15 +- components/landing/Footer.tsx | 16 +- components/landing/Hero.tsx | 56 +- components/landing/LandingPage.tsx | 57 +- components/landing/MasonryGrid.tsx | 58 +- components/landing/Navigation.tsx | 72 +- components/landing/Pricing.tsx | 35 +- components/landing/Sponsors.tsx | 315 ++-- components/mockups/ImacMockupRenderer.tsx | 1 - components/mockups/IphoneMockupRenderer.tsx | 2 - components/mockups/IwatchMockupRenderer.tsx | 1 - components/mockups/MacbookMockupRenderer.tsx | 2 - components/mockups/MockupControls.tsx | 52 +- components/mockups/MockupGallery.tsx | 42 +- components/mockups/MockupRenderer.tsx | 31 +- components/mockups/index.ts | 1 - components/overlays/index.ts | 1 - components/overlays/overlay-controls.tsx | 85 +- components/overlays/overlay-gallery.tsx | 3 +- components/overlays/overlay-renderer.tsx | 30 +- components/presets/PresetSelector.tsx | 185 +-- components/presets/index.ts | 3 +- components/templates/TemplatePreview.tsx | 26 +- components/templates/TemplateSelector.tsx | 40 +- components/text-overlay/index.ts | 3 +- .../text-overlay/text-overlay-controls.tsx | 545 ++++--- components/ui/accordion.tsx | 20 +- components/ui/alert-dialog.tsx | 60 +- components/ui/avatar.tsx | 30 +- components/ui/button.tsx | 43 +- components/ui/card.tsx | 51 +- components/ui/dialog.tsx | 50 +- components/ui/dropdown-menu.tsx | 69 +- components/ui/glass-input-wrapper.tsx | 67 +- components/ui/hero-video-dialog.tsx | 76 +- components/ui/input.tsx | 12 +- components/ui/label.tsx | 15 +- components/ui/moving-border.tsx | 103 +- components/ui/optimized-image.tsx | 43 +- components/ui/popover.tsx | 24 +- components/ui/select.tsx | 61 +- components/ui/separator.tsx | 12 +- components/ui/sheet.tsx | 63 +- components/ui/sidebar.tsx | 328 ++-- components/ui/skeleton.tsx | 6 +- components/ui/slider.tsx | 21 +- components/ui/tabs.tsx | 40 +- components/ui/tooltip.tsx | 18 +- hooks/use-mobile.ts | 6 +- hooks/useAspectRatioDimensions.ts | 112 +- hooks/useCanvas.ts | 18 +- hooks/useExport.ts | 283 ++-- lib/analytics.ts | 23 +- lib/aspect-ratio-utils.ts | 95 +- lib/canvas/templates.ts | 110 +- lib/canvas/utils.ts | 203 +-- lib/cloudinary-backgrounds.ts | 310 ++-- lib/cloudinary-demo-images.ts | 64 +- lib/cloudinary-overlays.ts | 136 +- lib/cloudinary.ts | 74 +- lib/constants.ts | 169 +-- lib/constants/aspect-ratios.ts | 25 +- lib/constants/backgrounds.ts | 74 +- lib/constants/fonts.ts | 42 +- lib/constants/gradient-colors.ts | 19 +- lib/constants/index.ts | 13 +- lib/constants/mockups.ts | 5 +- lib/constants/overlays.ts | 1 - lib/constants/presets.ts | 39 +- lib/constants/solid-colors.ts | 17 +- lib/db.ts | 1 - lib/export-storage.ts | 201 ++- lib/export/export-service.ts | 1333 +++++++++-------- lib/export/export-utils.ts | 305 ++-- lib/export/watermark.ts | 207 +-- lib/image-storage.ts | 129 +- lib/patterns.ts | 3 +- lib/rate-limit.ts | 13 +- lib/screenshot-cache.ts | 92 +- lib/store/export-utils.ts | 43 +- lib/store/index.ts | 109 +- lib/utils.ts | 4 +- next.config.ts | 10 +- package-lock.json | 17 + package.json | 5 +- postcss.config.mjs | 6 +- prisma.config.ts | 14 +- types/canvas.ts | 124 +- types/editor.ts | 91 +- types/mockup.ts | 1 - vercel.json | 1 - 139 files changed, 5775 insertions(+), 5721 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e6ab8f6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +build +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3b9953c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "semi": false, + "tabWidth": 2, + "printWidth": 100 +} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 272afd2..7cd4ebc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -7,31 +7,37 @@ Stage is a modern web-based canvas editor built with Next.js 16 and React 19. It ## Tech Stack ### Core Framework + - **Next.js 16** - React framework with App Router - **React 19** - UI library with React Compiler enabled - **TypeScript** - Type safety throughout the codebase ### Canvas & Rendering + - **Konva/React-Konva** - 2D canvas rendering engine for user images and overlays - **html2canvas** - DOM-to-canvas conversion for background rendering - **modern-screenshot** - 3D transform capture for perspective effects ### State Management + - **Zustand** - Lightweight state management with two main stores: - `useImageStore` - Main image and design state - `useEditorStore` - Canvas rendering state (synced with image store) ### Styling + - **Tailwind CSS 4** - Utility-first CSS framework - **Radix UI** - Accessible component primitives - **Lucide React** - Icon library ### Image Processing & Storage + - **Cloudinary** (optional) - Image optimization and CDN - **IndexedDB** - Client-side storage for images and exports - **Sharp** - Server-side image processing (dev dependency) ### Analytics + - **Umami** - Privacy-focused analytics ## Project Structure @@ -107,7 +113,9 @@ stage/ The application uses a dual-store pattern with Zustand: #### 1. Image Store (`useImageStore`) + Manages the main design state: + - Uploaded image URL and metadata - Background configuration (gradient, solid, image) - Text overlays array @@ -118,7 +126,9 @@ Manages the main design state: - Aspect ratio selection #### 2. Editor Store (`useEditorStore`) + Manages canvas rendering state: + - Screenshot/image state (for Konva) - Background mode (solid/gradient) - Shadow configuration @@ -138,6 +148,7 @@ The canvas rendering uses a hybrid approach: 3. **Overlay Layer** - Text and image overlays rendered separately, composited on top This separation allows: + - High-quality background rendering with CSS effects - Precise image positioning with Konva - Proper layering of overlays above user content @@ -183,6 +194,7 @@ Images are stored using a hybrid approach: - Export preferences When an image is uploaded: + - A blob URL is created for immediate use - The blob is saved to IndexedDB with a unique ID - The ID is stored in canvas objects for persistence @@ -190,17 +202,20 @@ When an image is uploaded: ### Component Architecture #### Layout Components + - `EditorLayout` - Main editor container with responsive panels - `EditorLeftPanel` - Left sidebar with controls - `EditorRightPanel` - Right sidebar with style options - `EditorBottomBar` - Bottom bar with export/actions #### Canvas Components + - `EditorCanvas` - Wrapper that shows upload UI or canvas - `ClientCanvas` - Main Konva canvas renderer (client-only) - `CanvasContext` - Context provider for canvas operations #### Control Components + - `BorderControls` - Border style and configuration - `ShadowControls` - Shadow customization - `Perspective3DControls` - 3D transform controls @@ -209,22 +224,27 @@ When an image is uploaded: ## Key Features Implementation ### 1. Image Upload + - **File Upload**: Uses `react-dropzone` for drag-and-drop - **Website Screenshot**: API route calls ScreenshotAPI.net service - **Validation**: File type and size validation - **Storage**: Blob URL creation + IndexedDB persistence ### 2. Background System + Supports three background types: + - **Gradient**: CSS linear gradients with customizable colors and angles - **Solid**: Single color backgrounds - **Image**: Cloudinary-hosted images or uploaded images Background effects: + - **Blur**: Applied via CSS filter, captured in export - **Noise**: Generated noise texture with overlay blend mode ### 3. Text Overlays + - Multiple text overlays with independent positioning - Custom fonts, colors, sizes, weights - Text shadows with customizable properties @@ -232,12 +252,14 @@ Background effects: - Position stored as percentage for responsive scaling ### 4. Image Overlays + - Decorative overlays from Cloudinary gallery - Custom uploaded overlays - Position, size, rotation, flip controls - Opacity and visibility toggles ### 5. Image Transformations + - **Scale**: Percentage-based scaling - **Opacity**: 0-100% opacity control - **Border Radius**: Rounded corners @@ -246,6 +268,7 @@ Background effects: - **3D Perspective**: CSS 3D transforms with perspective ### 6. Export System + - **Format**: PNG (with transparency support) - **Quality**: 0-1 quality slider - **Scale**: Up to 5x scaling for high-resolution exports @@ -253,7 +276,9 @@ Background effects: - **Storage**: Exported images saved to IndexedDB ### 7. Presets + Pre-configured design presets that apply: + - Aspect ratio - Background configuration - Border and shadow settings @@ -319,16 +344,19 @@ ClientCanvas re-renders with new state ## Performance Considerations ### Canvas Rendering + - Konva stage uses `batchDraw()` to minimize redraws - Pattern and noise textures are cached - Background images are loaded once and reused ### Export Performance + - Background and overlays exported separately to optimize memory - High-resolution exports use scaling instead of large canvas dimensions - Export operations are async to prevent UI blocking ### Image Loading + - Cloudinary images use optimized URLs with auto-format and quality - Images are cached in browser cache - IndexedDB provides persistent storage for offline access @@ -350,6 +378,7 @@ BETTER_AUTH_URL=https://your-domain.com ## API Routes ### `/api/screenshot` + - **Method**: POST - **Purpose**: Capture website screenshots using Puppeteer - **Body**: `{ url: string }` @@ -380,6 +409,7 @@ BETTER_AUTH_URL=https://your-domain.com ## Dependencies Overview ### Production Dependencies + - **next** (16.0.1) - React framework - **react** (19.2.0) - UI library - **konva** (10.0.8) - Canvas library @@ -392,6 +422,7 @@ BETTER_AUTH_URL=https://your-domain.com - **tailwindcss** (4) - Styling ### Development Dependencies + - **typescript** (5) - Type checking - **sharp** (0.34.4) - Image processing - **tsx** (4.20.6) - TypeScript execution @@ -407,6 +438,7 @@ BETTER_AUTH_URL=https://your-domain.com ## Future Architecture Considerations ### Potential Improvements + 1. **Web Workers**: Move heavy export operations to web workers 2. **Service Worker**: Cache assets and enable offline functionality 3. **Virtual Scrolling**: For large overlay galleries @@ -418,16 +450,19 @@ BETTER_AUTH_URL=https://your-domain.com ## Testing Strategy ### Unit Tests + - Store logic (Zustand stores) - Utility functions (export, image processing) - Component logic (hooks) ### Integration Tests + - Export pipeline - Store synchronization - Image upload flow ### E2E Tests + - Complete user workflows - Export functionality - Cross-browser compatibility @@ -435,6 +470,7 @@ BETTER_AUTH_URL=https://your-domain.com ## Deployment ### Vercel Configuration + - Serverless functions for API routes - Edge functions for static assets - Environment variables configured in Vercel dashboard @@ -446,6 +482,7 @@ npm run build # Next.js production build ``` ### Runtime Configuration + - `vercel.json` configures function timeouts and memory - Screenshot API route has 10s timeout and 1024MB memory @@ -458,4 +495,3 @@ npm run build # Next.js production build ## Contributing See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed contribution guidelines. - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e0e592..ab40076 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,18 +35,21 @@ Thank you for your interest in contributing to Stage! This document provides gui ### Development Setup 1. **Fork and Clone** + ```bash git clone https://github.com/your-username/stage.git cd stage ``` 2. **Install Dependencies** + ```bash npm install ``` 3. **Set Up Environment Variables** Create a `.env.local` file in the root directory: + ```env # Optional: Cloudinary Configuration NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name @@ -56,10 +59,11 @@ Thank you for your interest in contributing to Stage! This document provides gui # Optional: Screenshot API SCREENSHOTAPI_KEY=your-screenshot-api-key ``` - + Note: The app works without these, but some features (like Cloudinary image optimization) will be limited. 4. **Start Development Server** + ```bash npm run dev ``` @@ -150,6 +154,7 @@ import './styles.css' ### Branch Strategy 1. **Create a Feature Branch** + ```bash git checkout -b feature/your-feature-name # or @@ -185,6 +190,7 @@ Follow conventional commits format: ``` **Types:** + - `feat`: New feature - `fix`: Bug fix - `docs`: Documentation changes @@ -195,6 +201,7 @@ Follow conventional commits format: - `chore`: Maintenance tasks **Examples:** + ``` feat(export): add watermark to exported images fix(canvas): fix image positioning on resize @@ -212,6 +219,7 @@ refactor(store): simplify state management 4. Update types if needed Example: + ```typescript // components/controls/NewControl.tsx 'use client' @@ -220,7 +228,7 @@ import { useImageStore } from '@/lib/store' export function NewControl() { const { someValue, setSomeValue } = useImageStore() - + return (
{/* Your control UI */} @@ -276,6 +284,7 @@ npm run build ``` This will: + - Check TypeScript types - Verify imports - Catch compilation errors @@ -293,6 +302,7 @@ npm run lint ### Pull Request Process 1. **Push Your Branch** + ```bash git push origin feature/your-feature-name ``` @@ -304,25 +314,31 @@ npm run lint - Fill out the PR template 3. **PR Description Template** + ```markdown ## Description + Brief description of changes ## Type of Change + - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Testing + - [ ] Tested manually - [ ] TypeScript compiles without errors - [ ] No console errors ## Screenshots (if applicable) + [Add screenshots here] ## Checklist + - [ ] Code follows project style guidelines - [ ] Self-review completed - [ ] Comments added for complex code @@ -405,28 +421,35 @@ Use the bug report template and include: ```markdown ## Bug Description + [Clear description of the bug] ## Steps to Reproduce + 1. Go to '...' 2. Click on '...' 3. See error ## Expected Behavior + [What should happen] ## Actual Behavior + [What actually happens] ## Environment + - Browser: [e.g., Chrome 120] - OS: [e.g., macOS 14] - Device: [e.g., Desktop] ## Screenshots + [Add screenshots if applicable] ## Console Errors + [Any console errors] ``` @@ -443,7 +466,7 @@ Use the bug report template and include: ```typescript /** * Exports the canvas element as an image - * + * * @param elementId - ID of the element to export * @param options - Export options (format, quality, scale) * @param konvaStage - Konva stage instance @@ -481,6 +504,7 @@ export async function exportElement( ## Recognition Contributors will be: + - Listed in the project README (if desired) - Credited in release notes - Appreciated by the community! @@ -492,4 +516,3 @@ By contributing, you agree that your contributions will be licensed under the sa --- Thank you for contributing to Stage! 🎨 - diff --git a/README.md b/README.md index 6d2f5b2..64e8b4a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im ## Features ### Image Editing + - **Image Upload** - Drag & drop or file picker for image uploads - **Website Screenshots** - Capture screenshots of websites via URL - **Image Transformations** - Scale, opacity, rotation, and border radius controls @@ -18,6 +19,7 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im - **Shadows** - Customizable shadows with blur, offset, spread, and color controls ### Text & Overlays + - **Text Overlays** - Add multiple text layers with independent positioning - **Custom Fonts** - Choose from a variety of font families - **Text Styling** - Customize font size, weight, color, and opacity @@ -26,12 +28,14 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im - **Overlay Controls** - Position, size, rotation, flip, and opacity controls ### Backgrounds + - **Gradient Backgrounds** - Beautiful gradient presets with customizable colors and angles - **Solid Colors** - Choose from a palette of solid color backgrounds - **Image Backgrounds** - Upload your own or use Cloudinary-hosted backgrounds - **Background Effects** - Apply blur and noise effects to backgrounds ### Design Tools + - **Aspect Ratios** - Support for Instagram, social media, and standard formats - Square (1:1), Portrait (4:5, 9:16), Landscape (16:9) - Open Graph, Twitter Banner, LinkedIn Banner, YouTube Banner @@ -41,6 +45,7 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im - **Copy to Clipboard** - Copy designs directly to clipboard ### User Experience + - **Responsive Design** - Works seamlessly on desktop and mobile - **Real-time Preview** - See changes instantly as you edit - **Local Storage** - Designs persist in browser storage @@ -56,12 +61,14 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im ### Installation 1. **Clone the repository** + ```bash git clone https://github.com/your-username/stage.git cd stage ``` 2. **Install dependencies** + ```bash npm install ``` @@ -69,6 +76,7 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im 3. **Set up environment variables** Create a `.env.local` file in the root directory: + ```env # Database (Required for screenshot caching) DATABASE_URL="postgresql://user:password@host:port/dbname?schema=public" @@ -85,15 +93,17 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im > **Note**: Screenshot feature requires database and Cloudinary. All other core features including **export work fully in-browser**. Cloudinary is also used for optional image optimization of backgrounds and overlays. 4. **Set up the database** + ```bash # Run Prisma migrations to create the database schema npx prisma migrate dev --name init - + # Or use db push for quick setup (no migration files) npx prisma db push ``` 5. **Start the development server** + ```bash npm run dev ``` @@ -132,31 +142,37 @@ A modern web-based canvas editor for creating stunning visual designs. Upload im ## 🛠️ Tech Stack ### Core + - **[Next.js 16](https://nextjs.org/)** - React framework with App Router - **[React 19](https://react.dev/)** - UI library with React Compiler - **[TypeScript](https://www.typescriptlang.org/)** - Type safety ### Canvas & Rendering + - **[Konva](https://konvajs.org/)** - 2D canvas rendering engine - **[React-Konva](https://github.com/konvajs/react-konva)** - React bindings for Konva - **[html2canvas](https://html2canvas.hertzen.com/)** - DOM-to-canvas conversion - **[modern-screenshot](https://github.com/1000px/modern-screenshot)** - 3D transform capture ### State Management + - **[Zustand](https://github.com/pmndrs/zustand)** - Lightweight state management ### Styling + - **[Tailwind CSS 4](https://tailwindcss.com/)** - Utility-first CSS framework - **[Radix UI](https://www.radix-ui.com/)** - Accessible component primitives - **[Lucide React](https://lucide.dev/)** - Icon library ### Image Processing & Storage + - **[Cloudinary](https://cloudinary.com/)** - Image optimization, CDN, and screenshot storage - **[Sharp](https://sharp.pixelplumbing.com/)** - Server-side image processing - **[Puppeteer](https://pptr.dev/)** - Website screenshot capture - **[@sparticuz/chromium](https://github.com/Sparticuz/chromium)** - Chromium for serverless ### Database & Caching + - **[Prisma](https://www.prisma.io/)** - Type-safe ORM - **[PostgreSQL](https://www.postgresql.org/)** - Database for screenshot caching @@ -222,6 +238,7 @@ curl -X POST https://your-domain.com/api/cleanup-cache \ ### Rate Limiting The screenshot API includes built-in rate limiting: + - **Limit**: 20 requests per minute per IP - **Response**: 429 status with `Retry-After` header - **Headers**: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` @@ -229,6 +246,7 @@ The screenshot API includes built-in rate limiting: ### Cache Expiration Screenshot cache expires after 2 days to stay within Cloudinary free tier limits: + - **Storage**: 25 GB - **Bandwidth**: 25 GB/month - **Transformations**: 25,000/month diff --git a/app/api/cleanup-cache/route.ts b/app/api/cleanup-cache/route.ts index 08f652a..33e8e45 100644 --- a/app/api/cleanup-cache/route.ts +++ b/app/api/cleanup-cache/route.ts @@ -13,17 +13,13 @@ export async function POST(request: NextRequest) { } await clearOldCache() - - return NextResponse.json({ - success: true, - message: 'Cache cleanup completed' + + return NextResponse.json({ + success: true, + message: 'Cache cleanup completed', }) } catch (error) { console.error('Cache cleanup error:', error) - return NextResponse.json( - { error: 'Cache cleanup failed' }, - { status: 500 } - ) + return NextResponse.json({ error: 'Cache cleanup failed' }, { status: 500 }) } } - diff --git a/app/api/screenshot/invalidate/route.ts b/app/api/screenshot/invalidate/route.ts index 423be34..d007cd8 100644 --- a/app/api/screenshot/invalidate/route.ts +++ b/app/api/screenshot/invalidate/route.ts @@ -7,10 +7,7 @@ export async function POST(request: NextRequest) { const { url, urls } = body if (!url && !urls) { - return NextResponse.json( - { error: 'Either "url" or "urls" is required' }, - { status: 400 } - ) + return NextResponse.json({ error: 'Either "url" or "urls" is required' }, { status: 400 }) } if (url && urls) { @@ -22,10 +19,7 @@ export async function POST(request: NextRequest) { if (url) { if (typeof url !== 'string') { - return NextResponse.json( - { error: '"url" must be a string' }, - { status: 400 } - ) + return NextResponse.json({ error: '"url" must be a string' }, { status: 400 }) } try { @@ -37,10 +31,7 @@ export async function POST(request: NextRequest) { ) } } catch (error) { - return NextResponse.json( - { error: 'Invalid URL format' }, - { status: 400 } - ) + return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 }) } await invalidateCache(url) @@ -52,17 +43,11 @@ export async function POST(request: NextRequest) { if (urls) { if (!Array.isArray(urls)) { - return NextResponse.json( - { error: '"urls" must be an array' }, - { status: 400 } - ) + return NextResponse.json({ error: '"urls" must be an array' }, { status: 400 }) } if (urls.length === 0) { - return NextResponse.json( - { error: '"urls" array cannot be empty' }, - { status: 400 } - ) + return NextResponse.json({ error: '"urls" array cannot be empty' }, { status: 400 }) } for (const u of urls) { @@ -82,10 +67,7 @@ export async function POST(request: NextRequest) { ) } } catch (error) { - return NextResponse.json( - { error: `Invalid URL format: ${u}` }, - { status: 400 } - ) + return NextResponse.json({ error: `Invalid URL format: ${u}` }, { status: 400 }) } } @@ -97,16 +79,9 @@ export async function POST(request: NextRequest) { }) } - return NextResponse.json( - { error: 'Invalid request' }, - { status: 400 } - ) + return NextResponse.json({ error: 'Invalid request' }, { status: 400 }) } catch (error) { console.error('Error invalidating cache:', error) - return NextResponse.json( - { error: 'Failed to invalidate cache' }, - { status: 500 } - ) + return NextResponse.json({ error: 'Failed to invalidate cache' }, { status: 500 }) } } - diff --git a/app/api/screenshot/route.ts b/app/api/screenshot/route.ts index 1906017..fa978f1 100644 --- a/app/api/screenshot/route.ts +++ b/app/api/screenshot/route.ts @@ -1,13 +1,18 @@ import { NextRequest, NextResponse } from 'next/server' import chromium from '@sparticuz/chromium' -import { getCachedScreenshot, cacheScreenshot, normalizeUrl, invalidateCache } from '@/lib/screenshot-cache' +import { + getCachedScreenshot, + cacheScreenshot, + normalizeUrl, + invalidateCache, +} from '@/lib/screenshot-cache' import { checkRateLimit } from '@/lib/rate-limit' export const maxDuration = 10 async function getBrowser() { const isProduction = process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV - + // Memory-optimized args for serverless const memoryOptimizedArgs = [ '--no-sandbox', @@ -37,7 +42,7 @@ async function getBrowser() { '--disable-ipc-flooding-protection', '--disable-renderer-backgrounding', ] - + if (isProduction) { const puppeteerCore = await import('puppeteer-core') return await puppeteerCore.default.launch({ @@ -57,25 +62,26 @@ async function getBrowser() { export async function POST(request: NextRequest) { let browser = null - + try { - const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown' + const ip = + request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown' const rateLimit = checkRateLimit(ip) - + if (!rateLimit.allowed) { return NextResponse.json( - { + { error: 'Rate limit exceeded. Please try again later.', - retryAfter: Math.ceil((rateLimit.resetAt - Date.now()) / 1000) + retryAfter: Math.ceil((rateLimit.resetAt - Date.now()) / 1000), }, - { + { status: 429, headers: { 'Retry-After': Math.ceil((rateLimit.resetAt - Date.now()) / 1000).toString(), 'X-RateLimit-Limit': '20', 'X-RateLimit-Remaining': '0', - 'X-RateLimit-Reset': rateLimit.resetAt.toString() - } + 'X-RateLimit-Reset': rateLimit.resetAt.toString(), + }, } ) } @@ -84,26 +90,17 @@ export async function POST(request: NextRequest) { const { url, forceRefresh } = body if (!url || typeof url !== 'string') { - return NextResponse.json( - { error: 'URL is required' }, - { status: 400 } - ) + return NextResponse.json({ error: 'URL is required' }, { status: 400 }) } let validUrl: URL try { validUrl = new URL(url) if (!['http:', 'https:'].includes(validUrl.protocol)) { - return NextResponse.json( - { error: 'URL must use http or https protocol' }, - { status: 400 } - ) + return NextResponse.json({ error: 'URL must use http or https protocol' }, { status: 400 }) } } catch (error) { - return NextResponse.json( - { error: 'Invalid URL format' }, - { status: 400 } - ) + return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 }) } const normalizedUrl = normalizeUrl(validUrl.toString()) @@ -148,13 +145,13 @@ export async function POST(request: NextRequest) { timeout: 8000, }) - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 500)) - const screenshot = await page.screenshot({ + const screenshot = (await page.screenshot({ type: 'png', encoding: 'base64', fullPage: false, - }) as string + })) as string await browser.close() browser = null @@ -189,14 +186,20 @@ export async function POST(request: NextRequest) { ) } - if (error.message.includes('net::ERR_NAME_NOT_RESOLVED') || error.message.includes('net::ERR_CONNECTION_REFUSED')) { + if ( + error.message.includes('net::ERR_NAME_NOT_RESOLVED') || + error.message.includes('net::ERR_CONNECTION_REFUSED') + ) { return NextResponse.json( { error: 'Failed to connect to the website. Please check the URL and try again.' }, { status: 400 } ) } - if (error.message.includes('detached') || error.message.includes('LifecycleWatcher disposed')) { + if ( + error.message.includes('detached') || + error.message.includes('LifecycleWatcher disposed') + ) { return NextResponse.json( { error: 'Screenshot capture was interrupted. Please try again.' }, { status: 500 } diff --git a/app/globals.css b/app/globals.css index 5ad7097..3f42f02 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,25 +1,25 @@ -@import "tailwindcss"; -@import "tw-animate-css"; +@import 'tailwindcss'; +@import 'tw-animate-css'; @custom-variant dark (&:is(.dark *)); :root { - --background: oklch(1.0000 0 0); + --background: oklch(1 0 0); --foreground: oklch(0.2686 0 0); - --card: oklch(1.0000 0 0); + --card: oklch(1 0 0); --card-foreground: oklch(0.2686 0 0); - --popover: oklch(1.0000 0 0); + --popover: oklch(1 0 0); --popover-foreground: oklch(0.2686 0 0); --primary: oklch(0.7686 0.1647 70.0804); --primary-foreground: oklch(0 0 0); - --secondary: oklch(0.9670 0.0029 264.5419); + --secondary: oklch(0.967 0.0029 264.5419); --secondary-foreground: oklch(0.4461 0.0263 256.8018); --muted: oklch(0.9846 0.0017 247.8389); - --muted-foreground: oklch(0.5510 0.0234 264.3637); + --muted-foreground: oklch(0.551 0.0234 264.3637); --accent: oklch(0.9869 0.0214 95.2774); --accent-foreground: oklch(0.4732 0.1247 46.2007); --destructive: oklch(0.6368 0.2078 25.3313); - --destructive-foreground: oklch(1.0000 0 0); + --destructive-foreground: oklch(1 0 0); --border: oklch(0.9276 0.0058 264.5313); --input: oklch(0.9276 0.0058 264.5313); --ring: oklch(0.7686 0.1647 70.0804); @@ -31,7 +31,7 @@ --sidebar: oklch(0.9846 0.0017 247.8389); --sidebar-foreground: oklch(0.2686 0 0); --sidebar-primary: oklch(0.7686 0.1647 70.0804); - --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-primary-foreground: oklch(1 0 0); --sidebar-accent: oklch(0.9869 0.0214 95.2774); --sidebar-accent-foreground: oklch(0.4732 0.1247 46.2007); --sidebar-border: oklch(0.9276 0.0058 264.5313); @@ -48,11 +48,11 @@ --shadow-color: hsl(0 0% 0%); --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); - --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); - --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); - --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10); - --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10); - --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10); + --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); + --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); + --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1); + --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1); + --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1); --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); --tracking-normal: 0em; --spacing: 0.25rem; @@ -74,7 +74,7 @@ --accent: oklch(0.4732 0.1247 46.2007); --accent-foreground: oklch(0.9243 0.1151 95.7459); --destructive: oklch(0.6368 0.2078 25.3313); - --destructive-foreground: oklch(1.0000 0 0); + --destructive-foreground: oklch(1 0 0); --border: oklch(0.3715 0 0); --input: oklch(0.3715 0 0); --ring: oklch(0.7686 0.1647 70.0804); @@ -86,7 +86,7 @@ --sidebar: oklch(0.1684 0 0); --sidebar-foreground: oklch(0.9219 0 0); --sidebar-primary: oklch(0.7686 0.1647 70.0804); - --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-primary-foreground: oklch(1 0 0); --sidebar-accent: oklch(0.4732 0.1247 46.2007); --sidebar-accent-foreground: oklch(0.9243 0.1151 95.7459); --sidebar-border: oklch(0.3715 0 0); @@ -103,11 +103,11 @@ --shadow-color: hsl(0 0% 0%); --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); - --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); - --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10); - --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10); - --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10); - --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10); + --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); + --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); + --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1); + --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1); + --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1); --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); } @@ -182,19 +182,22 @@ border: 1px solid rgba(255, 255, 255, 0.3); } /* Improve touch targets on mobile */ - button, a, [role="button"] { + button, + a, + [role='button'] { @apply touch-manipulation; } /* Ensure minimum touch target size on mobile for interactive elements */ @media (max-width: 768px) { - button:not([class*="size"]):not([class*="h-"]), - a:not([class*="size"]):not([class*="h-"]), - [role="button"]:not([class*="size"]):not([class*="h-"]) { + button:not([class*='size']):not([class*='h-']), + a:not([class*='size']):not([class*='h-']), + [role='button']:not([class*='size']):not([class*='h-']) { min-height: 44px; } } /* Prevent text selection on mobile interactions */ - input, textarea { + input, + textarea { @apply text-base; /* Prevent zoom on iOS when focusing inputs */ font-size: 16px; @@ -207,7 +210,9 @@ } /* Prevent double tap zoom on mobile */ @media (max-width: 768px) { - button, a, [role="button"] { + button, + a, + [role='button'] { touch-action: manipulation; } } diff --git a/app/home/page.tsx b/app/home/page.tsx index 685ebd5..c4a592e 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -1,9 +1,9 @@ -import { EditorLayout } from "@/components/editor/EditorLayout"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { EditorLayout } from '@/components/editor/EditorLayout' +import { ErrorBoundary } from '@/components/ErrorBoundary' /** * Editor Page - Public Access - * + * * This page is now publicly accessible without authentication. */ export default async function EditorPage() { @@ -11,5 +11,5 @@ export default async function EditorPage() { - ); + ) } diff --git a/app/landing/page.tsx b/app/landing/page.tsx index 13dcf32..70c204f 100644 --- a/app/landing/page.tsx +++ b/app/landing/page.tsx @@ -1,22 +1,22 @@ -import { LandingPage } from "@/components/landing/LandingPage"; +import { LandingPage } from '@/components/landing/LandingPage' const features = [ { - title: "Image Upload & Customization", + title: 'Image Upload & Customization', description: - "Upload images and customize size, opacity, borders, shadows, and border radius for complete control over your visuals.", + 'Upload images and customize size, opacity, borders, shadows, and border radius for complete control over your visuals.', }, { - title: "Text Overlays & Backgrounds", + title: 'Text Overlays & Backgrounds', description: - "Add multiple text layers with custom fonts, colors, and shadows. Choose from gradients, solid colors, or upload your own backgrounds.", + 'Add multiple text layers with custom fonts, colors, and shadows. Choose from gradients, solid colors, or upload your own backgrounds.', }, { - title: "Professional Export", + title: 'Professional Export', description: - "Export as PNG (with transparency) with adjustable quality and scale up to 5x. All processing happens in your browser—no external services required. Perfect for social media and high-resolution output.", + 'Export as PNG (with transparency) with adjustable quality and scale up to 5x. All processing happens in your browser—no external services required. Perfect for social media and high-resolution output.', }, -]; +] export default function Landing() { return ( @@ -28,6 +28,5 @@ export default function Landing() { ctaHref="/home" features={features} /> - ); + ) } - diff --git a/app/layout.tsx b/app/layout.tsx index 8c594cf..a3d1e4f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,50 +1,61 @@ -import type { Metadata, Viewport } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import type { Metadata, Viewport } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); + variable: '--font-geist-sans', + subsets: ['latin'], +}) const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); + variable: '--font-geist-mono', + subsets: ['latin'], +}) export const metadata: Metadata = { title: { - default: "Stage - Image Showcase Builder", - template: "%s | Stage", + default: 'Stage - Image Showcase Builder', + template: '%s | Stage', }, - description: "Create stunning showcase images for your projects with customizable templates and layouts. A fully in-browser canvas editor for adding images, text, and backgrounds—no external services required.", - keywords: ["image editor", "canvas editor", "design tool", "image showcase", "template builder", "in-browser editor", "client-side export"], - authors: [{ name: "Stage" }], - creator: "Stage", - publisher: "Stage", - metadataBase: new URL(process.env.BETTER_AUTH_URL || "https://stage-psi-one.vercel.app"), + description: + 'Create stunning showcase images for your projects with customizable templates and layouts. A fully in-browser canvas editor for adding images, text, and backgrounds—no external services required.', + keywords: [ + 'image editor', + 'canvas editor', + 'design tool', + 'image showcase', + 'template builder', + 'in-browser editor', + 'client-side export', + ], + authors: [{ name: 'Stage' }], + creator: 'Stage', + publisher: 'Stage', + metadataBase: new URL(process.env.BETTER_AUTH_URL || 'https://stage-psi-one.vercel.app'), openGraph: { - type: "website", - locale: "en_US", - url: "/", - siteName: "Stage", - title: "Stage - Image Showcase Builder", - description: "Create stunning showcase images for your projects with customizable templates and layouts", + type: 'website', + locale: 'en_US', + url: '/', + siteName: 'Stage', + title: 'Stage - Image Showcase Builder', + description: + 'Create stunning showcase images for your projects with customizable templates and layouts', images: [ { - url: "https://stage-psi-one.vercel.app/og.png", + url: 'https://stage-psi-one.vercel.app/og.png', width: 1200, height: 630, - alt: "Stage - Image Showcase Builder", + alt: 'Stage - Image Showcase Builder', }, ], }, twitter: { - card: "summary_large_image", - title: "Stage - Image Showcase Builder", - description: "Create stunning showcase images for your projects with customizable templates and layouts", - images: ["https://stage-psi-one.vercel.app/og.png"], - creator: "@stage", + card: 'summary_large_image', + title: 'Stage - Image Showcase Builder', + description: + 'Create stunning showcase images for your projects with customizable templates and layouts', + images: ['https://stage-psi-one.vercel.app/og.png'], + creator: '@stage', }, robots: { index: true, @@ -52,40 +63,40 @@ export const metadata: Metadata = { googleBot: { index: true, follow: true, - "max-video-preview": -1, - "max-image-preview": "large", - "max-snippet": -1, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, }, }, icons: { - icon: "/logo.png", - shortcut: "/logo.png", - apple: "/logo.png", + icon: '/logo.png', + shortcut: '/logo.png', + apple: '/logo.png', }, -}; +} export const viewport: Viewport = { - width: "device-width", + width: 'device-width', initialScale: 1, maximumScale: 5, userScalable: true, -}; +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( - + - - {children} - + {children} - ); + ) } diff --git a/app/page.tsx b/app/page.tsx index 3021415..79ad417 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ -import { redirect } from "next/navigation"; +import { redirect } from 'next/navigation' export default function Home() { - redirect("/home"); + redirect('/home') } diff --git a/app/sitemap.ts b/app/sitemap.ts index 60eb3ee..bbe15e5 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -2,7 +2,7 @@ import { MetadataRoute } from 'next' export default function sitemap(): MetadataRoute.Sitemap { const baseUrl = process.env.BETTER_AUTH_URL || 'https://stage-psi-one.vercel.app' - + return [ { url: baseUrl, @@ -24,4 +24,3 @@ export default function sitemap(): MetadataRoute.Sitemap { }, ] } - diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index 05d30de..961a47a 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,12 +1,12 @@ -"use client"; +'use client' -import React from "react"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import React from 'react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' interface ErrorBoundaryState { - hasError: boolean; - error: Error | null; + hasError: boolean + error: Error | null } export class ErrorBoundary extends React.Component< @@ -14,16 +14,16 @@ export class ErrorBoundary extends React.Component< ErrorBoundaryState > { constructor(props: { children: React.ReactNode }) { - super(props); - this.state = { hasError: false, error: null }; + super(props) + this.state = { hasError: false, error: null } } static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { hasError: true, error }; + return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - console.error("Error caught by boundary:", error, errorInfo); + console.error('Error caught by boundary:', error, errorInfo) } render() { @@ -33,9 +33,7 @@ export class ErrorBoundary extends React.Component< Something went wrong - - An error occurred while loading the editor. - + An error occurred while loading the editor. {this.state.error && ( @@ -45,8 +43,8 @@ export class ErrorBoundary extends React.Component< )}
- ); + ) } - return this.props.children; + return this.props.children } } diff --git a/components/SponsorButton.tsx b/components/SponsorButton.tsx index 609dd8f..3c63db6 100644 --- a/components/SponsorButton.tsx +++ b/components/SponsorButton.tsx @@ -1,36 +1,32 @@ -'use client'; +'use client' -import * as React from 'react'; -import { Button } from '@/components/ui/button'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { FaCoffee, FaDollarSign, FaMobileAlt, FaCopy, FaCheck, FaHeart } from 'react-icons/fa'; -import { cn } from '@/lib/utils'; -import { Heart } from 'lucide-react'; -import { Button as MovingBorderButton } from '@/components/ui/moving-border'; +import * as React from 'react' +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { FaCoffee, FaDollarSign, FaMobileAlt, FaCopy, FaCheck, FaHeart } from 'react-icons/fa' +import { cn } from '@/lib/utils' +import { Heart } from 'lucide-react' +import { Button as MovingBorderButton } from '@/components/ui/moving-border' interface SponsorButtonProps { - className?: string; - variant?: 'bar' | 'floating'; + className?: string + variant?: 'bar' | 'floating' } export function SponsorButton({ className, variant = 'bar' }: SponsorButtonProps) { - const [isOpen, setIsOpen] = React.useState(false); - const [copied, setCopied] = React.useState(false); - const upiId = 'kartik.labhshetwar@oksbi'; + const [isOpen, setIsOpen] = React.useState(false) + const [copied, setCopied] = React.useState(false) + const upiId = 'kartik.labhshetwar@oksbi' const handleCopy = async () => { try { - await navigator.clipboard.writeText(upiId); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + await navigator.clipboard.writeText(upiId) + setCopied(true) + setTimeout(() => setCopied(false), 2000) } catch (err) { - console.error('Failed to copy:', err); + console.error('Failed to copy:', err) } - }; + } if (variant === 'floating') { return ( @@ -44,17 +40,12 @@ export function SponsorButton({ className, variant = 'bar' }: SponsorButtonProps - + - ); + ) } return ( @@ -63,7 +54,7 @@ export function SponsorButton({ className, variant = 'bar' }: SponsorButtonProps Sponsor - + - ); + ) } -function SponsorContent({ - upiId, - copied, - onCopy -}: { - upiId: string; - copied: boolean; - onCopy: () => void; +function SponsorContent({ + upiId, + copied, + onCopy, +}: { + upiId: string + copied: boolean + onCopy: () => void }) { return (
@@ -132,9 +118,7 @@ function SponsorContent({
- - UPI Payment - + UPI Payment
Scan QR or copy UPI ID
- + {/* QR Code */}
- UPI QR Code
- +

UPI ID: {upiId}

Scan to pay with any UPI app

- ); + ) } - diff --git a/components/aspect-ratio/aspect-ratio-dropdown.tsx b/components/aspect-ratio/aspect-ratio-dropdown.tsx index 4096737..b7aa0da 100644 --- a/components/aspect-ratio/aspect-ratio-dropdown.tsx +++ b/components/aspect-ratio/aspect-ratio-dropdown.tsx @@ -1,19 +1,15 @@ -import { - Popover, - PopoverTrigger, - PopoverContent, -} from '@/components/ui/popover'; -import { aspectRatios } from '@/lib/constants/aspect-ratios'; -import { useImageStore } from '@/lib/store'; -import { AspectRatioPicker } from './aspect-ratio-picker'; -import { Button } from '@/components/ui/button'; -import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper'; -import * as React from 'react'; +import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' +import { aspectRatios } from '@/lib/constants/aspect-ratios' +import { useImageStore } from '@/lib/store' +import { AspectRatioPicker } from './aspect-ratio-picker' +import { Button } from '@/components/ui/button' +import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper' +import * as React from 'react' export const AspectRatioDropdown = () => { - const { selectedAspectRatio } = useImageStore(); - const current = aspectRatios.find((ar) => ar.id === selectedAspectRatio); - const [open, setOpen] = React.useState(false); + const { selectedAspectRatio } = useImageStore() + const current = aspectRatios.find((ar) => ar.id === selectedAspectRatio) + const [open, setOpen] = React.useState(false) return ( @@ -45,6 +41,5 @@ export const AspectRatioDropdown = () => { setOpen(false)} />
- ); -}; - + ) +} diff --git a/components/aspect-ratio/aspect-ratio-picker.tsx b/components/aspect-ratio/aspect-ratio-picker.tsx index e733883..0e09d46 100644 --- a/components/aspect-ratio/aspect-ratio-picker.tsx +++ b/components/aspect-ratio/aspect-ratio-picker.tsx @@ -1,28 +1,33 @@ -import { aspectRatios } from '@/lib/constants/aspect-ratios'; -import { useImageStore } from '@/lib/store'; +import { aspectRatios } from '@/lib/constants/aspect-ratios' +import { useImageStore } from '@/lib/store' interface AspectRatioPickerProps { - onSelect?: () => void; + onSelect?: () => void } -export const AspectRatioPicker = ({ onSelect }: AspectRatioPickerProps = {} as AspectRatioPickerProps) => { - const { selectedAspectRatio, setAspectRatio } = useImageStore(); +export const AspectRatioPicker = ( + { onSelect }: AspectRatioPickerProps = {} as AspectRatioPickerProps +) => { + const { selectedAspectRatio, setAspectRatio } = useImageStore() // Group aspect ratios by category - const groupedRatios = aspectRatios.reduce((acc, ratio) => { - if (!acc[ratio.category]) { - acc[ratio.category] = []; - } - acc[ratio.category].push(ratio); - return acc; - }, {} as Record); + const groupedRatios = aspectRatios.reduce( + (acc, ratio) => { + if (!acc[ratio.category]) { + acc[ratio.category] = [] + } + acc[ratio.category].push(ratio) + return acc + }, + {} as Record + ) - const categories = Object.keys(groupedRatios); + const categories = Object.keys(groupedRatios) const handleSelect = (id: string) => { - setAspectRatio(id); - onSelect?.(); - }; + setAspectRatio(id) + onSelect?.() + } return (
@@ -33,7 +38,7 @@ export const AspectRatioPicker = ({ onSelect }: AspectRatioPickerProps = {} as A
{groupedRatios[category].map((aspectRatio) => { - const isSelected = selectedAspectRatio === aspectRatio.id; + const isSelected = selectedAspectRatio === aspectRatio.id return ( - ); + ) })}
))} - ); -}; - + ) +} diff --git a/components/aspect-ratio/index.ts b/components/aspect-ratio/index.ts index 8d88089..7b6c559 100644 --- a/components/aspect-ratio/index.ts +++ b/components/aspect-ratio/index.ts @@ -1,4 +1,3 @@ // Export all aspect ratio components -export { AspectRatioDropdown } from './aspect-ratio-dropdown'; -export { AspectRatioPicker } from './aspect-ratio-picker'; - +export { AspectRatioDropdown } from './aspect-ratio-dropdown' +export { AspectRatioPicker } from './aspect-ratio-picker' diff --git a/components/canvas/CanvasContext.tsx b/components/canvas/CanvasContext.tsx index 63f502b..6db3027 100644 --- a/components/canvas/CanvasContext.tsx +++ b/components/canvas/CanvasContext.tsx @@ -1,615 +1,653 @@ -"use client"; +'use client' -import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from "react"; -import Konva from "konva"; -import type { CanvasOperations } from "@/types/editor"; -import type { AspectRatioPreset } from "@/lib/constants"; -import { DEFAULT_ASPECT_RATIO } from "@/lib/constants"; -import { saveImageBlob, getBlobUrlFromStored, deleteImageBlob } from "@/lib/image-storage"; +import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react' +import Konva from 'konva' +import type { CanvasOperations } from '@/types/editor' +import type { AspectRatioPreset } from '@/lib/constants' +import { DEFAULT_ASPECT_RATIO } from '@/lib/constants' +import { saveImageBlob, getBlobUrlFromStored, deleteImageBlob } from '@/lib/image-storage' -const CANVAS_OBJECTS_KEY = "canvas-objects"; -const BACKGROUND_PREFS_KEY = "canvas-background-prefs"; +const CANVAS_OBJECTS_KEY = 'canvas-objects' +const BACKGROUND_PREFS_KEY = 'canvas-background-prefs' interface CanvasObject { - id: string; - type: "image" | "text"; - x: number; - y: number; - width?: number; - height?: number; - scaleX?: number; - scaleY?: number; - rotation?: number; - elevationX?: number; // Tilt/perspective around X axis (vertical perspective) - elevationY?: number; // Tilt/perspective around Y axis (horizontal perspective) - fill?: string; - fontSize?: number; - text?: string; - imageUrl?: string; - image?: HTMLImageElement; - [key: string]: any; + id: string + type: 'image' | 'text' + x: number + y: number + width?: number + height?: number + scaleX?: number + scaleY?: number + rotation?: number + elevationX?: number // Tilt/perspective around X axis (vertical perspective) + elevationY?: number // Tilt/perspective around Y axis (horizontal perspective) + fill?: string + fontSize?: number + text?: string + imageUrl?: string + image?: HTMLImageElement + [key: string]: any } interface CanvasContextType { - stage: Konva.Stage | null; - layer: Konva.Layer | null; - initializeCanvas: (stage: Konva.Stage, layer: Konva.Layer) => void; - operations: CanvasOperations; - selectedObject: CanvasObject | null; - objects: CanvasObject[]; + stage: Konva.Stage | null + layer: Konva.Layer | null + initializeCanvas: (stage: Konva.Stage, layer: Konva.Layer) => void + operations: CanvasOperations + selectedObject: CanvasObject | null + objects: CanvasObject[] history: { - undo: () => void; - redo: () => void; - save: () => void; - }; - canvasDimensions: { width: number; height: number }; - aspectRatio: AspectRatioPreset; - setAspectRatio: (preset: AspectRatioPreset) => void; - setCanvasDimensions: (width: number, height: number) => void; - saveDesign: () => Promise<{ canvasData: any; previewUrl?: string }>; - loadDesign: (canvasData: any) => Promise; + undo: () => void + redo: () => void + save: () => void + } + canvasDimensions: { width: number; height: number } + aspectRatio: AspectRatioPreset + setAspectRatio: (preset: AspectRatioPreset) => void + setCanvasDimensions: (width: number, height: number) => void + saveDesign: () => Promise<{ canvasData: any; previewUrl?: string }> + loadDesign: (canvasData: any) => Promise } -const CanvasContext = createContext(undefined); +const CanvasContext = createContext(undefined) export function CanvasProvider({ children }: { children: React.ReactNode }) { - const [stage, setStage] = useState(null); - const [layer, setLayer] = useState(null); - const [selectedObject, setSelectedObject] = useState(null); - const [objects, setObjects] = useState([]); - const [aspectRatio, setAspectRatioState] = useState(DEFAULT_ASPECT_RATIO); + const [stage, setStage] = useState(null) + const [layer, setLayer] = useState(null) + const [selectedObject, setSelectedObject] = useState(null) + const [objects, setObjects] = useState([]) + const [aspectRatio, setAspectRatioState] = useState(DEFAULT_ASPECT_RATIO) const [canvasDimensions, setCanvasDimensionsState] = useState<{ width: number; height: number }>({ width: DEFAULT_ASPECT_RATIO.width, height: DEFAULT_ASPECT_RATIO.height, - }); - const historyRef = useRef<{ past: CanvasObject[][]; present: CanvasObject[]; future: CanvasObject[][] }>({ + }) + const historyRef = useRef<{ + past: CanvasObject[][] + present: CanvasObject[] + future: CanvasObject[][] + }>({ past: [], present: [], future: [], - }); - - const setAspectRatio = useCallback((preset: AspectRatioPreset) => { - setAspectRatioState(preset); - setCanvasDimensionsState({ - width: preset.width, - height: preset.height, - }); - - // Update stage dimensions if it exists - if (stage) { - stage.width(preset.width); - stage.height(preset.height); - if (layer) { - // Update background rectangle - const bgRect = layer.findOne((node: any) => node.id() === "canvas-background") as Konva.Rect; - if (bgRect && bgRect instanceof Konva.Rect) { - bgRect.width(preset.width); - bgRect.height(preset.height); - layer.batchDraw(); + }) + + const setAspectRatio = useCallback( + (preset: AspectRatioPreset) => { + setAspectRatioState(preset) + setCanvasDimensionsState({ + width: preset.width, + height: preset.height, + }) + + // Update stage dimensions if it exists + if (stage) { + stage.width(preset.width) + stage.height(preset.height) + if (layer) { + // Update background rectangle + const bgRect = layer.findOne( + (node: any) => node.id() === 'canvas-background' + ) as Konva.Rect + if (bgRect && bgRect instanceof Konva.Rect) { + bgRect.width(preset.width) + bgRect.height(preset.height) + layer.batchDraw() + } } } - } - }, [stage, layer]); - - const setCanvasDimensions = useCallback((width: number, height: number) => { - setCanvasDimensionsState({ width, height }); - if (stage) { - stage.width(width); - stage.height(height); - if (layer) { - const bgRect = layer.findOne((node: any) => node.id() === "canvas-background") as Konva.Rect; - if (bgRect && bgRect instanceof Konva.Rect) { - bgRect.width(width); - bgRect.height(height); - layer.batchDraw(); + }, + [stage, layer] + ) + + const setCanvasDimensions = useCallback( + (width: number, height: number) => { + setCanvasDimensionsState({ width, height }) + if (stage) { + stage.width(width) + stage.height(height) + if (layer) { + const bgRect = layer.findOne( + (node: any) => node.id() === 'canvas-background' + ) as Konva.Rect + if (bgRect && bgRect instanceof Konva.Rect) { + bgRect.width(width) + bgRect.height(height) + layer.batchDraw() + } } } - } - }, [stage, layer]); + }, + [stage, layer] + ) const initializeCanvas = useCallback((stageInstance: Konva.Stage, layerInstance: Konva.Layer) => { - setStage(stageInstance); - setLayer(layerInstance); - + setStage(stageInstance) + setLayer(layerInstance) + // Initialize history historyRef.current = { past: [], present: [], future: [], - }; - + } + // Restore objects from localStorage try { - const saved = localStorage.getItem(CANVAS_OBJECTS_KEY); + const saved = localStorage.getItem(CANVAS_OBJECTS_KEY) if (saved) { - const savedObjects: CanvasObject[] = JSON.parse(saved); + const savedObjects: CanvasObject[] = JSON.parse(saved) // Restore image objects by recreating the images from their URLs const restorePromises = savedObjects.map(async (obj) => { - if (obj.type === "image" && obj.imageUrl) { - let imageSrc = obj.imageUrl; - + if (obj.type === 'image' && obj.imageUrl) { + let imageSrc = obj.imageUrl + // If it's a stored image ID (not starting with blob: or http: or data:), get from IndexedDB - if (!imageSrc.startsWith("blob:") && !imageSrc.startsWith("http") && !imageSrc.startsWith("data:")) { - const blobUrl = await getBlobUrlFromStored(imageSrc); + if ( + !imageSrc.startsWith('blob:') && + !imageSrc.startsWith('http') && + !imageSrc.startsWith('data:') + ) { + const blobUrl = await getBlobUrlFromStored(imageSrc) if (blobUrl) { - imageSrc = blobUrl; + imageSrc = blobUrl } else { - console.warn(`Image blob not found for ID: ${imageSrc}`); - return null; // Skip this object if blob not found + console.warn(`Image blob not found for ID: ${imageSrc}`) + return null // Skip this object if blob not found } } - + const img = await new Promise((resolve, reject) => { - const image = new Image(); - image.crossOrigin = "anonymous"; - image.onload = () => resolve(image); - image.onerror = reject; - image.src = imageSrc; - }); - return { ...obj, image: img, imageUrl: imageSrc }; + const image = new Image() + image.crossOrigin = 'anonymous' + image.onload = () => resolve(image) + image.onerror = reject + image.src = imageSrc + }) + return { ...obj, image: img, imageUrl: imageSrc } } - return obj; - }); - + return obj + }) + Promise.all(restorePromises).then((restoredObjects) => { // Filter out null objects (failed restorations) - const validObjects = restoredObjects.filter(obj => obj !== null) as CanvasObject[]; - setObjects(validObjects); - historyRef.current.present = [...validObjects]; + const validObjects = restoredObjects.filter((obj) => obj !== null) as CanvasObject[] + setObjects(validObjects) + historyRef.current.present = [...validObjects] // Trigger a draw after a short delay to ensure layer is ready setTimeout(() => { if (layerInstance) { - layerInstance.batchDraw(); + layerInstance.batchDraw() } - }, 100); - }); + }, 100) + }) } } catch (error) { - console.error("Failed to restore canvas objects:", error); + console.error('Failed to restore canvas objects:', error) } - }, []); + }, []) const saveState = useCallback(() => { - const currentState = [...objects]; - historyRef.current.past.push([...historyRef.current.present]); - historyRef.current.present = [...currentState]; - historyRef.current.future = []; - }, [objects]); + const currentState = [...objects] + historyRef.current.past.push([...historyRef.current.present]) + historyRef.current.present = [...currentState] + historyRef.current.future = [] + }, [objects]) // Helper function to save objects to localStorage const saveObjectsToStorage = useCallback((objectsToSave: CanvasObject[]) => { try { - localStorage.setItem(CANVAS_OBJECTS_KEY, JSON.stringify(objectsToSave.map(obj => ({ - ...obj, - image: undefined, // Don't store image element, just the URL - })))); + localStorage.setItem( + CANVAS_OBJECTS_KEY, + JSON.stringify( + objectsToSave.map((obj) => ({ + ...obj, + image: undefined, // Don't store image element, just the URL + })) + ) + ) } catch (error) { - console.error("Failed to save canvas objects:", error); + console.error('Failed to save canvas objects:', error) } - }, []); + }, []) const undo = useCallback(() => { - if (historyRef.current.past.length === 0) return; - const previous = historyRef.current.past.pop()!; + if (historyRef.current.past.length === 0) return + const previous = historyRef.current.past.pop()! if (previous && Array.isArray(previous)) { - historyRef.current.future.unshift([...historyRef.current.present]); - historyRef.current.present = [...previous]; - setObjects([...previous]); - saveObjectsToStorage(previous); - setSelectedObject(null); + historyRef.current.future.unshift([...historyRef.current.present]) + historyRef.current.present = [...previous] + setObjects([...previous]) + saveObjectsToStorage(previous) + setSelectedObject(null) } - }, [saveObjectsToStorage]); + }, [saveObjectsToStorage]) const redo = useCallback(() => { - if (historyRef.current.future.length === 0) return; - const next = historyRef.current.future.shift()!; + if (historyRef.current.future.length === 0) return + const next = historyRef.current.future.shift()! if (next && Array.isArray(next)) { - historyRef.current.past.push([...historyRef.current.present]); - historyRef.current.present = [...next]; - setObjects([...next]); - saveObjectsToStorage(next); - setSelectedObject(null); + historyRef.current.past.push([...historyRef.current.present]) + historyRef.current.present = [...next] + setObjects([...next]) + saveObjectsToStorage(next) + setSelectedObject(null) } - }, [saveObjectsToStorage]); + }, [saveObjectsToStorage]) const operations: CanvasOperations = { addImage: async (imageUrl, options = {}) => { - if (!stage || !layer) return; - + if (!stage || !layer) return + try { const img = await new Promise((resolve, reject) => { - const image = new Image(); - image.crossOrigin = "anonymous"; - image.onload = () => resolve(image); - image.onerror = reject; - image.src = imageUrl; - }); - - const canvasWidth = stage.width(); - const canvasHeight = stage.height(); - const imgWidth = img.width || 1; - const imgHeight = img.height || 1; + const image = new Image() + image.crossOrigin = 'anonymous' + image.onload = () => resolve(image) + image.onerror = reject + image.src = imageUrl + }) + + const canvasWidth = stage.width() + const canvasHeight = stage.height() + const imgWidth = img.width || 1 + const imgHeight = img.height || 1 // Calculate scale to fit image within canvas while maintaining aspect ratio // Use a padding factor to ensure the image fits comfortably (0.9 means 90% of canvas) - const paddingFactor = 0.9; - const availableWidth = canvasWidth * paddingFactor; - const availableHeight = canvasHeight * paddingFactor; - + const paddingFactor = 0.9 + const availableWidth = canvasWidth * paddingFactor + const availableHeight = canvasHeight * paddingFactor + // Calculate scale to fit image within available space - const scaleX = availableWidth / imgWidth; - const scaleY = availableHeight / imgHeight; - const scale = Math.min(scaleX, scaleY, 1); // Don't scale up beyond original size + const scaleX = availableWidth / imgWidth + const scaleY = availableHeight / imgHeight + const scale = Math.min(scaleX, scaleY, 1) // Don't scale up beyond original size // Calculate scaled dimensions - const scaledWidth = imgWidth * scale; - const scaledHeight = imgHeight * scale; + const scaledWidth = imgWidth * scale + const scaledHeight = imgHeight * scale // Center the image on the canvas - const centerX = (canvasWidth - scaledWidth) / 2; - const centerY = (canvasHeight - scaledHeight) / 2; - - const imageId = `image-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - // If it's a blob URL, save the blob to IndexedDB for persistence - if (imageUrl.startsWith("blob:")) { - try { - // Fetch the blob from the blob URL - const response = await fetch(imageUrl); - const blob = await response.blob(); - // Save to IndexedDB - await saveImageBlob(blob, imageId); - } catch (error) { - console.error("Failed to save image blob:", error); + const centerX = (canvasWidth - scaledWidth) / 2 + const centerY = (canvasHeight - scaledHeight) / 2 + + const imageId = `image-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + + // If it's a blob URL, save the blob to IndexedDB for persistence + if (imageUrl.startsWith('blob:')) { + try { + // Fetch the blob from the blob URL + const response = await fetch(imageUrl) + const blob = await response.blob() + // Save to IndexedDB + await saveImageBlob(blob, imageId) + } catch (error) { + console.error('Failed to save image blob:', error) + } } - } - - const newObject: CanvasObject = { - id: imageId, - type: "image", - x: options.x ?? centerX, - y: options.y ?? centerY, - width: scaledWidth, - height: scaledHeight, - scaleX: 1, - scaleY: 1, - rotation: 0, - elevationX: 0, - elevationY: 0, - imageUrl: imageUrl.startsWith("blob:") ? imageId : imageUrl, // Store ID for blob URLs, URL for others - image: img, - }; - // For backward compatibility, also set left/top - (newObject as any).left = newObject.x; - (newObject as any).top = newObject.y; - (newObject as any).angle = newObject.rotation; + const newObject: CanvasObject = { + id: imageId, + type: 'image', + x: options.x ?? centerX, + y: options.y ?? centerY, + width: scaledWidth, + height: scaledHeight, + scaleX: 1, + scaleY: 1, + rotation: 0, + elevationX: 0, + elevationY: 0, + imageUrl: imageUrl.startsWith('blob:') ? imageId : imageUrl, // Store ID for blob URLs, URL for others + image: img, + } + + // For backward compatibility, also set left/top + ;(newObject as any).left = newObject.x + ;(newObject as any).top = newObject.y + ;(newObject as any).angle = newObject.rotation setObjects((prev) => { - const updated = [...prev, newObject]; - saveState(); - saveObjectsToStorage(updated); - return updated; - }); - setSelectedObject(newObject); - layer.batchDraw(); + const updated = [...prev, newObject] + saveState() + saveObjectsToStorage(updated) + return updated + }) + setSelectedObject(newObject) + layer.batchDraw() } catch (error) { - console.error("Failed to load image:", error); + console.error('Failed to load image:', error) } }, addText: async (text, options = {}) => { - if (!stage || !layer) return; - - const canvasWidth = stage.width(); - const canvasHeight = stage.height(); - + if (!stage || !layer) return + + const canvasWidth = stage.width() + const canvasHeight = stage.height() + const newObject: CanvasObject = { id: `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - type: "text", + type: 'text', x: options.x ?? canvasWidth / 2, y: options.y ?? canvasHeight / 2, scaleX: 1, scaleY: 1, rotation: 0, - fill: options.color ?? "#000000", + fill: options.color ?? '#000000', fontSize: options.fontSize ?? 48, text: text, - }; + } // For backward compatibility, also set left/top - (newObject as any).left = newObject.x; - (newObject as any).top = newObject.y; - (newObject as any).angle = newObject.rotation; + ;(newObject as any).left = newObject.x + ;(newObject as any).top = newObject.y + ;(newObject as any).angle = newObject.rotation setObjects((prev) => { - const updated = [...prev, newObject]; - saveState(); - saveObjectsToStorage(updated); - return updated; - }); - setSelectedObject(newObject); - layer.batchDraw(); + const updated = [...prev, newObject] + saveState() + saveObjectsToStorage(updated) + return updated + }) + setSelectedObject(newObject) + layer.batchDraw() }, transformObject: (objectId, properties) => { - if (!layer) return; - - const idToUpdate = objectId || selectedObject?.id; - + if (!layer) return + + const idToUpdate = objectId || selectedObject?.id + setObjects((prev) => { const updated = prev.map((obj) => { if (obj.id === idToUpdate) { - const updatedObj: any = { ...obj, ...properties }; + const updatedObj: any = { ...obj, ...properties } // Map 'left' and 'top' to 'x' and 'y' for Konva compatibility if ('left' in properties) { - updatedObj.x = properties.left!; - updatedObj.left = properties.left!; + updatedObj.x = properties.left! + updatedObj.left = properties.left! } if ('top' in properties) { - updatedObj.y = properties.top!; - updatedObj.top = properties.top!; + updatedObj.y = properties.top! + updatedObj.top = properties.top! } if ('angle' in properties) { - updatedObj.rotation = properties.angle!; - updatedObj.angle = properties.angle!; + updatedObj.rotation = properties.angle! + updatedObj.angle = properties.angle! } if ('elevationX' in properties) { - updatedObj.elevationX = properties.elevationX!; + updatedObj.elevationX = properties.elevationX! } if ('elevationY' in properties) { - updatedObj.elevationY = properties.elevationY!; + updatedObj.elevationY = properties.elevationY! } // Handle text updates if ('text' in properties) { - updatedObj.text = (properties as any).text; + updatedObj.text = (properties as any).text } - + // Update selected object if it's the one being transformed if (selectedObject?.id === idToUpdate) { - setSelectedObject(updatedObj); + setSelectedObject(updatedObj) } - return updatedObj; + return updatedObj } - return obj; - }); - saveState(); - saveObjectsToStorage(updated); - return updated; - }); - - layer.batchDraw(); + return obj + }) + saveState() + saveObjectsToStorage(updated) + return updated + }) + + layer.batchDraw() }, deleteObject: (objectId) => { - if (!layer) return; - - const idToDelete = objectId || selectedObject?.id; - if (!idToDelete) return; + if (!layer) return + + const idToDelete = objectId || selectedObject?.id + if (!idToDelete) return // Find the object to check if it's a stored image - const objectToDelete = objects.find(obj => obj.id === idToDelete); - if (objectToDelete && objectToDelete.type === "image" && objectToDelete.imageUrl) { + const objectToDelete = objects.find((obj) => obj.id === idToDelete) + if (objectToDelete && objectToDelete.type === 'image' && objectToDelete.imageUrl) { // If it's a stored image (not a blob URL, http, or data URL), delete from IndexedDB - const imageUrl = objectToDelete.imageUrl; - if (!imageUrl.startsWith("blob:") && !imageUrl.startsWith("http") && !imageUrl.startsWith("data:")) { - deleteImageBlob(imageUrl).catch(err => { - console.error("Failed to delete image blob:", err); - }); + const imageUrl = objectToDelete.imageUrl + if ( + !imageUrl.startsWith('blob:') && + !imageUrl.startsWith('http') && + !imageUrl.startsWith('data:') + ) { + deleteImageBlob(imageUrl).catch((err) => { + console.error('Failed to delete image blob:', err) + }) } } setObjects((prev) => { - const updated = prev.filter((obj) => obj.id !== idToDelete); - saveState(); - saveObjectsToStorage(updated); - return updated; - }); - + const updated = prev.filter((obj) => obj.id !== idToDelete) + saveState() + saveObjectsToStorage(updated) + return updated + }) + if (selectedObject?.id === idToDelete) { - setSelectedObject(null); + setSelectedObject(null) } - - layer.batchDraw(); + + layer.batchDraw() }, exportCanvas: async (format, quality = 1) => { - if (!stage || !layer) return ""; - + if (!stage || !layer) return '' + return new Promise((resolve) => { // Create a temporary layer for watermark - const watermarkLayer = new Konva.Layer(); - + const watermarkLayer = new Konva.Layer() + // Create watermark text - const canvasWidth = stage.width(); - const canvasHeight = stage.height(); - const fontSize = Math.max(16, canvasWidth * 0.02); // Responsive font size - const padding = Math.max(15, canvasWidth * 0.015); // Responsive padding - + const canvasWidth = stage.width() + const canvasHeight = stage.height() + const fontSize = Math.max(16, canvasWidth * 0.02) // Responsive font size + const padding = Math.max(15, canvasWidth * 0.015) // Responsive padding + // Create watermark text only (no background) const watermarkText = new Konva.Text({ - text: "stagee.art", + text: 'stagee.art', fontSize: fontSize, - fontFamily: "system-ui, -apple-system, sans-serif", - fill: "rgba(255, 255, 255, 0.8)", - fontStyle: "normal", - fontVariant: "normal", + fontFamily: 'system-ui, -apple-system, sans-serif', + fill: 'rgba(255, 255, 255, 0.8)', + fontStyle: 'normal', + fontVariant: 'normal', x: canvasWidth - padding - 60, y: canvasHeight - fontSize - padding, - }); - + }) + // Adjust position based on actual text width - watermarkText.x(canvasWidth - watermarkText.width() - padding); - - watermarkLayer.add(watermarkText); - stage.add(watermarkLayer); - watermarkLayer.draw(); + watermarkText.x(canvasWidth - watermarkText.width() - padding) + + watermarkLayer.add(watermarkText) + stage.add(watermarkLayer) + watermarkLayer.draw() // Export the Konva Stage as a data URL // This captures the entire canvas including all objects, background, and watermark const dataURL = stage.toDataURL({ - mimeType: "image/png", + mimeType: 'image/png', quality: quality, pixelRatio: 1, // Use 1 for standard resolution, increase for higher quality - }); + }) // Clean up watermark layer - watermarkLayer.destroy(); + watermarkLayer.destroy() - resolve(dataURL); - }); + resolve(dataURL) + }) }, getSelectedObject: () => { - return selectedObject; + return selectedObject }, clearSelection: () => { - setSelectedObject(null); + setSelectedObject(null) if (layer) { - layer.batchDraw(); + layer.batchDraw() } }, selectObject: (objectId: string) => { - const obj = objects.find((o) => o.id === objectId); + const obj = objects.find((o) => o.id === objectId) if (obj) { - setSelectedObject(obj); + setSelectedObject(obj) if (layer) { - layer.batchDraw(); + layer.batchDraw() } } }, - }; + } // Save design - capture current canvas state const saveDesign = useCallback(async (): Promise<{ canvasData: any; previewUrl?: string }> => { // Get background preferences from localStorage - let backgroundPreferences = null; + let backgroundPreferences = null try { - const saved = localStorage.getItem(BACKGROUND_PREFS_KEY); + const saved = localStorage.getItem(BACKGROUND_PREFS_KEY) if (saved) { - backgroundPreferences = JSON.parse(saved); + backgroundPreferences = JSON.parse(saved) } } catch (error) { - console.error("Failed to load background preferences:", error); + console.error('Failed to load background preferences:', error) } // Prepare canvas data const canvasData = { - objects: objects.map(obj => ({ + objects: objects.map((obj) => ({ ...obj, image: undefined, // Don't include image element })), dimensions: canvasDimensions, aspectRatio: aspectRatio, background: backgroundPreferences, - }; + } // Generate preview URL if stage exists - let previewUrl: string | undefined; + let previewUrl: string | undefined if (stage && layer) { try { previewUrl = stage.toDataURL({ - mimeType: "image/png", + mimeType: 'image/png', quality: 0.7, pixelRatio: 0.5, // Lower resolution for preview - }); + }) } catch (error) { - console.error("Failed to generate preview:", error); + console.error('Failed to generate preview:', error) } } - return { canvasData, previewUrl }; - }, [objects, canvasDimensions, aspectRatio, stage, layer]); + return { canvasData, previewUrl } + }, [objects, canvasDimensions, aspectRatio, stage, layer]) // Load design - restore canvas state - const loadDesign = useCallback(async (canvasData: any) => { - if (!stage || !layer) { - console.error("Canvas not initialized"); - return; - } + const loadDesign = useCallback( + async (canvasData: any) => { + if (!stage || !layer) { + console.error('Canvas not initialized') + return + } - try { - // Restore dimensions and aspect ratio - if (canvasData.dimensions) { - setCanvasDimensionsState(canvasData.dimensions); - setAspectRatioState(canvasData.aspectRatio || DEFAULT_ASPECT_RATIO); - - // Update stage dimensions - stage.width(canvasData.dimensions.width); - stage.height(canvasData.dimensions.height); - - // Update background rect - const bgRect = layer.findOne((node: any) => node.id() === "canvas-background") as Konva.Rect; - if (bgRect && bgRect instanceof Konva.Rect) { - bgRect.width(canvasData.dimensions.width); - bgRect.height(canvasData.dimensions.height); + try { + // Restore dimensions and aspect ratio + if (canvasData.dimensions) { + setCanvasDimensionsState(canvasData.dimensions) + setAspectRatioState(canvasData.aspectRatio || DEFAULT_ASPECT_RATIO) + + // Update stage dimensions + stage.width(canvasData.dimensions.width) + stage.height(canvasData.dimensions.height) + + // Update background rect + const bgRect = layer.findOne( + (node: any) => node.id() === 'canvas-background' + ) as Konva.Rect + if (bgRect && bgRect instanceof Konva.Rect) { + bgRect.width(canvasData.dimensions.width) + bgRect.height(canvasData.dimensions.height) + } } - } - // Restore background preferences - if (canvasData.background) { - try { - localStorage.setItem(BACKGROUND_PREFS_KEY, JSON.stringify(canvasData.background)); - // Trigger background restoration by dispatching a custom event - window.dispatchEvent(new CustomEvent("restore-background", { detail: canvasData.background })); - } catch (error) { - console.error("Failed to save background preferences:", error); + // Restore background preferences + if (canvasData.background) { + try { + localStorage.setItem(BACKGROUND_PREFS_KEY, JSON.stringify(canvasData.background)) + // Trigger background restoration by dispatching a custom event + window.dispatchEvent( + new CustomEvent('restore-background', { detail: canvasData.background }) + ) + } catch (error) { + console.error('Failed to save background preferences:', error) + } } - } - // Restore objects - if (canvasData.objects && Array.isArray(canvasData.objects)) { - const restorePromises = canvasData.objects.map(async (obj: CanvasObject) => { - if (obj.type === "image" && obj.imageUrl) { - let imageSrc = obj.imageUrl; - - // If it's a stored image ID (not starting with blob: or http: or data:), get from IndexedDB - if (!imageSrc.startsWith("blob:") && !imageSrc.startsWith("http") && !imageSrc.startsWith("data:")) { - const blobUrl = await getBlobUrlFromStored(imageSrc); - if (blobUrl) { - imageSrc = blobUrl; - } else { - console.warn(`Image blob not found for ID: ${imageSrc}`); - return null; // Skip this object if blob not found + // Restore objects + if (canvasData.objects && Array.isArray(canvasData.objects)) { + const restorePromises = canvasData.objects.map(async (obj: CanvasObject) => { + if (obj.type === 'image' && obj.imageUrl) { + let imageSrc = obj.imageUrl + + // If it's a stored image ID (not starting with blob: or http: or data:), get from IndexedDB + if ( + !imageSrc.startsWith('blob:') && + !imageSrc.startsWith('http') && + !imageSrc.startsWith('data:') + ) { + const blobUrl = await getBlobUrlFromStored(imageSrc) + if (blobUrl) { + imageSrc = blobUrl + } else { + console.warn(`Image blob not found for ID: ${imageSrc}`) + return null // Skip this object if blob not found + } } + + const img = await new Promise((resolve, reject) => { + const image = new Image() + image.crossOrigin = 'anonymous' + image.onload = () => resolve(image) + image.onerror = reject + image.src = imageSrc + }) + return { ...obj, image: img, imageUrl: imageSrc } } - - const img = await new Promise((resolve, reject) => { - const image = new Image(); - image.crossOrigin = "anonymous"; - image.onload = () => resolve(image); - image.onerror = reject; - image.src = imageSrc; - }); - return { ...obj, image: img, imageUrl: imageSrc }; - } - return obj; - }); - - const restoredObjects = await Promise.all(restorePromises); - const validObjects = restoredObjects.filter(obj => obj !== null) as CanvasObject[]; - - setObjects(validObjects); - historyRef.current.present = [...validObjects]; - historyRef.current.past = []; - historyRef.current.future = []; - saveObjectsToStorage(validObjects); - setSelectedObject(null); - - // Trigger a draw after a short delay - setTimeout(() => { - if (layer) { - layer.batchDraw(); - } - }, 100); + return obj + }) + + const restoredObjects = await Promise.all(restorePromises) + const validObjects = restoredObjects.filter((obj) => obj !== null) as CanvasObject[] + + setObjects(validObjects) + historyRef.current.present = [...validObjects] + historyRef.current.past = [] + historyRef.current.future = [] + saveObjectsToStorage(validObjects) + setSelectedObject(null) + + // Trigger a draw after a short delay + setTimeout(() => { + if (layer) { + layer.batchDraw() + } + }, 100) + } + } catch (error) { + console.error('Failed to load design:', error) + throw error } - } catch (error) { - console.error("Failed to load design:", error); - throw error; - } - }, [stage, layer, saveObjectsToStorage]); + }, + [stage, layer, saveObjectsToStorage] + ) return ( {children} - ); + ) } export function useCanvasContext() { - const context = useContext(CanvasContext); + const context = useContext(CanvasContext) if (!context) { - throw new Error("useCanvasContext must be used within CanvasProvider"); + throw new Error('useCanvasContext must be used within CanvasProvider') } - return context; -} \ No newline at end of file + return context +} diff --git a/components/canvas/ClientCanvas.tsx b/components/canvas/ClientCanvas.tsx index c64e4d5..986afc4 100644 --- a/components/canvas/ClientCanvas.tsx +++ b/components/canvas/ClientCanvas.tsx @@ -15,7 +15,7 @@ import { OVERLAY_PUBLIC_IDS } from '@/lib/cloudinary-overlays' import { MockupRenderer } from '@/components/mockups/MockupRenderer' // Global ref to store the Konva stage for export -let globalKonvaStage: any = null; +let globalKonvaStage: any = null /** * Parse CSS linear gradient string to Konva gradient properties @@ -23,43 +23,43 @@ let globalKonvaStage: any = null; */ function parseLinearGradient(gradientString: string, width: number, height: number) { // Extract direction and colors from CSS gradient string - const match = gradientString.match(/linear-gradient\((.+)\)/); - if (!match) return null; + const match = gradientString.match(/linear-gradient\((.+)\)/) + if (!match) return null - const parts = match[1].split(',').map(p => p.trim()); - const direction = parts[0]; - const colors = parts.slice(1); + const parts = match[1].split(',').map((p) => p.trim()) + const direction = parts[0] + const colors = parts.slice(1) // Determine gradient direction - let startPoint = { x: 0, y: 0 }; - let endPoint = { x: width, y: 0 }; // Default to horizontal (to right) + let startPoint = { x: 0, y: 0 } + let endPoint = { x: width, y: 0 } // Default to horizontal (to right) if (direction.includes('right')) { - startPoint = { x: 0, y: 0 }; - endPoint = { x: width, y: 0 }; + startPoint = { x: 0, y: 0 } + endPoint = { x: width, y: 0 } } else if (direction.includes('left')) { - startPoint = { x: width, y: 0 }; - endPoint = { x: 0, y: 0 }; + startPoint = { x: width, y: 0 } + endPoint = { x: 0, y: 0 } } else if (direction.includes('bottom')) { - startPoint = { x: 0, y: 0 }; - endPoint = { x: 0, y: height }; + startPoint = { x: 0, y: 0 } + endPoint = { x: 0, y: height } } else if (direction.includes('top')) { - startPoint = { x: 0, y: height }; - endPoint = { x: 0, y: 0 }; + startPoint = { x: 0, y: height } + endPoint = { x: 0, y: 0 } } // Build color stops array [position, color, position, color, ...] - const colorStops: (number | string)[] = []; + const colorStops: (number | string)[] = [] colors.forEach((color, index) => { - const position = index / (colors.length - 1); - colorStops.push(position, color.trim()); - }); + const position = index / (colors.length - 1) + colorStops.push(position, color.trim()) + }) return { startPoint, endPoint, colorStops, - }; + } } function CanvasRenderer({ image }: { image: HTMLImageElement }) { @@ -70,19 +70,19 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { const updateStage = () => { if (stageRef.current) { // react-konva Stage ref gives us the Stage instance directly - globalKonvaStage = stageRef.current; + globalKonvaStage = stageRef.current } - }; + } - updateStage(); + updateStage() // Also check after a short delay to ensure ref is set - const timeout = setTimeout(updateStage, 100); + const timeout = setTimeout(updateStage, 100) return () => { - clearTimeout(timeout); - globalKonvaStage = null; - }; - }); + clearTimeout(timeout) + globalKonvaStage = null + } + }) const patternRectRef = useRef(null) const noiseRectRef = useRef(null) const backgroundRef = useRef(null) @@ -115,7 +115,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { updateMockup, } = useImageStore() - const hasMockups = mockups.length > 0 && mockups.some(m => m.isVisible) + const hasMockups = mockups.length > 0 && mockups.some((m) => m.isVisible) const responsiveDimensions = useResponsiveCanvasDimensions() const backgroundStyle = getBackgroundCSS(backgroundConfig) @@ -130,7 +130,9 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { const [noiseTexture, setNoiseTexture] = useState(null) // Load overlay images - const [loadedOverlayImages, setLoadedOverlayImages] = useState>({}) + const [loadedOverlayImages, setLoadedOverlayImages] = useState>( + {} + ) useEffect(() => { if (backgroundNoise > 0) { @@ -175,7 +177,12 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { // Check if it's a Cloudinary public ID or URL let imageUrl = imageValue - if (typeof imageUrl === 'string' && !imageUrl.startsWith('http') && !imageUrl.startsWith('blob:') && !imageUrl.startsWith('data:')) { + if ( + typeof imageUrl === 'string' && + !imageUrl.startsWith('http') && + !imageUrl.startsWith('blob:') && + !imageUrl.startsWith('data:') + ) { // It might be a Cloudinary public ID, construct URL const { cloudinaryPublicIds } = require('@/lib/cloudinary-backgrounds') if (cloudinaryPublicIds.includes(imageUrl)) { @@ -211,19 +218,21 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { if (!overlay.isVisible) continue try { - const isCloudinaryId = OVERLAY_PUBLIC_IDS.includes(overlay.src as any) || - (typeof overlay.src === 'string' && overlay.src.startsWith('overlays/')) - - const imageUrl = isCloudinaryId && !overlay.isCustom - ? getCldImageUrl({ - src: overlay.src, - width: overlay.size * 2, - height: overlay.size * 2, - quality: 'auto', - format: 'auto', - crop: 'fit', - }) - : overlay.src + const isCloudinaryId = + OVERLAY_PUBLIC_IDS.includes(overlay.src as any) || + (typeof overlay.src === 'string' && overlay.src.startsWith('overlays/')) + + const imageUrl = + isCloudinaryId && !overlay.isCustom + ? getCldImageUrl({ + src: overlay.src, + width: overlay.size * 2, + height: overlay.size * 2, + quality: 'auto', + format: 'auto', + crop: 'fit', + }) + : overlay.src const img = new window.Image() img.crossOrigin = 'anonymous' @@ -334,13 +343,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { if (patternRectRef.current) { patternRectRef.current.cache() } - }, [ - patternImage, - canvasW, - canvasH, - patternStyle.opacity, - patternStyle.blur, - ]) + }, [patternImage, canvasW, canvasH, patternStyle.opacity, patternStyle.blur]) // Cache background when blur is active useEffect(() => { @@ -348,14 +351,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { backgroundRef.current.cache() backgroundRef.current.getLayer()?.batchDraw() } - }, [ - backgroundBlur, - backgroundConfig, - backgroundBorderRadius, - canvasW, - canvasH, - bgImage, - ]) + }, [backgroundBlur, backgroundConfig, backgroundBorderRadius, canvasW, canvasH, bgImage]) let imageScaledW, imageScaledH if (contentW / contentH > imageAspect) { @@ -372,9 +368,9 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { showFrame && frame.type === 'solid' ? frame.width : showFrame && frame.type === 'ruler' - ? frame.width + 2 - : 0 - const windowPadding = showFrame && frame.type === 'window' ? (frame.padding || 20) : 0 + ? frame.width + 2 + : 0 + const windowPadding = showFrame && frame.type === 'window' ? frame.padding || 20 : 0 const windowHeader = showFrame && frame.type === 'window' ? 40 : 0 const eclipseBorder = showFrame && frame.type === 'eclipse' ? frame.width + 2 : 0 const framedW = imageScaledW + frameOffset * 2 + windowPadding * 2 + eclipseBorder @@ -388,10 +384,10 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { side === 'bottom' ? { x: 0, y: elevation } : side === 'right' - ? { x: elevation, y: 0 } - : side === 'bottom-right' - ? { x: diag, y: diag } - : { x: 0, y: 0 } + ? { x: elevation, y: 0 } + : side === 'bottom-right' + ? { x: diag, y: diag } + : { x: 0, y: 0 } return { shadowColor: color, @@ -411,7 +407,9 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { rotateX(${perspective3D.rotateX}deg) rotateY(${perspective3D.rotateY}deg) rotateZ(${perspective3D.rotateZ + screenshot.rotation}deg) - `.replace(/\s+/g, ' ').trim() + ` + .replace(/\s+/g, ' ') + .trim() // Check if 3D transforms are active (any non-default value) const has3DTransform = @@ -487,11 +485,12 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { height: '100%', objectFit: 'cover', opacity: imageOpacity, - borderRadius: showFrame && frame.type === 'window' - ? '0 0 12px 12px' - : showFrame && frame.type === 'ruler' - ? `${screenshot.radius * 0.8}px` - : `${screenshot.radius}px`, + borderRadius: + showFrame && frame.type === 'window' + ? '0 0 12px 12px' + : showFrame && frame.type === 'ruler' + ? `${screenshot.radius * 0.8}px` + : `${screenshot.radius}px`, transform: perspective3DTransform, transformOrigin: 'center center', willChange: 'transform', @@ -530,7 +529,11 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { /> ) : backgroundConfig.type === 'gradient' && backgroundStyle.background ? ( (() => { - const gradientProps = parseLinearGradient(backgroundStyle.background as string, canvasW, canvasH); + const gradientProps = parseLinearGradient( + backgroundStyle.background as string, + canvasW, + canvasH + ) return gradientProps ? ( - ); + ) })() ) : ( - - {/* Solid Frame */} - {showFrame && frame.type === 'solid' && ( - - )} - - {/* Glassy Frame */} - {showFrame && frame.type === 'glassy' && ( - - )} - - {/* Ruler Frame */} - {showFrame && frame.type === 'ruler' && ( - + + + {/* Solid Frame */} + {showFrame && frame.type === 'solid' && ( + )} + {/* Glassy Frame */} + {showFrame && frame.type === 'glassy' && ( + )} + {/* Ruler Frame */} + {showFrame && frame.type === 'ruler' && ( + + - - {/* Top ruler marks */} - {Array.from({ length: Math.floor(framedW / 10) - 1 }).map((_, i) => ( - - ))} - {/* Left ruler marks */} - {Array.from({ length: Math.floor(framedH / 10) - 1 }).map((_, i) => ( - - ))} - {/* Right ruler marks */} - {Array.from({ length: Math.floor(framedH / 10) - 1 }).map((_, i) => ( - - ))} - {/* Bottom ruler marks */} - {Array.from({ length: Math.floor(framedW / 10) - 1 }).map((_, i) => ( - - ))} + + + + + {/* Top ruler marks */} + {Array.from({ length: Math.floor(framedW / 10) - 1 }).map((_, i) => ( + + ))} + {/* Left ruler marks */} + {Array.from({ length: Math.floor(framedH / 10) - 1 }).map((_, i) => ( + + ))} + {/* Right ruler marks */} + {Array.from({ length: Math.floor(framedH / 10) - 1 }).map((_, i) => ( + + ))} + {/* Bottom ruler marks */} + {Array.from({ length: Math.floor(framedW / 10) - 1 }).map((_, i) => ( + + ))} + - - - - )} + + + )} + + {/* Infinite Mirror Frame */} + {showFrame && frame.type === 'infinite-mirror' && ( + <> + {Array.from({ length: 4 }).map((_, i) => ( + + ))} + + )} + + {/* Eclipse Frame */} + {showFrame && frame.type === 'eclipse' && ( + + + + + )} - {/* Infinite Mirror Frame */} - {showFrame && frame.type === 'infinite-mirror' && ( - <> - {Array.from({ length: 4 }).map((_, i) => ( + {/* Stack Frame */} + {showFrame && frame.type === 'stack' && ( + <> + {/* Bottom layer - darkest */} - ))} - - )} + {/* Middle layer */} + + {/* Top layer - lightest, will have image on top */} + + + )} - {/* Eclipse Frame */} - {showFrame && frame.type === 'eclipse' && ( - - - - - )} + {/* Window Frame */} + {showFrame && frame.type === 'window' && ( + <> + + + {/* Window control buttons (red, yellow, green) */} + + + + + + )} - {/* Stack Frame */} - {showFrame && frame.type === 'stack' && ( - <> - {/* Bottom layer - darkest */} - - {/* Middle layer */} - - {/* Top layer - lightest, will have image on top */} + {/* Dotted Frame */} + {showFrame && frame.type === 'dotted' && ( - - )} - - {/* Window Frame */} - {showFrame && frame.type === 'window' && ( - <> - - - {/* Window control buttons (red, yellow, green) */} - - - - - - )} - - {/* Dotted Frame */} - {showFrame && frame.type === 'dotted' && ( - - )} - - {/* Focus Frame */} - {showFrame && frame.type === 'focus' && ( - - - - - - - )} + )} - - - + {/* Focus Frame */} + {showFrame && frame.type === 'focus' && ( + + + + + + + )} + + + + )} {/* Text Overlays Layer - Canvas based */} @@ -953,7 +963,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { const newX = (e.target.x() / canvasW) * 100 const newY = (e.target.y() / canvasH) * 100 updateTextOverlay(overlay.id, { - position: { x: newX, y: newY } + position: { x: newX, y: newY }, }) }} onMouseEnter={(e) => { @@ -1010,7 +1020,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { draggable={true} onDragEnd={(e) => { updateImageOverlay(overlay.id, { - position: { x: e.target.x(), y: e.target.y() } + position: { x: e.target.x(), y: e.target.y() }, }) }} onMouseEnter={(e) => { @@ -1037,7 +1047,7 @@ function CanvasRenderer({ image }: { image: HTMLImageElement }) { // Export function to get the Konva stage export function getKonvaStage(): any { - return globalKonvaStage; + return globalKonvaStage } export default function ClientCanvas() { diff --git a/components/canvas/EditorCanvas.tsx b/components/canvas/EditorCanvas.tsx index 7320db0..ab1b4ea 100644 --- a/components/canvas/EditorCanvas.tsx +++ b/components/canvas/EditorCanvas.tsx @@ -28,4 +28,3 @@ export function EditorCanvas() { return } - diff --git a/components/canvas/EditorStoreSync.tsx b/components/canvas/EditorStoreSync.tsx index 1624573..f597e16 100644 --- a/components/canvas/EditorStoreSync.tsx +++ b/components/canvas/EditorStoreSync.tsx @@ -8,7 +8,11 @@ import { GradientKey } from '@/lib/constants/gradient-colors' import { AspectRatioKey } from '@/lib/constants/aspect-ratios' // Helper function to parse gradient string and extract colors -function parseGradientColors(gradientStr: string): { colorA: string; colorB: string; direction: number } { +function parseGradientColors(gradientStr: string): { + colorA: string + colorB: string + direction: number +} { let colorA = '#4168d0' let colorB = '#c850c0' let direction = 43 @@ -61,7 +65,8 @@ export function EditorStoreSync() { // Sync background const bgConfig = imageStore.backgroundConfig if (bgConfig.type === 'gradient') { - const gradientStr = gradientColors[bgConfig.value as GradientKey] || gradientColors.sunset_vibes + const gradientStr = + gradientColors[bgConfig.value as GradientKey] || gradientColors.sunset_vibes const { colorA, colorB, direction } = parseGradientColors(gradientStr) if ( editorStore.background.mode !== 'gradient' || @@ -157,4 +162,3 @@ export function EditorStoreSync() { return null } - diff --git a/components/canvas/dialogs/ExportDialog.tsx b/components/canvas/dialogs/ExportDialog.tsx index 90be850..e65d46f 100644 --- a/components/canvas/dialogs/ExportDialog.tsx +++ b/components/canvas/dialogs/ExportDialog.tsx @@ -1,45 +1,48 @@ -"use client"; +'use client' -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { ScaleSlider } from "@/components/export"; +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { ScaleSlider } from '@/components/export' interface ExportDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onExport: () => Promise; - scale: number; - isExporting: boolean; - onScaleChange: (scale: number) => void; + open: boolean + onOpenChange: (open: boolean) => void + onExport: () => Promise + scale: number + isExporting: boolean + onScaleChange: (scale: number) => void } -export function ExportDialog({ - open, - onOpenChange, +export function ExportDialog({ + open, + onOpenChange, onExport, scale, isExporting, onScaleChange, }: ExportDialogProps) { - const [error, setError] = useState(null); + const [error, setError] = useState(null) const handleExport = async () => { - setError(null); + setError(null) try { - await onExport(); - onOpenChange(false); + await onExport() + onOpenChange(false) } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to export image. Please try again."; - setError(errorMessage); + const errorMessage = + err instanceof Error ? err.message : 'Failed to export image. Please try again.' + setError(errorMessage) } - }; + } return ( - Export Canvas + + Export Canvas +
@@ -62,11 +65,10 @@ export function ExportDialog({ className="w-full h-11 font-semibold bg-primary hover:bg-primary/90 text-primary-foreground" size="lg" > - {isExporting ? "Exporting..." : "Export as PNG"} + {isExporting ? 'Exporting...' : 'Export as PNG'}
- ); + ) } - diff --git a/components/canvas/dialogs/SaveDesignDialog.tsx b/components/canvas/dialogs/SaveDesignDialog.tsx index 9cdf414..7da600d 100644 --- a/components/canvas/dialogs/SaveDesignDialog.tsx +++ b/components/canvas/dialogs/SaveDesignDialog.tsx @@ -1,55 +1,66 @@ -"use client"; +'use client' -import { useState, useEffect } from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { useCanvasContext } from "../CanvasContext"; -import { useRouter } from "next/navigation"; +import { useState, useEffect } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { useCanvasContext } from '../CanvasContext' +import { useRouter } from 'next/navigation' interface SaveDesignDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - designId?: string; // If provided, this is an update operation - designName?: string; // Existing design name if updating + open: boolean + onOpenChange: (open: boolean) => void + designId?: string // If provided, this is an update operation + designName?: string // Existing design name if updating } -export function SaveDesignDialog({ open, onOpenChange, designId, designName }: SaveDesignDialogProps) { - const { saveDesign } = useCanvasContext(); - const [name, setName] = useState(designName || ""); - const [description, setDescription] = useState(""); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const router = useRouter(); +export function SaveDesignDialog({ + open, + onOpenChange, + designId, + designName, +}: SaveDesignDialogProps) { + const { saveDesign } = useCanvasContext() + const [name, setName] = useState(designName || '') + const [description, setDescription] = useState('') + const [isSaving, setIsSaving] = useState(false) + const [error, setError] = useState(null) + const router = useRouter() // Update name when designName prop changes useEffect(() => { if (designName) { - setName(designName); + setName(designName) } - }, [designName]); + }, [designName]) const handleSave = async () => { if (!name.trim()) { - setError("Design name is required"); - return; + setError('Design name is required') + return } - setIsSaving(true); - setError(null); + setIsSaving(true) + setError(null) try { // Get canvas data and preview - const { canvasData, previewUrl } = await saveDesign(); + const { canvasData, previewUrl } = await saveDesign() // Save to API - const url = designId ? `/api/designs/${designId}` : "/api/designs"; - const method = designId ? "PATCH" : "POST"; + const url = designId ? `/api/designs/${designId}` : '/api/designs' + const method = designId ? 'PATCH' : 'POST' const response = await fetch(url, { method, headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ name: name.trim(), @@ -57,31 +68,31 @@ export function SaveDesignDialog({ open, onOpenChange, designId, designName }: S canvasData, previewUrl, }), - }); + }) if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || "Failed to save design"); + const data = await response.json() + throw new Error(data.error || 'Failed to save design') } // Close dialog and refresh - onOpenChange(false); - setName(""); - setDescription(""); - router.refresh(); + onOpenChange(false) + setName('') + setDescription('') + router.refresh() } catch (err) { - console.error("Error saving design:", err); - setError(err instanceof Error ? err.message : "Failed to save design"); + console.error('Error saving design:', err) + setError(err instanceof Error ? err.message : 'Failed to save design') } finally { - setIsSaving(false); + setIsSaving(false) } - }; + } return ( - {designId ? "Update Design" : "Save Design"} + {designId ? 'Update Design' : 'Save Design'}
@@ -108,29 +119,17 @@ export function SaveDesignDialog({ open, onOpenChange, designId, designName }: S disabled={isSaving} />
- {error && ( -
- {error} -
- )} + {error &&
{error}
}
- -
- ); + ) } - diff --git a/components/controls/BackgroundEffects.tsx b/components/controls/BackgroundEffects.tsx index cd4cbe3..850fd54 100644 --- a/components/controls/BackgroundEffects.tsx +++ b/components/controls/BackgroundEffects.tsx @@ -1,29 +1,24 @@ -'use client'; +'use client' -import * as React from 'react'; -import { useImageStore } from '@/lib/store'; -import { Slider } from '@/components/ui/slider'; -import { Label } from '@/components/ui/label'; +import * as React from 'react' +import { useImageStore } from '@/lib/store' +import { Slider } from '@/components/ui/slider' +import { Label } from '@/components/ui/label' export function BackgroundEffects() { - const { - backgroundBlur, - backgroundNoise, - setBackgroundBlur, - setBackgroundNoise, - } = useImageStore(); + const { backgroundBlur, backgroundNoise, setBackgroundBlur, setBackgroundNoise } = useImageStore() return (
-

Background Effects

- +

+ Background Effects +

+ {/* Blur */}
- - {backgroundBlur}px - + {backgroundBlur}px
- - {backgroundNoise}% - + {backgroundNoise}%
- ); + ) } - diff --git a/components/controls/BorderControls.tsx b/components/controls/BorderControls.tsx index 5751c25..5291b83 100644 --- a/components/controls/BorderControls.tsx +++ b/components/controls/BorderControls.tsx @@ -13,67 +13,44 @@ import { useState, useEffect } from 'react' const isValidHex = (color: string) => /^#[0-9A-F]{6}$/i.test(color) function ColorInput({ - value, onChange, className = '', - }: { - value: string onChange: (value: string) => void className?: string - }) { - const [localValue, setLocalValue] = useState(value) useEffect(() => { - setLocalValue(value) - }, [value]) const handleBlur = () => { - if (isValidHex(localValue)) { - onChange(localValue) - } else { - setLocalValue(value) - } - } return ( - setLocalValue(e.target.value)} - onBlur={handleBlur} - className={className} - /> - ) - } const frameOptions = [ - { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' }, @@ -97,13 +74,11 @@ const frameOptions = [ { value: 'dotted', label: 'Dotted' }, { value: 'focus', label: 'Focus' }, - ] as const type FrameType = (typeof frameOptions)[number]['value'] function FramePreview({ - type, selected, @@ -111,9 +86,7 @@ function FramePreview({ onSelect, children, - }: { - type: FrameType selected: boolean @@ -121,99 +94,68 @@ function FramePreview({ onSelect: () => void children: React.ReactNode - }) { - return ( -
- -
{frameOptions.find((f) => f.value === type)?.label}
- +
+ {frameOptions.find((f) => f.value === type)?.label} +
- ) - } const framePreviews: Record = { - none:
, solid:
, - glassy:
, + glassy: ( +
+ ), 'window-light': ( -
-
-
-
-
- ), 'window-dark': ( -
-
-
-
-
- ), 'infinite-mirror': ( -
-
-
-
-
- ), ruler: ( -
-
@@ -231,43 +173,31 @@ const framePreviews: Record = {
-
- ), eclipse:
, 'stack-light': ( -
-
-
- ), 'stack-dark': ( -
-
-
- ), dotted:
, focus: ( -
-
@@ -275,135 +205,92 @@ const framePreviews: Record = {
-
- ), - } export function BorderControls() { - const { imageBorder, setImageBorder } = useImageStore() const handleSelect = (value: FrameType) => { - if (value.startsWith('window-') || value.startsWith('stack-')) { - const [type, theme] = value.split('-') - setImageBorder({ type: type as 'window' | 'stack', theme: theme as 'light' | 'dark', enabled: true }) - + setImageBorder({ + type: type as 'window' | 'stack', + theme: theme as 'light' | 'dark', + enabled: true, + }) } else { - - setImageBorder({ type: value as Exclude, enabled: true }) - + setImageBorder({ + type: value as Exclude, + enabled: true, + }) } - } const isSelected = (value: FrameType) => { - if (value.startsWith('window-') || value.startsWith('stack-')) { - const [type, theme] = value.split('-') return imageBorder.type === type && imageBorder.theme === theme - } return imageBorder.type === value - } return ( -
-
Frame
-
-
- {frameOptions.map(({ value }) => ( - handleSelect(value)} - > - {framePreviews[value]} - - ))} -
-
- {['solid', 'dotted', 'infinite-mirror', 'eclipse', 'focus', 'ruler'].includes(imageBorder.type) && ( - + {['solid', 'dotted', 'infinite-mirror', 'eclipse', 'focus', 'ruler'].includes( + imageBorder.type + ) && (
-
-
- setImageBorder({ color: e.target.value, enabled: true })} - className="absolute inset-0 size-full cursor-pointer opacity-0" - /> -
setImageBorder({ color, enabled: true })} - /> -
-
- )} {['solid', 'glassy', 'dotted', 'eclipse', 'ruler', 'focus'].includes(imageBorder.type) && ( -
- +
- {imageBorder.width}px + + {imageBorder.width}px +
- )} {imageBorder.type === 'window' && ( - <> -
- setImageBorder({ title: e.target.value, enabled: true })} - /> -
@@ -450,18 +330,14 @@ export function BorderControls() { max={100} step={1} /> - {imageBorder.padding || 20}px + + {imageBorder.padding || 20}px +
- - )} -
-
- ) - } diff --git a/components/controls/BorderStyleSelector.tsx b/components/controls/BorderStyleSelector.tsx index 2fbf2b8..ee52595 100644 --- a/components/controls/BorderStyleSelector.tsx +++ b/components/controls/BorderStyleSelector.tsx @@ -1,19 +1,19 @@ -'use client'; +'use client' -import * as React from 'react'; -import { ImageBorder } from '@/lib/store'; -import { cn } from '@/lib/utils'; +import * as React from 'react' +import { ImageBorder } from '@/lib/store' +import { cn } from '@/lib/utils' interface BorderStyleSelectorProps { - border: ImageBorder; - onBorderChange: (border: ImageBorder | Partial) => void; + border: ImageBorder + onBorderChange: (border: ImageBorder | Partial) => void } type BorderStyle = { - id: ImageBorder['style']; - label: string; - preview: React.ReactNode; -}; + id: ImageBorder['style'] + label: string + preview: React.ReactNode +} export function BorderStyleSelector({ border, onBorderChange }: BorderStyleSelectorProps) { const borderStyles: BorderStyle[] = [ @@ -52,11 +52,11 @@ export function BorderStyleSelector({ border, onBorderChange }: BorderStyleSelec
), }, - ]; + ] const handleStyleSelect = (styleId: ImageBorder['style']) => { - onBorderChange({ style: styleId, enabled: true }); - }; + onBorderChange({ style: styleId, enabled: true }) + } return (
@@ -65,7 +65,7 @@ export function BorderStyleSelector({ border, onBorderChange }: BorderStyleSelec
{borderStyles.map((style) => { - const isSelected = border.style === style.id && border.enabled; + const isSelected = border.style === style.id && border.enabled return ( - ); + ) })}
- ); + ) } - diff --git a/components/controls/Perspective3DControls.tsx b/components/controls/Perspective3DControls.tsx index 7717ea6..1c738db 100644 --- a/components/controls/Perspective3DControls.tsx +++ b/components/controls/Perspective3DControls.tsx @@ -1,22 +1,22 @@ -'use client'; +'use client' -import * as React from 'react'; -import { useImageStore } from '@/lib/store'; -import { Slider } from '@/components/ui/slider'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; +import * as React from 'react' +import { useImageStore } from '@/lib/store' +import { Slider } from '@/components/ui/slider' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' interface TransformPreset { - name: string; + name: string values: { - perspective: number; - rotateX: number; - rotateY: number; - rotateZ: number; - translateX: number; - translateY: number; - scale: number; - }; + perspective: number + rotateX: number + rotateY: number + rotateZ: number + translateX: number + translateY: number + scale: number + } } const PRESETS: TransformPreset[] = [ @@ -68,14 +68,14 @@ const PRESETS: TransformPreset[] = [ scale: 0.95, }, }, -]; +] export function Perspective3DControls() { - const { perspective3D, setPerspective3D } = useImageStore(); + const { perspective3D, setPerspective3D } = useImageStore() const applyPreset = (preset: TransformPreset) => { - setPerspective3D(preset.values); - }; + setPerspective3D(preset.values) + } const reset = () => { setPerspective3D({ @@ -86,8 +86,8 @@ export function Perspective3DControls() { translateX: 0, translateY: 0, scale: 1, - }); - }; + }) + } return (
@@ -126,7 +126,9 @@ export function Perspective3DControls() {
- {perspective3D.perspective}px + + {perspective3D.perspective}px +
- {perspective3D.rotateX}° + + {perspective3D.rotateX}° +
- {perspective3D.rotateY}° + + {perspective3D.rotateY}° +
- {perspective3D.rotateZ}° + + {perspective3D.rotateZ}° +
- {perspective3D.translateX}% + + {perspective3D.translateX}% +
- {perspective3D.translateY}% + + {perspective3D.translateY}% +
- {perspective3D.scale.toFixed(2)} + + {perspective3D.scale.toFixed(2)} +
- ); + ) } - diff --git a/components/controls/ShadowControls.tsx b/components/controls/ShadowControls.tsx index ebc33c0..6949801 100644 --- a/components/controls/ShadowControls.tsx +++ b/components/controls/ShadowControls.tsx @@ -1,16 +1,16 @@ -'use client'; +'use client' -import * as React from 'react'; -import { Label } from '@/components/ui/label'; -import { Slider } from '@/components/ui/slider'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper'; -import { ImageShadow } from '@/lib/store'; +import * as React from 'react' +import { Label } from '@/components/ui/label' +import { Slider } from '@/components/ui/slider' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper' +import { ImageShadow } from '@/lib/store' interface ShadowControlsProps { - shadow: ImageShadow; - onShadowChange: (shadow: ImageShadow | Partial) => void; + shadow: ImageShadow + onShadowChange: (shadow: ImageShadow | Partial) => void } export function ShadowControls({ shadow, onShadowChange }: ShadowControlsProps) { @@ -19,15 +19,13 @@ export function ShadowControls({ shadow, onShadowChange }: ShadowControlsProps) { blur: 10, offsetX: 0, offsetY: 4, spread: 0, label: 'Medium' }, { blur: 20, offsetX: 0, offsetY: 8, spread: 0, label: 'Large' }, { blur: 40, offsetX: 0, offsetY: 16, spread: 0, label: 'XL' }, - ]; + ] return (
- +
@@ -133,29 +147,29 @@ export function ShadowControls({ shadow, onShadowChange }: ShadowControlsProps) type="color" value={(() => { // Extract RGB from rgba or rgb string - const rgbMatch = shadow.color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + const rgbMatch = shadow.color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) if (rgbMatch) { - const r = parseInt(rgbMatch[1]); - const g = parseInt(rgbMatch[2]); - const b = parseInt(rgbMatch[3]); - return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; + const r = parseInt(rgbMatch[1]) + const g = parseInt(rgbMatch[2]) + const b = parseInt(rgbMatch[3]) + return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}` } // If it's already hex, return it if (shadow.color.startsWith('#')) { - return shadow.color; + return shadow.color } - return '#000000'; + return '#000000' })()} onChange={(e) => { // Convert hex to rgba for better control - const hex = e.target.value; - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); + const hex = e.target.value + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) // Extract alpha from current color if it exists - const alphaMatch = shadow.color.match(/rgba\([^)]+,\s*([\d.]+)\)/); - const currentAlpha = alphaMatch ? alphaMatch[1] : '0.3'; - onShadowChange({ color: `rgba(${r}, ${g}, ${b}, ${currentAlpha})` }); + const alphaMatch = shadow.color.match(/rgba\([^)]+,\s*([\d.]+)\)/) + const currentAlpha = alphaMatch ? alphaMatch[1] : '0.3' + onShadowChange({ color: `rgba(${r}, ${g}, ${b}, ${currentAlpha})` }) }} className="w-12 h-10 rounded-lg border border-border cursor-pointer" /> @@ -169,7 +183,9 @@ export function ShadowControls({ shadow, onShadowChange }: ShadowControlsProps) className="border-0 bg-transparent text-sm focus-visible:ring-0 focus-visible:ring-offset-0" /> -
Opacity
+
+ Opacity +
{ - const alphaMatch = shadow.color.match(/rgba\([^)]+,\s*([\d.]+)\)/); - return alphaMatch ? alphaMatch[1] : '0.3'; + const alphaMatch = shadow.color.match(/rgba\([^)]+,\s*([\d.]+)\)/) + return alphaMatch ? alphaMatch[1] : '0.3' })()} onChange={(e) => { - const alpha = parseFloat(e.target.value) || 0.3; - const rgbMatch = shadow.color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + const alpha = parseFloat(e.target.value) || 0.3 + const rgbMatch = shadow.color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) if (rgbMatch) { onShadowChange({ color: `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})`, - }); + }) } else { - onShadowChange({ color: `rgba(0, 0, 0, ${alpha})` }); + onShadowChange({ color: `rgba(0, 0, 0, ${alpha})` }) } }} className="border-0 bg-transparent text-sm px-2 py-2 focus-visible:ring-0 focus-visible:ring-offset-0" @@ -201,6 +217,5 @@ export function ShadowControls({ shadow, onShadowChange }: ShadowControlsProps) )}
- ); + ) } - diff --git a/components/controls/UploadDropzone.tsx b/components/controls/UploadDropzone.tsx index ad3e84d..0df634d 100644 --- a/components/controls/UploadDropzone.tsx +++ b/components/controls/UploadDropzone.tsx @@ -51,7 +51,12 @@ export function UploadDropzone() { [handleFile] ) - const { getRootProps, getInputProps, isDragActive: dropzoneActive, fileRejections } = useDropzone({ + const { + getRootProps, + getInputProps, + isDragActive: dropzoneActive, + fileRejections, + } = useDropzone({ onDrop, accept: { 'image/jpeg': ['.jpg', '.jpeg'], @@ -154,12 +159,15 @@ export function UploadDropzone() {
{active ? ( -

Drop the image here...

+

+ Drop the image here... +

) : (

Drag & drop an image here

- or tap to browse • PNG, JPG, WEBP up to {MAX_IMAGE_SIZE / 1024 / 1024}MB • or paste an image + or tap to browse • PNG, JPG, WEBP up to {MAX_IMAGE_SIZE / 1024 / 1024}MB • or + paste an image

)} @@ -180,4 +188,3 @@ export function UploadDropzone() {
) } - diff --git a/components/controls/WebsiteScreenshotInput.tsx b/components/controls/WebsiteScreenshotInput.tsx index 9a0c426..c62f004 100644 --- a/components/controls/WebsiteScreenshotInput.tsx +++ b/components/controls/WebsiteScreenshotInput.tsx @@ -16,36 +16,41 @@ export function WebsiteScreenshotInput() { const normalizeUrl = (urlString: string): string => { let normalized = urlString.trim() - + // Remove leading/trailing whitespace normalized = normalized.trim() - + // If URL doesn't start with http:// or https://, add https:// if (!normalized.match(/^https?:\/\//i)) { normalized = `https://${normalized}` } - + return normalized } - const validateUrl = (urlString: string): { valid: boolean; normalized?: string; error?: string } => { + const validateUrl = ( + urlString: string + ): { valid: boolean; normalized?: string; error?: string } => { try { const normalized = normalizeUrl(urlString) const urlObj = new URL(normalized) - + // Ensure URL has http or https protocol if (!['http:', 'https:'].includes(urlObj.protocol)) { return { valid: false, error: 'URL must use http or https protocol' } } - + // Basic validation - must have a hostname if (!urlObj.hostname || urlObj.hostname.length === 0) { return { valid: false, error: 'Please enter a valid URL with a domain name' } } - + return { valid: true, normalized } } catch (error) { - return { valid: false, error: 'Please enter a valid URL (e.g., example.com or https://example.com)' } + return { + valid: false, + error: 'Please enter a valid URL (e.g., example.com or https://example.com)', + } } } @@ -105,16 +110,13 @@ export function WebsiteScreenshotInput() { } catch (error) { console.error('Screenshot error:', error) setError( - error instanceof Error - ? error.message - : 'Failed to capture screenshot. Please try again.' + error instanceof Error ? error.message : 'Failed to capture screenshot. Please try again.' ) } finally { setIsLoading(false) } } - const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !isLoading) { handleCapture() @@ -144,7 +146,11 @@ export function WebsiteScreenshotInput() { const value = e.target.value.trim() if (value) { const validation = validateUrl(value) - if (validation.valid && validation.normalized && validation.normalized !== value) { + if ( + validation.valid && + validation.normalized && + validation.normalized !== value + ) { setUrl(validation.normalized) } } @@ -154,11 +160,7 @@ export function WebsiteScreenshotInput() { className="pl-9" />
-

- Enter a website URL to capture a viewport screenshot (visible browser area). https:// will be added automatically if missing. + Enter a website URL to capture a viewport screenshot (visible browser area). https:// will + be added automatically if missing.

@@ -183,15 +186,10 @@ export function WebsiteScreenshotInput() { {isLoading && (
-

- Capturing screenshot... -

-

- This may take a few seconds -

+

Capturing screenshot...

+

This may take a few seconds

)}
) } - diff --git a/components/editor/EditorContent.tsx b/components/editor/EditorContent.tsx index 5a15378..3d5b75b 100644 --- a/components/editor/EditorContent.tsx +++ b/components/editor/EditorContent.tsx @@ -1,25 +1,24 @@ -"use client"; +'use client' -import * as React from "react"; -import { cn } from "@/lib/utils"; +import * as React from 'react' +import { cn } from '@/lib/utils' interface EditorContentProps { - children: React.ReactNode; - className?: string; + children: React.ReactNode + className?: string } export function EditorContent({ children, className }: EditorContentProps) { return (
{children}
- ); + ) } - diff --git a/components/editor/EditorHeader.tsx b/components/editor/EditorHeader.tsx index 3399d6f..c714319 100644 --- a/components/editor/EditorHeader.tsx +++ b/components/editor/EditorHeader.tsx @@ -1,34 +1,37 @@ -"use client"; +'use client' -import * as React from "react"; -import Link from "next/link"; -import Image from "next/image"; -import { SidebarTrigger } from "@/components/ui/sidebar"; -import { FaGithub } from "react-icons/fa"; -import { cn } from "@/lib/utils"; +import * as React from 'react' +import Link from 'next/link' +import Image from 'next/image' +import { SidebarTrigger } from '@/components/ui/sidebar' +import { FaGithub } from 'react-icons/fa' +import { cn } from '@/lib/utils' interface EditorHeaderProps { - className?: string; + className?: string } export function EditorHeader({ className }: EditorHeaderProps) { return (
- - - Stage + Stage @@ -42,8 +45,8 @@ export function EditorHeader({ className }: EditorHeaderProps) { target="_blank" rel="noopener noreferrer" className={cn( - "p-2 rounded-lg transition-all touch-manipulation", - "hover:bg-accent text-muted-foreground hover:text-foreground hover:shadow-sm" + 'p-2 rounded-lg transition-all touch-manipulation', + 'hover:bg-accent text-muted-foreground hover:text-foreground hover:shadow-sm' )} aria-label="GitHub repository" > @@ -53,6 +56,5 @@ export function EditorHeader({ className }: EditorHeaderProps) {
- ); + ) } - diff --git a/components/editor/EditorLayout.tsx b/components/editor/EditorLayout.tsx index 636a722..edb2157 100644 --- a/components/editor/EditorLayout.tsx +++ b/components/editor/EditorLayout.tsx @@ -1,26 +1,26 @@ -"use client"; +'use client' -import * as React from "react"; -import { EditorLeftPanel } from "./editor-left-panel"; -import { EditorRightPanel } from "./editor-right-panel"; -import { EditorContent } from "./EditorContent"; -import { EditorCanvas } from "@/components/canvas/EditorCanvas"; -import { EditorStoreSync } from "@/components/canvas/EditorStoreSync"; -import { EditorBottomBar } from "./editor-bottom-bar"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { Sheet, SheetContent } from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; -import { Menu, Settings } from "lucide-react"; +import * as React from 'react' +import { EditorLeftPanel } from './editor-left-panel' +import { EditorRightPanel } from './editor-right-panel' +import { EditorContent } from './EditorContent' +import { EditorCanvas } from '@/components/canvas/EditorCanvas' +import { EditorStoreSync } from '@/components/canvas/EditorStoreSync' +import { EditorBottomBar } from './editor-bottom-bar' +import { useIsMobile } from '@/hooks/use-mobile' +import { Sheet, SheetContent } from '@/components/ui/sheet' +import { Button } from '@/components/ui/button' +import { Menu, Settings } from 'lucide-react' function EditorMain() { - const isMobile = useIsMobile(); - const [leftPanelOpen, setLeftPanelOpen] = React.useState(false); - const [rightPanelOpen, setRightPanelOpen] = React.useState(false); + const isMobile = useIsMobile() + const [leftPanelOpen, setLeftPanelOpen] = React.useState(false) + const [rightPanelOpen, setRightPanelOpen] = React.useState(false) return (
- + {/* Mobile Header */} {isMobile && (
@@ -47,7 +47,7 @@ function EditorMain() {
{/* Left Panel - Desktop */} {!isMobile && } - + {/* Left Panel - Mobile Sheet */} {isMobile && ( @@ -56,17 +56,17 @@ function EditorMain() { )} - + {/* Center Canvas */}
- + {/* Right Panel - Desktop */} {!isMobile && } - + {/* Right Panel - Mobile Sheet */} {isMobile && ( @@ -76,13 +76,13 @@ function EditorMain() { )}
- + {/* Bottom Bar */}
- ); + ) } export function EditorLayout() { - return ; + return } diff --git a/components/editor/UploadArea.tsx b/components/editor/UploadArea.tsx index fe43979..d1e19bb 100644 --- a/components/editor/UploadArea.tsx +++ b/components/editor/UploadArea.tsx @@ -1,99 +1,103 @@ -"use client"; +'use client' -import * as React from "react"; -import { useDropzone } from "react-dropzone"; -import { FaImage } from "react-icons/fa"; -import { ALLOWED_IMAGE_TYPES, MAX_IMAGE_SIZE } from "@/lib/constants"; -import { cn } from "@/lib/utils"; +import * as React from 'react' +import { useDropzone } from 'react-dropzone' +import { FaImage } from 'react-icons/fa' +import { ALLOWED_IMAGE_TYPES, MAX_IMAGE_SIZE } from '@/lib/constants' +import { cn } from '@/lib/utils' interface UploadAreaProps { - onUpload: (file: File) => void; - error?: string | null; - className?: string; + onUpload: (file: File) => void + error?: string | null + className?: string } export function UploadArea({ onUpload, error, className }: UploadAreaProps) { - const [isDragActive, setIsDragActive] = React.useState(false); - const containerRef = React.useRef(null); + const [isDragActive, setIsDragActive] = React.useState(false) + const containerRef = React.useRef(null) const validateFile = React.useCallback((file: File): string | null => { if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { - return `File type not supported. Please use: ${ALLOWED_IMAGE_TYPES.join(", ")}`; + return `File type not supported. Please use: ${ALLOWED_IMAGE_TYPES.join(', ')}` } if (file.size > MAX_IMAGE_SIZE) { - return `File size too large. Maximum size is ${MAX_IMAGE_SIZE / 1024 / 1024}MB`; + return `File size too large. Maximum size is ${MAX_IMAGE_SIZE / 1024 / 1024}MB` } - return null; - }, []); + return null + }, []) const handleFile = React.useCallback( (file: File) => { - const validationError = validateFile(file); + const validationError = validateFile(file) if (validationError) { - return; + return } - onUpload(file); + onUpload(file) }, [validateFile, onUpload] - ); + ) const onDrop = React.useCallback( (acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { - handleFile(acceptedFiles[0]); + handleFile(acceptedFiles[0]) } }, [handleFile] - ); + ) - const { getRootProps, getInputProps, isDragActive: dropzoneActive } = useDropzone({ + const { + getRootProps, + getInputProps, + isDragActive: dropzoneActive, + } = useDropzone({ onDrop, accept: { - "image/*": ALLOWED_IMAGE_TYPES.map((type) => type.split("/")[1]), + 'image/*': ALLOWED_IMAGE_TYPES.map((type) => type.split('/')[1]), }, maxSize: MAX_IMAGE_SIZE, multiple: false, onDragEnter: () => setIsDragActive(true), onDragLeave: () => setIsDragActive(false), - }); + }) // Handle paste event React.useEffect(() => { const handlePaste = async (e: ClipboardEvent) => { // Only handle paste if the upload area is focused or visible - if (!containerRef.current) return; + if (!containerRef.current) return - const items = e.clipboardData?.items; - if (!items) return; + const items = e.clipboardData?.items + if (!items) return for (let i = 0; i < items.length; i++) { - const item = items[i]; - + const item = items[i] + // Check if the item is an image - if (item.type.startsWith("image/")) { - e.preventDefault(); - - const file = item.getAsFile(); + if (item.type.startsWith('image/')) { + e.preventDefault() + + const file = item.getAsFile() if (file) { - handleFile(file); + handleFile(file) } - break; + break } } - }; + } // Add paste event listener to the document - document.addEventListener("paste", handlePaste); + document.addEventListener('paste', handlePaste) return () => { - document.removeEventListener("paste", handlePaste); - }; - }, [handleFile]); + document.removeEventListener('paste', handlePaste) + } + }, [handleFile]) - const active = isDragActive || dropzoneActive; + const active = isDragActive || dropzoneActive return ( -
+

Upload Image

@@ -105,22 +109,22 @@ export function UploadArea({ onUpload, error, className }: UploadAreaProps) {
- +
@@ -131,11 +135,10 @@ export function UploadArea({ onUpload, error, className }: UploadAreaProps) {

Drop the image here...

) : (
-

- Drag & drop an image here -

+

Drag & drop an image here

- or tap to browse • PNG, JPG, WEBP up to {MAX_IMAGE_SIZE / 1024 / 1024}MB • or paste an image + or tap to browse • PNG, JPG, WEBP up to {MAX_IMAGE_SIZE / 1024 / 1024}MB • or paste + an image

)} @@ -148,6 +151,5 @@ export function UploadArea({ onUpload, error, className }: UploadAreaProps) { )}
- ); + ) } - diff --git a/components/editor/editor-bottom-bar.tsx b/components/editor/editor-bottom-bar.tsx index 0fd7c69..874de5f 100644 --- a/components/editor/editor-bottom-bar.tsx +++ b/components/editor/editor-bottom-bar.tsx @@ -1,57 +1,55 @@ -'use client'; +'use client' -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Shuffle, Undo2, Redo2 } from 'lucide-react'; -import { FaGithub } from 'react-icons/fa'; -import { SponsorButton } from '@/components/SponsorButton'; -import { motion, useSpring, useTransform } from 'motion/react'; +import * as React from 'react' +import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { Shuffle, Undo2, Redo2 } from 'lucide-react' +import { FaGithub } from 'react-icons/fa' +import { SponsorButton } from '@/components/SponsorButton' +import { motion, useSpring, useTransform } from 'motion/react' function useGitHubStars() { - const [stars, setStars] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [stars, setStars] = useState(null) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const fetchStars = async () => { try { - const response = await fetch("https://api.github.com/repos/KartikLabhshetwar/stage"); + const response = await fetch('https://api.github.com/repos/KartikLabhshetwar/stage') if (response.ok) { - const data = await response.json(); - setStars(data.stargazers_count); + const data = await response.json() + setStars(data.stargazers_count) } } catch (error) { - console.error("Failed to fetch GitHub stars:", error); + console.error('Failed to fetch GitHub stars:', error) } finally { - setIsLoading(false); + setIsLoading(false) } - }; + } - fetchStars(); - }, []); + fetchStars() + }, []) - return { stars, isLoading }; + return { stars, isLoading } } function AnimatedCounter({ value }: { value: number }) { const spring = useSpring(0, { damping: 30, stiffness: 100, - }); + }) useEffect(() => { - spring.set(value); - }, [spring, value]); + spring.set(value) + }, [spring, value]) - const display = useTransform(spring, (current) => - Math.round(current).toLocaleString() - ); + const display = useTransform(spring, (current) => Math.round(current).toLocaleString()) - return {display}; + return {display} } export function EditorBottomBar() { - const { stars, isLoading } = useGitHubStars(); + const { stars, isLoading } = useGitHubStars() return (
@@ -84,6 +82,5 @@ export function EditorBottomBar() {
- ); + ) } - diff --git a/components/editor/editor-left-panel.tsx b/components/editor/editor-left-panel.tsx index db3c54e..853b496 100644 --- a/components/editor/editor-left-panel.tsx +++ b/components/editor/editor-left-panel.tsx @@ -1,31 +1,27 @@ -'use client'; +'use client' -import * as React from 'react'; -import Link from 'next/link'; -import Image from 'next/image'; -import { TextOverlayControls } from '@/components/text-overlay/text-overlay-controls'; -import { OverlayGallery, OverlayControls } from '@/components/overlays'; -import { MockupGallery, MockupControls } from '@/components/mockups'; -import { StyleTabs } from './style-tabs'; -import { Button } from '@/components/ui/button'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Download, Trash2, Copy, ImageIcon, Type, Sticker } from 'lucide-react'; -import { useImageStore } from '@/lib/store'; -import { ExportDialog } from '@/components/canvas/dialogs/ExportDialog'; -import { useExport } from '@/hooks/useExport'; -import { PresetSelector } from '@/components/presets/PresetSelector'; -import { FaXTwitter } from 'react-icons/fa6'; +import * as React from 'react' +import Link from 'next/link' +import Image from 'next/image' +import { TextOverlayControls } from '@/components/text-overlay/text-overlay-controls' +import { OverlayGallery, OverlayControls } from '@/components/overlays' +import { MockupGallery, MockupControls } from '@/components/mockups' +import { StyleTabs } from './style-tabs' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Download, Trash2, Copy, ImageIcon, Type, Sticker } from 'lucide-react' +import { useImageStore } from '@/lib/store' +import { ExportDialog } from '@/components/canvas/dialogs/ExportDialog' +import { useExport } from '@/hooks/useExport' +import { PresetSelector } from '@/components/presets/PresetSelector' +import { FaXTwitter } from 'react-icons/fa6' export function EditorLeftPanel() { - const { - uploadedImageUrl, - selectedAspectRatio, - clearImage, - } = useImageStore(); - - const [exportDialogOpen, setExportDialogOpen] = React.useState(false); - const [copySuccess, setCopySuccess] = React.useState(false); - const [activeTab, setActiveTab] = React.useState('image'); + const { uploadedImageUrl, selectedAspectRatio, clearImage } = useImageStore() + + const [exportDialogOpen, setExportDialogOpen] = React.useState(false) + const [copySuccess, setCopySuccess] = React.useState(false) + const [activeTab, setActiveTab] = React.useState('image') const { settings: exportSettings, @@ -33,7 +29,7 @@ export function EditorLeftPanel() { updateScale, exportImage, copyImage, - } = useExport(selectedAspectRatio); + } = useExport(selectedAspectRatio) return ( <> @@ -41,14 +37,11 @@ export function EditorLeftPanel() { {/* Header */}
- - Stage + + Stage
@@ -66,23 +59,27 @@ export function EditorLeftPanel() {
{/* Tabs Navigation */} - + - Image - Text - @@ -96,10 +93,10 @@ export function EditorLeftPanel() { {/* Style Controls */} - + {/* Mockup Gallery */} - + {/* Mockup Controls */} @@ -112,7 +109,7 @@ export function EditorLeftPanel() { {/* Overlay Gallery */} - + {/* Image Overlays Section */} @@ -134,13 +131,13 @@ export function EditorLeftPanel() { onClick={() => { copyImage() .then(() => { - setCopySuccess(true); - setTimeout(() => setCopySuccess(false), 2000); + setCopySuccess(true) + setTimeout(() => setCopySuccess(false), 2000) }) .catch((error) => { - console.error('Failed to copy:', error); - alert('Failed to copy image to clipboard. Please try again.'); - }); + console.error('Failed to copy:', error) + alert('Failed to copy image to clipboard. Please try again.') + }) }} disabled={!uploadedImageUrl || isExporting} className="flex-1 h-11 justify-center gap-2 rounded-xl bg-muted hover:bg-muted/80 text-foreground shadow-sm hover:shadow-md transition-all font-medium border border-border" @@ -170,6 +167,5 @@ export function EditorLeftPanel() { onScaleChange={updateScale} /> - ); + ) } - diff --git a/components/editor/editor-right-panel.tsx b/components/editor/editor-right-panel.tsx index 5e2d1fd..a8596a8 100644 --- a/components/editor/editor-right-panel.tsx +++ b/components/editor/editor-right-panel.tsx @@ -1,24 +1,29 @@ -'use client'; +'use client' -import * as React from 'react'; -import { useImageStore } from '@/lib/store'; -import { AspectRatioDropdown } from '@/components/aspect-ratio/aspect-ratio-dropdown'; -import { Button } from '@/components/ui/button'; -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { aspectRatios } from '@/lib/constants/aspect-ratios'; -import { useDropzone } from 'react-dropzone'; -import { ALLOWED_IMAGE_TYPES, MAX_IMAGE_SIZE } from '@/lib/constants'; -import { getCldImageUrl } from '@/lib/cloudinary'; -import { backgroundCategories, getAvailableCategories, cloudinaryPublicIds } from '@/lib/cloudinary-backgrounds'; -import { gradientColors, type GradientKey } from '@/lib/constants/gradient-colors'; -import { solidColors, type SolidColorKey } from '@/lib/constants/solid-colors'; -import { Label } from '@/components/ui/label'; -import { Slider } from '@/components/ui/slider'; -import { FaImage, FaTimes } from 'react-icons/fa'; -import { BackgroundEffects } from '@/components/controls/BackgroundEffects'; +import * as React from 'react' +import { useImageStore } from '@/lib/store' +import { AspectRatioDropdown } from '@/components/aspect-ratio/aspect-ratio-dropdown' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { ChevronDown, ChevronUp } from 'lucide-react' +import { aspectRatios } from '@/lib/constants/aspect-ratios' +import { useDropzone } from 'react-dropzone' +import { ALLOWED_IMAGE_TYPES, MAX_IMAGE_SIZE } from '@/lib/constants' +import { getCldImageUrl } from '@/lib/cloudinary' +import { + backgroundCategories, + getAvailableCategories, + cloudinaryPublicIds, +} from '@/lib/cloudinary-backgrounds' +import { gradientColors, type GradientKey } from '@/lib/constants/gradient-colors' +import { solidColors, type SolidColorKey } from '@/lib/constants/solid-colors' +import { Label } from '@/components/ui/label' +import { Slider } from '@/components/ui/slider' +import { FaImage, FaTimes } from 'react-icons/fa' +import { BackgroundEffects } from '@/components/controls/BackgroundEffects' export function EditorRightPanel() { - const { + const { selectedAspectRatio, backgroundConfig, backgroundBorderRadius, @@ -26,56 +31,62 @@ export function EditorRightPanel() { setBackgroundValue, setBackgroundOpacity, setBackgroundBorderRadius, - } = useImageStore(); - - const [expanded, setExpanded] = React.useState(true); - const [bgUploadError, setBgUploadError] = React.useState(null); - const selectedRatio = aspectRatios.find((ar) => ar.id === selectedAspectRatio); + } = useImageStore() + + const [expanded, setExpanded] = React.useState(true) + const [bgUploadError, setBgUploadError] = React.useState(null) + const selectedRatio = aspectRatios.find((ar) => ar.id === selectedAspectRatio) const validateFile = (file: File): string | null => { if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { - return `File type not supported. Please use: ${ALLOWED_IMAGE_TYPES.join(', ')}`; + return `File type not supported. Please use: ${ALLOWED_IMAGE_TYPES.join(', ')}` } if (file.size > MAX_IMAGE_SIZE) { - return `File size too large. Maximum size is ${MAX_IMAGE_SIZE / 1024 / 1024}MB`; + return `File size too large. Maximum size is ${MAX_IMAGE_SIZE / 1024 / 1024}MB` } - return null; - }; + return null + } const onBgDrop = React.useCallback( (acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { - const file = acceptedFiles[0]; - const validationError = validateFile(file); + const file = acceptedFiles[0] + const validationError = validateFile(file) if (validationError) { - setBgUploadError(validationError); - return; + setBgUploadError(validationError) + return } - setBgUploadError(null); - const blobUrl = URL.createObjectURL(file); - setBackgroundValue(blobUrl); - setBackgroundType('image'); + setBgUploadError(null) + const blobUrl = URL.createObjectURL(file) + setBackgroundValue(blobUrl) + setBackgroundType('image') } }, [setBackgroundValue, setBackgroundType] - ); + ) - const { getRootProps: getBgRootProps, getInputProps: getBgInputProps, isDragActive: isBgDragActive } = useDropzone({ + const { + getRootProps: getBgRootProps, + getInputProps: getBgInputProps, + isDragActive: isBgDragActive, + } = useDropzone({ onDrop: onBgDrop, accept: { 'image/*': ALLOWED_IMAGE_TYPES.map((type) => type.split('/')[1]), }, maxSize: MAX_IMAGE_SIZE, multiple: false, - }); + }) return (
{/* Header */}
-

Canvas Settings

+

+ Canvas Settings +

- + {expanded && ( <> {/* Aspect Ratio */} @@ -93,7 +104,8 @@ export function EditorRightPanel() {
{selectedRatio && (
- {selectedRatio.width}:{selectedRatio.height} • {selectedRatio.width}x{selectedRatio.height} + {selectedRatio.width}:{selectedRatio.height} • {selectedRatio.width}x + {selectedRatio.height}
)} @@ -108,61 +120,71 @@ export function EditorRightPanel() {
{/* Background Section */}
-

Background

- +

+ Background +

+ {/* Background Type Selector */}
-
- - + - + + }`} + > + Gradient +
- + {/* Gradient Selector */} {backgroundConfig.type === 'gradient' && (
@@ -177,7 +199,7 @@ export function EditorRightPanel() { ? 'border-primary ring-2 ring-ring shadow-sm' : 'border-border hover:border-border/80' }`} - style={{ + style={{ background: gradientColors[key], }} title={key.replace(/_/g, ' ')} @@ -188,26 +210,26 @@ export function EditorRightPanel() { )} {/* Solid Color Selector */} - {backgroundConfig.type === 'solid' && ( -
+ {backgroundConfig.type === 'solid' && ( +
{(Object.keys(solidColors) as SolidColorKey[]).map((key) => ( -
+ /> + ))} +
)} @@ -215,80 +237,89 @@ export function EditorRightPanel() { {backgroundConfig.type === 'image' && (
{/* Current Background Preview */} - {backgroundConfig.value && - (backgroundConfig.value.startsWith('blob:') || - backgroundConfig.value.startsWith('http') || - backgroundConfig.value.startsWith('data:') || - cloudinaryPublicIds.includes(backgroundConfig.value)) && ( -
- -
- {(() => { - // Check if it's a Cloudinary public ID - const isCloudinaryPublicId = typeof backgroundConfig.value === 'string' && - !backgroundConfig.value.startsWith('blob:') && - !backgroundConfig.value.startsWith('http') && - !backgroundConfig.value.startsWith('data:') && - cloudinaryPublicIds.includes(backgroundConfig.value); - - let imageUrl = backgroundConfig.value as string; - - // If it's a Cloudinary public ID, get the optimized URL - if (isCloudinaryPublicId) { - imageUrl = getCldImageUrl({ - src: backgroundConfig.value as string, - width: 600, - height: 400, - quality: 'auto', - format: 'auto', - crop: 'fill', - gravity: 'auto', - }); - } - - return ( - <> - Current background - - - ); - })()} + {backgroundConfig.value && + (backgroundConfig.value.startsWith('blob:') || + backgroundConfig.value.startsWith('http') || + backgroundConfig.value.startsWith('data:') || + cloudinaryPublicIds.includes(backgroundConfig.value)) && ( +
+ +
+ {(() => { + // Check if it's a Cloudinary public ID + const isCloudinaryPublicId = + typeof backgroundConfig.value === 'string' && + !backgroundConfig.value.startsWith('blob:') && + !backgroundConfig.value.startsWith('http') && + !backgroundConfig.value.startsWith('data:') && + cloudinaryPublicIds.includes(backgroundConfig.value) + + let imageUrl = backgroundConfig.value as string + + // If it's a Cloudinary public ID, get the optimized URL + if (isCloudinaryPublicId) { + imageUrl = getCldImageUrl({ + src: backgroundConfig.value as string, + width: 600, + height: 400, + quality: 'auto', + format: 'auto', + crop: 'fill', + gravity: 'auto', + }) + } + + return ( + <> + Current background + + + ) + })()} +
-
- )} + )} {/* Preset Backgrounds */} {backgroundCategories && Object.keys(backgroundCategories).length > 0 && (
- +
{getAvailableCategories() - .filter((category: string) => category !== 'demo' && category !== 'nature') + .filter( + (category: string) => category !== 'demo' && category !== 'nature' + ) .map((category: string) => { - const categoryBackgrounds = backgroundCategories[category]; - if (!categoryBackgrounds || categoryBackgrounds.length === 0) return null; + const categoryBackgrounds = backgroundCategories[category] + if (!categoryBackgrounds || categoryBackgrounds.length === 0) + return null - const categoryDisplayName = category.charAt(0).toUpperCase() + category.slice(1); + const categoryDisplayName = + category.charAt(0).toUpperCase() + category.slice(1) return (
@@ -305,14 +336,14 @@ export function EditorRightPanel() { format: 'auto', crop: 'fill', gravity: 'auto', - }); + }) return ( - ); + ) })}
- ); + ) })}
@@ -340,7 +371,9 @@ export function EditorRightPanel() { {/* Upload Background Image */}
- +
-
+
{isBgDragActive ? ( -

Drop the image here...

+

+ Drop the image here... +

) : (

Drag & drop an image here

- or click to browse • PNG, JPG, WEBP up to {MAX_IMAGE_SIZE / 1024 / 1024}MB + or click to browse • PNG, JPG, WEBP up to {MAX_IMAGE_SIZE / 1024 / 1024} + MB

)} @@ -372,8 +412,25 @@ export function EditorRightPanel() {
)}
-
- )} +
+ )} + + {/* Background Image URL */} + {backgroundConfig.type === 'image' && ( +
+ + { + setBackgroundValue(e.target.value) + }} + placeholder="Enter image URL" + className="text-xs" + /> +
+ )} {/* Border Radius */}
@@ -406,7 +463,9 @@ export function EditorRightPanel() {
- {backgroundBorderRadius}px + + {backgroundBorderRadius}px +
- {Math.round((backgroundConfig.opacity !== undefined ? backgroundConfig.opacity : 1) * 100)}% + {Math.round( + (backgroundConfig.opacity !== undefined ? backgroundConfig.opacity : 1) * 100 + )} + %
)}
- ); + ) } - diff --git a/components/editor/sidebar-left.tsx b/components/editor/sidebar-left.tsx index d2705fe..16dfd39 100644 --- a/components/editor/sidebar-left.tsx +++ b/components/editor/sidebar-left.tsx @@ -1,27 +1,20 @@ -'use client'; +'use client' -import * as React from 'react'; -import { - Sidebar, - SidebarContent, - SidebarHeader, - SidebarFooter, -} from '@/components/ui/sidebar'; -import { useImageStore } from '@/lib/store'; -import { ExportDialog } from '@/components/canvas/dialogs/ExportDialog'; -import { StyleTabs } from './style-tabs'; -import { Button } from '@/components/ui/button'; -import { Download, Trash2 } from 'lucide-react'; -import { PresetSelector } from '@/components/presets/PresetSelector'; -import { useExport } from '@/hooks/useExport'; -import { FaGithub } from 'react-icons/fa'; +import * as React from 'react' +import { Sidebar, SidebarContent, SidebarHeader, SidebarFooter } from '@/components/ui/sidebar' +import { useImageStore } from '@/lib/store' +import { ExportDialog } from '@/components/canvas/dialogs/ExportDialog' +import { StyleTabs } from './style-tabs' +import { Button } from '@/components/ui/button' +import { Download, Trash2 } from 'lucide-react' +import { PresetSelector } from '@/components/presets/PresetSelector' +import { useExport } from '@/hooks/useExport' +import { FaGithub } from 'react-icons/fa' -export function SidebarLeft({ - ...props -}: React.ComponentProps) { - const { - uploadedImageUrl, - selectedAspectRatio, +export function SidebarLeft({ ...props }: React.ComponentProps) { + const { + uploadedImageUrl, + selectedAspectRatio, clearImage, selectedGradient, borderRadius, @@ -32,21 +25,21 @@ export function SidebarLeft({ imageScale, imageBorder, imageShadow, - } = useImageStore(); - const [exportDialogOpen, setExportDialogOpen] = React.useState(false); + } = useImageStore() + const [exportDialogOpen, setExportDialogOpen] = React.useState(false) const { settings: exportSettings, isExporting, updateScale, exportImage, - } = useExport(selectedAspectRatio); + } = useExport(selectedAspectRatio) return ( <> - @@ -106,5 +99,5 @@ export function SidebarLeft({ onScaleChange={updateScale} /> - ); + ) } diff --git a/components/editor/style-tabs.tsx b/components/editor/style-tabs.tsx index fa6d7a3..93b52e7 100644 --- a/components/editor/style-tabs.tsx +++ b/components/editor/style-tabs.tsx @@ -1,14 +1,14 @@ -'use client'; +'use client' -import * as React from 'react'; -import { useImageStore } from '@/lib/store'; -import { Slider } from '@/components/ui/slider'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; -import { BorderControls } from '@/components/controls/BorderControls'; -import { ShadowControls } from '@/components/controls/ShadowControls'; -import { Perspective3DControls } from '@/components/controls/Perspective3DControls'; +import * as React from 'react' +import { useImageStore } from '@/lib/store' +import { Slider } from '@/components/ui/slider' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' +import { BorderControls } from '@/components/controls/BorderControls' +import { ShadowControls } from '@/components/controls/ShadowControls' +import { Perspective3DControls } from '@/components/controls/Perspective3DControls' export function StyleTabs() { const { @@ -20,12 +20,12 @@ export function StyleTabs() { setImageOpacity, setImageScale, setImageShadow, - } = useImageStore(); + } = useImageStore() return (

IMAGE

- + Style @@ -38,87 +38,91 @@ export function StyleTabs() { -
- -
- - -
-
- - {borderRadius}px +
+ +
+ + +
+
+ + {borderRadius}px +
+ setBorderRadius(value[0])} + min={0} + max={100} + step={1} + />
- setBorderRadius(value[0])} - min={0} - max={100} - step={1} - /> -
- + + + - +
+
+ +
+ setImageScale(value[0])} + min={10} + max={200} + step={1} + /> + + {imageScale}% + +
+
+

+ Adjust the size of the image (10% - 200%) +

+
-
- +
setImageScale(value[0])} - min={10} - max={200} - step={1} + value={[imageOpacity]} + onValueChange={(value) => setImageOpacity(value[0])} + min={0} + max={1} + step={0.01} /> - {imageScale}% + {Math.round(imageOpacity * 100)}%
-

- Adjust the size of the image (10% - 200%) -

-
- -
- -
- setImageOpacity(value[0])} - min={0} - max={1} - step={0.01} - /> - - {Math.round(imageOpacity * 100)}% - -
-
@@ -126,5 +130,5 @@ export function StyleTabs() {
- ); + ) } diff --git a/components/export/FormatSelector.tsx b/components/export/FormatSelector.tsx index c5ac82a..253c8c0 100644 --- a/components/export/FormatSelector.tsx +++ b/components/export/FormatSelector.tsx @@ -2,12 +2,12 @@ * Format selector component for export options */ -import { Button } from '@/components/ui/button'; -import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' interface FormatSelectorProps { - format: 'png'; - onFormatChange: (format: 'png') => void; + format: 'png' + onFormatChange: (format: 'png') => void } export function FormatSelector({ format, onFormatChange }: FormatSelectorProps) { @@ -17,13 +17,12 @@ export function FormatSelector({ format, onFormatChange }: FormatSelectorProps)
- ); + ) } - diff --git a/components/export/QualitySlider.tsx b/components/export/QualitySlider.tsx index f8952a9..7ff13e5 100644 --- a/components/export/QualitySlider.tsx +++ b/components/export/QualitySlider.tsx @@ -2,19 +2,19 @@ * Quality slider component for JPEG export options */ -import { Slider } from '@/components/ui/slider'; -import { Label } from '@/components/ui/label'; +import { Slider } from '@/components/ui/slider' +import { Label } from '@/components/ui/label' interface QualitySliderProps { - quality: number; - onQualityChange: (quality: number) => void; - min?: number; - max?: number; - step?: number; + quality: number + onQualityChange: (quality: number) => void + min?: number + max?: number + step?: number } -export function QualitySlider({ - quality, +export function QualitySlider({ + quality, onQualityChange, min = 0.1, max = 1, @@ -31,9 +31,10 @@ export function QualitySlider({ max={max} step={step} /> - {Math.round(quality * 100)}% + + {Math.round(quality * 100)}% +
- ); + ) } - diff --git a/components/export/ScaleSlider.tsx b/components/export/ScaleSlider.tsx index 04818f8..82cc2fa 100644 --- a/components/export/ScaleSlider.tsx +++ b/components/export/ScaleSlider.tsx @@ -2,19 +2,19 @@ * Resolution scale slider component for export options */ -import { Slider } from '@/components/ui/slider'; -import { Label } from '@/components/ui/label'; +import { Slider } from '@/components/ui/slider' +import { Label } from '@/components/ui/label' interface ScaleSliderProps { - scale: number; - onScaleChange: (scale: number) => void; - min?: number; - max?: number; - step?: number; + scale: number + onScaleChange: (scale: number) => void + min?: number + max?: number + step?: number } -export function ScaleSlider({ - scale, +export function ScaleSlider({ + scale, onScaleChange, min = 1, max = 5, @@ -22,7 +22,9 @@ export function ScaleSlider({ }: ScaleSliderProps) { return (
- +
{scale}x
- ); + ) } - diff --git a/components/export/index.ts b/components/export/index.ts index 11d9cc3..aa90cd7 100644 --- a/components/export/index.ts +++ b/components/export/index.ts @@ -2,7 +2,6 @@ * Export settings components index */ -export { FormatSelector } from './FormatSelector'; -export { ScaleSlider } from './ScaleSlider'; -export { QualitySlider } from './QualitySlider'; - +export { FormatSelector } from './FormatSelector' +export { ScaleSlider } from './ScaleSlider' +export { QualitySlider } from './QualitySlider' diff --git a/components/image-render/index.ts b/components/image-render/index.ts index 410c629..fdf65e1 100644 --- a/components/image-render/index.ts +++ b/components/image-render/index.ts @@ -1,3 +1 @@ - -export { TextOverlayRenderer } from './text-overlay-renderer'; - +export { TextOverlayRenderer } from './text-overlay-renderer' diff --git a/components/image-render/text-overlay-renderer.tsx b/components/image-render/text-overlay-renderer.tsx index 729a965..f341b74 100644 --- a/components/image-render/text-overlay-renderer.tsx +++ b/components/image-render/text-overlay-renderer.tsx @@ -1,24 +1,23 @@ -'use client'; +'use client' -import { useImageStore } from '@/lib/store'; -import { getFontCSS } from '@/lib/constants/fonts'; +import { useImageStore } from '@/lib/store' +import { getFontCSS } from '@/lib/constants/fonts' export const TextOverlayRenderer = () => { - const { textOverlays } = useImageStore(); + const { textOverlays } = useImageStore() return ( <> {textOverlays.map((overlay) => { - if (!overlay.isVisible) return null; + if (!overlay.isVisible) return null const textShadowStyle = overlay.textShadow.enabled ? { textShadow: `${overlay.textShadow.offsetX}px ${overlay.textShadow.offsetY}px ${overlay.textShadow.blur}px ${overlay.textShadow.color}`, } - : {}; + : {} - const writingMode = - overlay.orientation === 'vertical' ? 'vertical-rl' : 'horizontal-tb'; + const writingMode = overlay.orientation === 'vertical' ? 'vertical-rl' : 'horizontal-tb' return (
{ > {overlay.text}
- ); + ) })} - ); -}; - + ) +} diff --git a/components/landing/FAQ.tsx b/components/landing/FAQ.tsx index f44bed0..cb01e26 100644 --- a/components/landing/FAQ.tsx +++ b/components/landing/FAQ.tsx @@ -1,76 +1,88 @@ -"use client"; +'use client' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, -} from "@/components/ui/accordion"; -import { Instrument_Serif } from "next/font/google"; +} from '@/components/ui/accordion' +import { Instrument_Serif } from 'next/font/google' const instrumentSerif = Instrument_Serif({ - weight: ["400"], - subsets: ["latin"], -}); + weight: ['400'], + subsets: ['latin'], +}) interface FAQItem { - question: string; - answer: string; + question: string + answer: string } interface FAQProps { - title?: string; - faqs?: FAQItem[]; + title?: string + faqs?: FAQItem[] } const defaultFAQs: FAQItem[] = [ { - question: "What is Stage?", - answer: "Stage is a modern web-based canvas editor that runs entirely in your browser. Create professional-looking designs for social media, presentations, or personal projects without any design software installation required.", + question: 'What is Stage?', + answer: + 'Stage is a modern web-based canvas editor that runs entirely in your browser. Create professional-looking designs for social media, presentations, or personal projects without any design software installation required.', }, { - question: "Do I need to create an account?", - answer: "No account required! Stage is completely free to use. Simply visit the editor and start creating your designs immediately—no sign-up, no login, no hassle.", + question: 'Do I need to create an account?', + answer: + 'No account required! Stage is completely free to use. Simply visit the editor and start creating your designs immediately—no sign-up, no login, no hassle.', }, { - question: "Is Stage free to use?", - answer: "Yes, Stage is completely free. Create unlimited designs, export without restrictions, and access all features at no cost. Just open your browser and start designing.", + question: 'Is Stage free to use?', + answer: + 'Yes, Stage is completely free. Create unlimited designs, export without restrictions, and access all features at no cost. Just open your browser and start designing.', }, { - question: "What can I create with Stage?", - answer: "You can create social media graphics (Instagram posts, stories, reels), image showcases, presentation visuals, and custom designs. Upload your images, add text overlays, customize backgrounds, apply professional presets, and export high-quality graphics.", + question: 'What can I create with Stage?', + answer: + 'You can create social media graphics (Instagram posts, stories, reels), image showcases, presentation visuals, and custom designs. Upload your images, add text overlays, customize backgrounds, apply professional presets, and export high-quality graphics.', }, { - question: "What export formats are available?", - answer: "Export your designs as PNG (with transparency support) or JPG. You can adjust the quality for JPG files and scale your exports up to 5x the original size for high-resolution output. Exported images include a subtle Stage watermark in the bottom-right corner. Perfect for both digital use and printing.", + question: 'What export formats are available?', + answer: + 'Export your designs as PNG (with transparency support) or JPG. You can adjust the quality for JPG files and scale your exports up to 5x the original size for high-resolution output. Exported images include a subtle Stage watermark in the bottom-right corner. Perfect for both digital use and printing.', }, { - question: "Which aspect ratios does Stage support?", - answer: "Stage supports Instagram formats (Square 1:1, Portrait 4:5, Story/Reel 9:16), social media formats (Landscape 16:9, Portrait 3:4), and standard photo formats. All formats are optimized for their respective platforms.", + question: 'Which aspect ratios does Stage support?', + answer: + 'Stage supports Instagram formats (Square 1:1, Portrait 4:5, Story/Reel 9:16), social media formats (Landscape 16:9, Portrait 3:4), and standard photo formats. All formats are optimized for their respective platforms.', }, { - question: "What are presets and how do I use them?", - answer: "Presets are one-click styling options that instantly transform your design. Stage includes 5 professional presets: Social Ready, Story Style, Minimal Clean, Bold Gradient, and Dark Elegant. Click any preset to apply it instantly to your canvas.", + question: 'What are presets and how do I use them?', + answer: + 'Presets are one-click styling options that instantly transform your design. Stage includes 5 professional presets: Social Ready, Story Style, Minimal Clean, Bold Gradient, and Dark Elegant. Click any preset to apply it instantly to your canvas.', }, { - question: "What image file formats can I upload?", - answer: "You can upload PNG, JPG, JPEG, or WEBP images. Each image can be up to 100MB in size. The editor handles all processing in your browser for fast, secure editing.", + question: 'What image file formats can I upload?', + answer: + 'You can upload PNG, JPG, JPEG, or WEBP images. Each image can be up to 100MB in size. The editor handles all processing in your browser for fast, secure editing.', }, { - question: "Can I save my designs to my computer?", - answer: "Yes! Export your completed designs directly to your device as PNG or JPG files. Save them anywhere you like—your desktop, cloud storage, or any folder. Your designs are yours to keep.", + question: 'Can I save my designs to my computer?', + answer: + 'Yes! Export your completed designs directly to your device as PNG or JPG files. Save them anywhere you like—your desktop, cloud storage, or any folder. Your designs are yours to keep.', }, { - question: "What customization options are available?", - answer: "For images: adjust size, opacity, borders (width, color, style), shadows, and border radius. For text: add multiple text overlays with custom fonts, colors, sizes, positions, and text shadows. For backgrounds: choose from gradients, solid colors, or upload your own background images.", + question: 'What customization options are available?', + answer: + 'For images: adjust size, opacity, borders (width, color, style), shadows, and border radius. For text: add multiple text overlays with custom fonts, colors, sizes, positions, and text shadows. For backgrounds: choose from gradients, solid colors, or upload your own background images.', }, -]; +] -export function FAQ({ title = "Frequently Asked Questions", faqs = defaultFAQs }: FAQProps) { +export function FAQ({ title = 'Frequently Asked Questions', faqs = defaultFAQs }: FAQProps) { return (
-

+

{title}

@@ -87,6 +99,5 @@ export function FAQ({ title = "Frequently Asked Questions", faqs = defaultFAQs }
- ); + ) } - diff --git a/components/landing/Features.tsx b/components/landing/Features.tsx index cd8f086..f106fb3 100644 --- a/components/landing/Features.tsx +++ b/components/landing/Features.tsx @@ -1,11 +1,11 @@ interface Feature { - title: string; - description: string; + title: string + description: string } interface FeaturesProps { - features: Feature[]; - title?: string; + features: Feature[] + title?: string } export function Features({ features, title }: FeaturesProps) { @@ -13,7 +13,9 @@ export function Features({ features, title }: FeaturesProps) {
{title && ( -

{title}

+

+ {title} +

)}
{features.map((feature, index) => ( @@ -27,6 +29,5 @@ export function Features({ features, title }: FeaturesProps) {
- ); + ) } - diff --git a/components/landing/Footer.tsx b/components/landing/Footer.tsx index 6349333..6e191de 100644 --- a/components/landing/Footer.tsx +++ b/components/landing/Footer.tsx @@ -1,16 +1,13 @@ -"use client"; +'use client' -import { FaGithub } from "react-icons/fa"; +import { FaGithub } from 'react-icons/fa' interface FooterProps { - brandName?: string; - additionalText?: string; + brandName?: string + additionalText?: string } -export function Footer({ - brandName = "Stage", - additionalText = "" -}: FooterProps) { +export function Footer({ brandName = 'Stage', additionalText = '' }: FooterProps) { return (
@@ -21,6 +18,5 @@ export function Footer({
- ); + ) } - diff --git a/components/landing/Hero.tsx b/components/landing/Hero.tsx index 1117837..71f609f 100644 --- a/components/landing/Hero.tsx +++ b/components/landing/Hero.tsx @@ -1,39 +1,41 @@ -"use client"; +'use client' -import { useState } from "react"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { HeroVideoDialog } from "@/components/ui/hero-video-dialog"; -import { Instrument_Serif } from "next/font/google"; +import { useState } from 'react' +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { HeroVideoDialog } from '@/components/ui/hero-video-dialog' +import { Instrument_Serif } from 'next/font/google' const instrumentSerif = Instrument_Serif({ - weight: ["400"], - subsets: ["latin"], -}); + weight: ['400'], + subsets: ['latin'], +}) interface HeroProps { - title: string; - subtitle?: string; - description: string; - ctaLabel?: string; - ctaHref?: string; + title: string + subtitle?: string + description: string + ctaLabel?: string + ctaHref?: string } export function Hero({ title, subtitle, description, - ctaLabel = "Get Started", - ctaHref = "/home", + ctaLabel = 'Get Started', + ctaHref = '/home', }: HeroProps) { - const [isVideoOpen, setIsVideoOpen] = useState(false); - const videoEmbedUrl = "https://www.youtube.com/embed/zDux_K4SsH0"; - const videoThumbnailUrl = "https://img.youtube.com/vi/zDux_K4SsH0/maxresdefault.jpg"; + const [isVideoOpen, setIsVideoOpen] = useState(false) + const videoEmbedUrl = 'https://www.youtube.com/embed/zDux_K4SsH0' + const videoThumbnailUrl = 'https://img.youtube.com/vi/zDux_K4SsH0/maxresdefault.jpg' return (
-

+

{title} {subtitle && ( <> @@ -51,13 +53,16 @@ export function Hero({
- -

- ); + ) } - diff --git a/components/landing/LandingPage.tsx b/components/landing/LandingPage.tsx index f2db8ca..55be85f 100644 --- a/components/landing/LandingPage.tsx +++ b/components/landing/LandingPage.tsx @@ -1,44 +1,44 @@ -import { Navigation } from "./Navigation"; -import { Hero } from "./Hero"; -import { Features } from "./Features"; -import { Footer } from "./Footer"; -import { MasonryGrid } from "./MasonryGrid"; -import { Pricing } from "./Pricing"; -import { FAQ } from "./FAQ"; -import { Sponsors, Sponsor } from "./Sponsors"; -import { SponsorButton } from "@/components/SponsorButton"; +import { Navigation } from './Navigation' +import { Hero } from './Hero' +import { Features } from './Features' +import { Footer } from './Footer' +import { MasonryGrid } from './MasonryGrid' +import { Pricing } from './Pricing' +import { FAQ } from './FAQ' +import { Sponsors, Sponsor } from './Sponsors' +import { SponsorButton } from '@/components/SponsorButton' interface Feature { - title: string; - description: string; + title: string + description: string } interface LandingPageProps { - heroTitle: string; - heroSubtitle?: string; - heroDescription: string; - ctaLabel?: string; - ctaHref?: string; - features: Feature[]; - featuresTitle?: string; - sponsors?: Sponsor[]; - sponsorsTitle?: string; - brandName?: string; - footerText?: string; + heroTitle: string + heroSubtitle?: string + heroDescription: string + ctaLabel?: string + ctaHref?: string + features: Feature[] + featuresTitle?: string + sponsors?: Sponsor[] + sponsorsTitle?: string + brandName?: string + footerText?: string } export function LandingPage({ heroTitle, heroSubtitle, heroDescription, - ctaLabel = "Get Started", - ctaHref = "/home", + ctaLabel = 'Get Started', + ctaHref = '/home', features, featuresTitle, sponsors, sponsorsTitle, - brandName = "Stage", - footerText = "Built with Next.js and Konva.", + brandName = 'Stage', + footerText = 'Built with Next.js and Konva.', }: LandingPageProps) { return (
@@ -55,9 +55,8 @@ export function LandingPage({ {/* */} -
+
- ); + ) } - diff --git a/components/landing/MasonryGrid.tsx b/components/landing/MasonryGrid.tsx index f9aa7f1..6d5d31b 100644 --- a/components/landing/MasonryGrid.tsx +++ b/components/landing/MasonryGrid.tsx @@ -1,46 +1,46 @@ -"use client"; +'use client' -import { OptimizedImage } from "@/components/ui/optimized-image"; -import { demoImagePublicIds } from "@/lib/cloudinary-demo-images"; +import { OptimizedImage } from '@/components/ui/optimized-image' +import { demoImagePublicIds } from '@/lib/cloudinary-demo-images' interface MasonryItem { - id: number; - image: string; - alt: string; - aspectRatio: string; + id: number + image: string + alt: string + aspectRatio: string } // Get aspect ratio based on demo image number const getAspectRatio = (index: number): string => { // Get demo number from index (1-based) - const demoNumber = index + 1; - + const demoNumber = index + 1 + // demo-1: long vertical card (tall portrait) if (demoNumber === 1) { - return "aspect-[2/3]"; // Long vertical card + return 'aspect-[2/3]' // Long vertical card } - + // demo-3 and demo-8: Social media posts/profile pages (horizontal/landscape) if (demoNumber === 3 || demoNumber === 8) { - return "aspect-[4/3]"; // Horizontal/landscape for social media posts + return 'aspect-[4/3]' // Horizontal/landscape for social media posts } - + // Landing pages: demo-2, 4, 5, 6, 9, 10, 11, 13, 14 (rectangle/landscape boxes) - const landingPageNumbers = [2, 4, 5, 6, 9, 10, 11, 13, 14]; + const landingPageNumbers = [2, 4, 5, 6, 9, 10, 11, 13, 14] if (landingPageNumbers.includes(demoNumber)) { - return "aspect-[16/9]"; // Rectangle/landscape box for landing pages + return 'aspect-[16/9]' // Rectangle/landscape box for landing pages } - + // Default ratios for other images (demo-7, 12, 15) const defaultRatios = [ - "aspect-[4/3]", // Classic - "aspect-square", // Square - "aspect-[3/4]", // Portrait - "aspect-[3/2]", // Landscape - "aspect-[5/4]", // Slightly tall - ]; - return defaultRatios[(demoNumber - 1) % defaultRatios.length]; -}; + 'aspect-[4/3]', // Classic + 'aspect-square', // Square + 'aspect-[3/4]', // Portrait + 'aspect-[3/2]', // Landscape + 'aspect-[5/4]', // Slightly tall + ] + return defaultRatios[(demoNumber - 1) % defaultRatios.length] +} // Use demo image Cloudinary public IDs directly with specific sizes const sampleItems: MasonryItem[] = demoImagePublicIds.map((publicId, index) => ({ @@ -48,17 +48,17 @@ const sampleItems: MasonryItem[] = demoImagePublicIds.map((publicId, index) => ( image: publicId, alt: `Gallery image ${index + 1}`, aspectRatio: getAspectRatio(index), -})); +})) export function MasonryGrid() { return (
{/* CSS Columns masonry layout */} -
{sampleItems.map((item) => ( @@ -85,5 +85,5 @@ export function MasonryGrid() {
- ); + ) } diff --git a/components/landing/Navigation.tsx b/components/landing/Navigation.tsx index ebbab8c..fe12a7c 100644 --- a/components/landing/Navigation.tsx +++ b/components/landing/Navigation.tsx @@ -1,77 +1,66 @@ -"use client"; +'use client' -import { useEffect, useState } from "react"; -import Link from "next/link"; -import Image from "next/image"; -import { Button } from "@/components/ui/button"; -import { FaGithub } from "react-icons/fa"; -import { SiX } from "react-icons/si"; -import { motion, useSpring, useTransform } from "motion/react"; +import { useEffect, useState } from 'react' +import Link from 'next/link' +import Image from 'next/image' +import { Button } from '@/components/ui/button' +import { FaGithub } from 'react-icons/fa' +import { SiX } from 'react-icons/si' +import { motion, useSpring, useTransform } from 'motion/react' interface NavigationProps { - ctaLabel?: string; - ctaHref?: string; + ctaLabel?: string + ctaHref?: string } function useGitHubStars() { - const [stars, setStars] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [stars, setStars] = useState(null) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const fetchStars = async () => { try { - const response = await fetch("https://api.github.com/repos/KartikLabhshetwar/stage"); + const response = await fetch('https://api.github.com/repos/KartikLabhshetwar/stage') if (response.ok) { - const data = await response.json(); - setStars(data.stargazers_count); + const data = await response.json() + setStars(data.stargazers_count) } } catch (error) { - console.error("Failed to fetch GitHub stars:", error); + console.error('Failed to fetch GitHub stars:', error) } finally { - setIsLoading(false); + setIsLoading(false) } - }; + } - fetchStars(); - }, []); + fetchStars() + }, []) - return { stars, isLoading }; + return { stars, isLoading } } function AnimatedCounter({ value }: { value: number }) { const spring = useSpring(0, { damping: 30, stiffness: 100, - }); + }) useEffect(() => { - spring.set(value); - }, [spring, value]); + spring.set(value) + }, [spring, value]) - const display = useTransform(spring, (current) => - Math.round(current).toLocaleString() - ); + const display = useTransform(spring, (current) => Math.round(current).toLocaleString()) - return {display}; + return {display} } -export function Navigation({ - ctaLabel = "Editor", - ctaHref = "/home" -}: NavigationProps) { - const { stars, isLoading } = useGitHubStars(); +export function Navigation({ ctaLabel = 'Editor', ctaHref = '/home' }: NavigationProps) { + const { stars, isLoading } = useGitHubStars() return ( - ); + ) } - diff --git a/components/landing/Pricing.tsx b/components/landing/Pricing.tsx index 340c55d..998f1f5 100644 --- a/components/landing/Pricing.tsx +++ b/components/landing/Pricing.tsx @@ -1,20 +1,29 @@ -"use client"; +'use client' -import Link from "next/link"; -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Instrument_Serif } from "next/font/google"; +import Link from 'next/link' +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Instrument_Serif } from 'next/font/google' const instrumentSerif = Instrument_Serif({ - weight: ["400"], - subsets: ["latin"], -}); + weight: ['400'], + subsets: ['latin'], +}) export function Pricing() { return (
-

+

Pricing

@@ -52,7 +61,10 @@ export function Pricing() { - @@ -102,6 +114,5 @@ export function Pricing() {
- ); + ) } - diff --git a/components/landing/Sponsors.tsx b/components/landing/Sponsors.tsx index 3e0176a..e637083 100644 --- a/components/landing/Sponsors.tsx +++ b/components/landing/Sponsors.tsx @@ -1,183 +1,185 @@ -"use client"; +'use client' -import Image from "next/image"; -import Link from "next/link"; -import { Instrument_Serif } from "next/font/google"; -import { Globe } from "lucide-react"; -import { FaXTwitter, FaGithub } from "react-icons/fa6"; +import Image from 'next/image' +import Link from 'next/link' +import { Instrument_Serif } from 'next/font/google' +import { Globe } from 'lucide-react' +import { FaXTwitter, FaGithub } from 'react-icons/fa6' const instrumentSerif = Instrument_Serif({ - weight: ["400"], - subsets: ["latin"], -}); + weight: ['400'], + subsets: ['latin'], +}) export interface Sponsor { - name: string; - avatar?: string; - avatarAlt?: string; - amount?: string; - amountType?: "one time" | "monthly" | "yearly"; - total?: string; - github?: string; - website?: string; - twitter?: string; - isSpecial?: boolean; - specialSince?: string; - url?: string; + name: string + avatar?: string + avatarAlt?: string + amount?: string + amountType?: 'one time' | 'monthly' | 'yearly' + total?: string + github?: string + website?: string + twitter?: string + isSpecial?: boolean + specialSince?: string + url?: string } interface SponsorsProps { - title?: string; - sponsors?: Sponsor[]; - showDescription?: boolean; + title?: string + sponsors?: Sponsor[] + showDescription?: boolean } const defaultSponsors: Sponsor[] = [ { - name: "Aayushman Singh", - avatar: "/aayushman.jpg", - avatarAlt: "aayushman singh", - amount: "$10.00", - amountType: "one time", - total: "$10.00", - github: "aayushman-singh", - twitter: "aayushman2703", - website: "https://aayushman.dev", - url: "https://x.com/aayushman2703", + name: 'Aayushman Singh', + avatar: '/aayushman.jpg', + avatarAlt: 'aayushman singh', + amount: '$10.00', + amountType: 'one time', + total: '$10.00', + github: 'aayushman-singh', + twitter: 'aayushman2703', + website: 'https://aayushman.dev', + url: 'https://x.com/aayushman2703', }, { - name: "Kanak Kumar Mahala", - avatar: "/kanak.jpg", - avatarAlt: "kanak kumar mahala", - amount: "$7.90", - amountType: "one time", - total: "$7.90", - github: "kanakk365", - twitter: "Lbringer_nikki", - website: "https://www.kanakk.me", - url: "https://x.com/kanak_k365", + name: 'Kanak Kumar Mahala', + avatar: '/kanak.jpg', + avatarAlt: 'kanak kumar mahala', + amount: '$7.90', + amountType: 'one time', + total: '$7.90', + github: 'kanakk365', + twitter: 'Lbringer_nikki', + website: 'https://www.kanakk.me', + url: 'https://x.com/kanak_k365', }, { - name: "Aditya Garimella", - avatar: "/aditya.jpg", - avatarAlt: "aditya garimella", - amount: "$7.88", - amountType: "one time", - total: "$7.88", - github: "", - twitter: "Lbringer_nikki", - website: "", - url: "https://x.com/Lbringer_nikki", + name: 'Aditya Garimella', + avatar: '/aditya.jpg', + avatarAlt: 'aditya garimella', + amount: '$7.88', + amountType: 'one time', + total: '$7.88', + github: '', + twitter: 'Lbringer_nikki', + website: '', + url: 'https://x.com/Lbringer_nikki', }, { - name: "Karan Kendre", - avatar: "/karan.jpg", - avatarAlt: "karan kendre", - amount: "$5.64", - amountType: "one time", - total: "$5.64", - github: "kendrekaran", - twitter: "karaan_dev", - website: "https://www.karaan.me/", - url: "https://x.com/karaan_dev", + name: 'Karan Kendre', + avatar: '/karan.jpg', + avatarAlt: 'karan kendre', + amount: '$5.64', + amountType: 'one time', + total: '$5.64', + github: 'kendrekaran', + twitter: 'karaan_dev', + website: 'https://www.karaan.me/', + url: 'https://x.com/karaan_dev', }, { - name: "Fardeen Mansoori", - avatar: "/fardeen.jpg", - avatarAlt: "fardeen mansoori", - amount: "$5.64", - amountType: "one time", - total: "$5.64", - github: "Fardeen26", - twitter: "fardeentwt", - website: "https://www.fardeen.me/", - url: "https://x.com/fardeentwt", + name: 'Fardeen Mansoori', + avatar: '/fardeen.jpg', + avatarAlt: 'fardeen mansoori', + amount: '$5.64', + amountType: 'one time', + total: '$5.64', + github: 'Fardeen26', + twitter: 'fardeentwt', + website: 'https://www.fardeen.me/', + url: 'https://x.com/fardeentwt', }, { - name: "Suhail Roushan", - avatar: "/suhail.png", - avatarAlt: "suhail roushan", - amount: "$5.00", - amountType: "one time", - total: "$5.00", - github: "suhailroushan13", - twitter: "0xsuhailroushan", - website: "https://suhailroushan.in/", - url: "https://x.com/0xsuhailroushan", + name: 'Suhail Roushan', + avatar: '/suhail.png', + avatarAlt: 'suhail roushan', + amount: '$5.00', + amountType: 'one time', + total: '$5.00', + github: 'suhailroushan13', + twitter: '0xsuhailroushan', + website: 'https://suhailroushan.in/', + url: 'https://x.com/0xsuhailroushan', }, { - name: "Arinjay Wyawhare", - avatar: "/arinjay.jpg", - avatarAlt: "arinjay wyawhare", - amount: "$4.74", - amountType: "one time", - total: "$4.74", - github: "jaywyawhare", - twitter: "jaywyawhare", - website: "https://jaywyawhare-github-io.vercel.app", - url: "https://x.com/jaywyawhare", + name: 'Arinjay Wyawhare', + avatar: '/arinjay.jpg', + avatarAlt: 'arinjay wyawhare', + amount: '$4.74', + amountType: 'one time', + total: '$4.74', + github: 'jaywyawhare', + twitter: 'jaywyawhare', + website: 'https://jaywyawhare-github-io.vercel.app', + url: 'https://x.com/jaywyawhare', }, { - name: "Chinmay Kabi", - avatar: "/chinmay.jpg", - avatarAlt: "chinmay kabi", - amount: "$2.82", - amountType: "one time", - total: "$2.82", - github: "", - twitter: "chinmaykabi", - website: "https://www.linkedin.com/in/chinmaykabi", - url: "https://x.com/ChinuKabi", + name: 'Chinmay Kabi', + avatar: '/chinmay.jpg', + avatarAlt: 'chinmay kabi', + amount: '$2.82', + amountType: 'one time', + total: '$2.82', + github: '', + twitter: 'chinmaykabi', + website: 'https://www.linkedin.com/in/chinmaykabi', + url: 'https://x.com/ChinuKabi', }, { - name: "Vedant Lamba", - avatar: "/vedant.jpg", - avatarAlt: "vedant lamba", - amount: "$1.14", - amountType: "one time", - total: "$1.14", - github: "vedantlamba", - twitter: "Vedantlamba", - website: "https://www.vedantlamba.com", - url: "https://x.com/Vedantlamba", + name: 'Vedant Lamba', + avatar: '/vedant.jpg', + avatarAlt: 'vedant lamba', + amount: '$1.14', + amountType: 'one time', + total: '$1.14', + github: 'vedantlamba', + twitter: 'Vedantlamba', + website: 'https://www.vedantlamba.com', + url: 'https://x.com/Vedantlamba', }, { - name: "Pranav Patil", - avatar: "/pranav.jpg", - avatarAlt: "pranav patil", - amount: "$1.14", - amountType: "one time", - total: "$1.14", - github: "21prnv", - twitter: "21prnv", - website: "https://www.prnv.space", - url: "https://x.com/21prnv", + name: 'Pranav Patil', + avatar: '/pranav.jpg', + avatarAlt: 'pranav patil', + amount: '$1.14', + amountType: 'one time', + total: '$1.14', + github: '21prnv', + twitter: '21prnv', + website: 'https://www.prnv.space', + url: 'https://x.com/21prnv', }, { - name: "Atharva Mhaske", - avatar: "/atharva.jpg", - avatarAlt: "atharva", - amount: "$1.14", - amountType: "one time", - total: "$1.14", - github: "atharvamhaske", - twitter: "AtharvaXDevs", - website: "https://atharvaxdevs.xyz/", - url: "https://x.com/AtharvaXDevs", + name: 'Atharva Mhaske', + avatar: '/atharva.jpg', + avatarAlt: 'atharva', + amount: '$1.14', + amountType: 'one time', + total: '$1.14', + github: 'atharvamhaske', + twitter: 'AtharvaXDevs', + website: 'https://atharvaxdevs.xyz/', + url: 'https://x.com/AtharvaXDevs', }, -]; +] -export function Sponsors({ - title = "Our Sponsors", +export function Sponsors({ + title = 'Our Sponsors', sponsors = defaultSponsors, - showDescription = false + showDescription = false, }: SponsorsProps) { - const hasSponsors = sponsors && sponsors.length > 0; + const hasSponsors = sponsors && sponsors.length > 0 return (
-

+

{title}

{hasSponsors ? ( @@ -185,7 +187,6 @@ export function Sponsors({ {sponsors.map((sponsor, index) => { const content = (
-
{/* Avatar */} @@ -214,11 +215,11 @@ export function Sponsors({

{sponsor.name}

- + {sponsor.amount && (

- {sponsor.amount} {sponsor.amountType || "one time"} + {sponsor.amount} {sponsor.amountType || 'one time'}

{sponsor.total && (

@@ -234,8 +235,12 @@ export function Sponsors({

- ); + ) if (sponsor.url) { return ( @@ -291,14 +300,14 @@ export function Sponsors({ > {content} - ); + ) } return (
{content}
- ); + ) })}
) : ( @@ -318,5 +327,5 @@ export function Sponsors({ )}
- ); + ) } diff --git a/components/mockups/ImacMockupRenderer.tsx b/components/mockups/ImacMockupRenderer.tsx index 92cfa25..5ff8402 100644 --- a/components/mockups/ImacMockupRenderer.tsx +++ b/components/mockups/ImacMockupRenderer.tsx @@ -134,4 +134,3 @@ export function ImacMockupRenderer({ mockup }: ImacMockupRendererProps) { ) } - diff --git a/components/mockups/IphoneMockupRenderer.tsx b/components/mockups/IphoneMockupRenderer.tsx index bf50d82..d2aaec1 100644 --- a/components/mockups/IphoneMockupRenderer.tsx +++ b/components/mockups/IphoneMockupRenderer.tsx @@ -122,5 +122,3 @@ export function IphoneMockupRenderer({ mockup }: IphoneMockupRendererProps) { ) } - - diff --git a/components/mockups/IwatchMockupRenderer.tsx b/components/mockups/IwatchMockupRenderer.tsx index 38b0d0b..53be7a3 100644 --- a/components/mockups/IwatchMockupRenderer.tsx +++ b/components/mockups/IwatchMockupRenderer.tsx @@ -138,4 +138,3 @@ export function IwatchMockupRenderer({ mockup }: IwatchMockupRendererProps) { ) } - diff --git a/components/mockups/MacbookMockupRenderer.tsx b/components/mockups/MacbookMockupRenderer.tsx index 888c175..f614353 100644 --- a/components/mockups/MacbookMockupRenderer.tsx +++ b/components/mockups/MacbookMockupRenderer.tsx @@ -134,5 +134,3 @@ export function MacbookMockupRenderer({ mockup }: MacbookMockupRendererProps) { ) } - - diff --git a/components/mockups/MockupControls.tsx b/components/mockups/MockupControls.tsx index d40b608..11fd645 100644 --- a/components/mockups/MockupControls.tsx +++ b/components/mockups/MockupControls.tsx @@ -9,18 +9,11 @@ import { getMockupDefinition } from '@/lib/constants/mockups' import Image from 'next/image' export function MockupControls() { - const { - mockups, - updateMockup, - removeMockup, - clearMockups, - } = useImageStore() + const { mockups, updateMockup, removeMockup, clearMockups } = useImageStore() const [selectedMockupId, setSelectedMockupId] = useState(null) - const selectedMockup = mockups.find( - (mockup) => mockup.id === selectedMockupId - ) + const selectedMockup = mockups.find((mockup) => mockup.id === selectedMockupId) const selectedDefinition = selectedMockup ? getMockupDefinition(selectedMockup.definitionId) @@ -119,9 +112,7 @@ export function MockupControls() { /> )}
- - {definition?.name || 'Mockup'} - + {definition?.name || 'Mockup'}

Position

- X Position + + X Position +
- {Math.round(selectedMockup.position.x)}px + + {Math.round(selectedMockup.position.x)}px +
- Y Position + + Y Position +
- {Math.round(selectedMockup.position.y)}px + + {Math.round(selectedMockup.position.y)}px +
@@ -241,4 +246,3 @@ export function MockupControls() {
) } - diff --git a/components/mockups/MockupGallery.tsx b/components/mockups/MockupGallery.tsx index 617a664..e84319b 100644 --- a/components/mockups/MockupGallery.tsx +++ b/components/mockups/MockupGallery.tsx @@ -16,29 +16,29 @@ export function MockupGallery() { const getDefaultPosition = (mockupSize: number, mockupType: string) => { const canvasWidth = responsiveDimensions.width || 1920 const canvasHeight = responsiveDimensions.height || 1080 - + let aspectRatio = 16 / 9 if (mockupType === 'iphone') aspectRatio = 9 / 16 else if (mockupType === 'iwatch') aspectRatio = 1 else if (mockupType === 'imac') aspectRatio = 2146 / 1207 - + const mockupHeight = mockupSize / aspectRatio - + return { - x: Math.max(20, (canvasWidth / 2) - (mockupSize / 2)), - y: Math.max(20, (canvasHeight / 2) - (mockupHeight / 2)), + x: Math.max(20, canvasWidth / 2 - mockupSize / 2), + y: Math.max(20, canvasHeight / 2 - mockupHeight / 2), } } const handleAddMockup = (definitionId: string) => { - const definition = MOCKUP_DEFINITIONS.find(d => d.id === definitionId) + const definition = MOCKUP_DEFINITIONS.find((d) => d.id === definitionId) let defaultSize = 600 if (definition?.type === 'iphone') defaultSize = 220 else if (definition?.type === 'iwatch') defaultSize = 150 else if (definition?.type === 'imac') defaultSize = 500 - + const defaultPosition = getDefaultPosition(defaultSize, definition?.type || 'macbook') - + addMockup({ definitionId, position: defaultPosition, @@ -59,36 +59,37 @@ export function MockupGallery() {

Device Mockups

-

- Add device frames to showcase your designs -

+

Add device frames to showcase your designs

- setActiveType(v as 'iphone' | 'macbook' | 'imac' | 'iwatch')}> + setActiveType(v as 'iphone' | 'macbook' | 'imac' | 'iwatch')} + > - MacBook - iMac - Watch - @@ -202,4 +203,3 @@ export function MockupGallery() {
) } - diff --git a/components/mockups/MockupRenderer.tsx b/components/mockups/MockupRenderer.tsx index 53b4f7f..e977fdf 100644 --- a/components/mockups/MockupRenderer.tsx +++ b/components/mockups/MockupRenderer.tsx @@ -16,18 +16,37 @@ interface MockupRendererProps { export function MockupRenderer({ mockup, canvasWidth, canvasHeight }: MockupRendererProps) { const definition = getMockupDefinition(mockup.definitionId) if (!definition) return null - + switch (definition.type) { case 'iphone': - return + return ( + + ) case 'macbook': - return + return ( + + ) case 'imac': - return + return ( + + ) case 'iwatch': - return + return ( + + ) default: return null } } - diff --git a/components/mockups/index.ts b/components/mockups/index.ts index e80ff59..4dfc691 100644 --- a/components/mockups/index.ts +++ b/components/mockups/index.ts @@ -5,4 +5,3 @@ export { IphoneMockupRenderer } from './IphoneMockupRenderer' export { MacbookMockupRenderer } from './MacbookMockupRenderer' export { ImacMockupRenderer } from './ImacMockupRenderer' export { IwatchMockupRenderer } from './IwatchMockupRenderer' - diff --git a/components/overlays/index.ts b/components/overlays/index.ts index f7de8ca..ad26186 100644 --- a/components/overlays/index.ts +++ b/components/overlays/index.ts @@ -1,4 +1,3 @@ export { OverlayGallery } from './overlay-gallery' export { OverlayRenderer } from './overlay-renderer' export { OverlayControls } from './overlay-controls' - diff --git a/components/overlays/overlay-controls.tsx b/components/overlays/overlay-controls.tsx index 26352df..8e2d718 100644 --- a/components/overlays/overlay-controls.tsx +++ b/components/overlays/overlay-controls.tsx @@ -9,18 +9,12 @@ import { getCldImageUrl } from '@/lib/cloudinary' import { OVERLAY_PUBLIC_IDS } from '@/lib/cloudinary-overlays' export function OverlayControls() { - const { - imageOverlays, - updateImageOverlay, - removeImageOverlay, - clearImageOverlays, - } = useImageStore() + const { imageOverlays, updateImageOverlay, removeImageOverlay, clearImageOverlays } = + useImageStore() const [selectedOverlayId, setSelectedOverlayId] = useState(null) - const selectedOverlay = imageOverlays.find( - (overlay) => overlay.id === selectedOverlayId - ) + const selectedOverlay = imageOverlays.find((overlay) => overlay.id === selectedOverlayId) const handleUpdateSize = (value: number[]) => { if (selectedOverlay) { @@ -108,30 +102,28 @@ export function OverlayControls() { }} className="h-6 w-6 p-0" > - {overlay.isVisible ? ( - - ) : ( - - )} + {overlay.isVisible ? : }
{(() => { // Check if this is a Cloudinary public ID or a custom upload - const isCloudinaryId = OVERLAY_PUBLIC_IDS.includes(overlay.src as any) || - (typeof overlay.src === 'string' && overlay.src.startsWith('overlays/')) - + const isCloudinaryId = + OVERLAY_PUBLIC_IDS.includes(overlay.src as any) || + (typeof overlay.src === 'string' && overlay.src.startsWith('overlays/')) + // Get the image URL - use Cloudinary if it's a Cloudinary ID, otherwise use the src directly - const imageUrl = isCloudinaryId && !overlay.isCustom - ? getCldImageUrl({ - src: overlay.src, - width: 32, - height: 32, - quality: 'auto', - format: 'auto', - crop: 'fit', - }) - : overlay.src - + const imageUrl = + isCloudinaryId && !overlay.isCustom + ? getCldImageUrl({ + src: overlay.src, + width: 32, + height: 32, + quality: 'auto', + format: 'auto', + crop: 'fit', + }) + : overlay.src + // Use regular img tag for Cloudinary URLs and data URLs return (
-

- Edit Overlay -

+

Edit Overlay

{/* Size */}
@@ -181,13 +171,17 @@ export function OverlayControls() { min={20} step={1} /> - {selectedOverlay.size}px + + {selectedOverlay.size}px +
{/* Rotation */}
- Rotation + + Rotation +
- {selectedOverlay.rotation}° + + {selectedOverlay.rotation}° +
@@ -211,7 +207,9 @@ export function OverlayControls() { min={0} step={0.01} /> - {Math.round(selectedOverlay.opacity * 100)}% + + {Math.round(selectedOverlay.opacity * 100)}% +
@@ -240,7 +238,9 @@ export function OverlayControls() {

Position

{/* X position */}
- X Position + + X Position +
- {Math.round(selectedOverlay.position.x)}px + + {Math.round(selectedOverlay.position.x)}px +
{/* Y position */}
- Y Position + + Y Position +
- {Math.round(selectedOverlay.position.y)}px + + {Math.round(selectedOverlay.position.y)}px +
@@ -288,4 +294,3 @@ export function OverlayControls() {
) } - diff --git a/components/overlays/overlay-gallery.tsx b/components/overlays/overlay-gallery.tsx index b211b86..680abb8 100644 --- a/components/overlays/overlay-gallery.tsx +++ b/components/overlays/overlay-gallery.tsx @@ -25,7 +25,7 @@ export function OverlayGallery() { const overlaySize = 150 // Position at top center: x = (canvasWidth / 2) - (overlaySize / 2), y = small offset from top return { - x: Math.max(20, (canvasWidth / 2) - (overlaySize / 2)), + x: Math.max(20, canvasWidth / 2 - overlaySize / 2), y: 30, // Small offset from top } } @@ -136,4 +136,3 @@ export function OverlayGallery() {
) } - diff --git a/components/overlays/overlay-renderer.tsx b/components/overlays/overlay-renderer.tsx index 3ec586b..7955df2 100644 --- a/components/overlays/overlay-renderer.tsx +++ b/components/overlays/overlay-renderer.tsx @@ -113,7 +113,7 @@ export function OverlayRenderer() {
{ - return ( - preset.aspectRatio === selectedAspectRatio && - preset.backgroundConfig.type === backgroundConfig.type && - preset.backgroundConfig.value === backgroundConfig.value && - preset.backgroundBorderRadius === backgroundBorderRadius && - preset.borderRadius === borderRadius && - preset.imageOpacity === imageOpacity && - preset.imageScale === imageScale && - preset.imageBorder.enabled === imageBorder.enabled && - preset.imageShadow.enabled === imageShadow.enabled && - (preset.backgroundBlur ?? 0) === backgroundBlur && - (preset.backgroundNoise ?? 0) === backgroundNoise - ); - }, [ - selectedAspectRatio, - backgroundConfig, - backgroundBorderRadius, - backgroundBlur, - backgroundNoise, - borderRadius, - imageOpacity, - imageScale, - imageBorder.enabled, - imageShadow.enabled, - ]); + const isPresetActive = React.useCallback( + (preset: PresetConfig) => { + return ( + preset.aspectRatio === selectedAspectRatio && + preset.backgroundConfig.type === backgroundConfig.type && + preset.backgroundConfig.value === backgroundConfig.value && + preset.backgroundBorderRadius === backgroundBorderRadius && + preset.borderRadius === borderRadius && + preset.imageOpacity === imageOpacity && + preset.imageScale === imageScale && + preset.imageBorder.enabled === imageBorder.enabled && + preset.imageShadow.enabled === imageShadow.enabled && + (preset.backgroundBlur ?? 0) === backgroundBlur && + (preset.backgroundNoise ?? 0) === backgroundNoise + ) + }, + [ + selectedAspectRatio, + backgroundConfig, + backgroundBorderRadius, + backgroundBlur, + backgroundNoise, + borderRadius, + imageOpacity, + imageScale, + imageBorder.enabled, + imageShadow.enabled, + ] + ) - const applyPreset = React.useCallback((preset: PresetConfig) => { - // Set all parameters from the preset - setAspectRatio(preset.aspectRatio); - setBackgroundConfig(preset.backgroundConfig); - setBackgroundType(preset.backgroundConfig.type); - setBackgroundValue(preset.backgroundConfig.value); - setBackgroundOpacity(preset.backgroundConfig.opacity ?? 1); - setBorderRadius(preset.borderRadius); - setBackgroundBorderRadius(preset.backgroundBorderRadius); - setImageOpacity(preset.imageOpacity); - setImageScale(preset.imageScale); - setImageBorder(preset.imageBorder); - setImageShadow(preset.imageShadow); - // Apply blur and noise if specified in preset - if (preset.backgroundBlur !== undefined) { - setBackgroundBlur(preset.backgroundBlur); - } - if (preset.backgroundNoise !== undefined) { - setBackgroundNoise(preset.backgroundNoise); - } - - // Close the popover after applying - setOpen(false); - }, [ - setAspectRatio, - setBackgroundConfig, - setBackgroundType, - setBackgroundValue, - setBackgroundOpacity, - setBorderRadius, - setBackgroundBorderRadius, - setBackgroundBlur, - setBackgroundNoise, - setImageOpacity, - setImageScale, - setImageBorder, - setImageShadow, - ]); + const applyPreset = React.useCallback( + (preset: PresetConfig) => { + // Set all parameters from the preset + setAspectRatio(preset.aspectRatio) + setBackgroundConfig(preset.backgroundConfig) + setBackgroundType(preset.backgroundConfig.type) + setBackgroundValue(preset.backgroundConfig.value) + setBackgroundOpacity(preset.backgroundConfig.opacity ?? 1) + setBorderRadius(preset.borderRadius) + setBackgroundBorderRadius(preset.backgroundBorderRadius) + setImageOpacity(preset.imageOpacity) + setImageScale(preset.imageScale) + setImageBorder(preset.imageBorder) + setImageShadow(preset.imageShadow) + // Apply blur and noise if specified in preset + if (preset.backgroundBlur !== undefined) { + setBackgroundBlur(preset.backgroundBlur) + } + if (preset.backgroundNoise !== undefined) { + setBackgroundNoise(preset.backgroundNoise) + } + + // Close the popover after applying + setOpen(false) + }, + [ + setAspectRatio, + setBackgroundConfig, + setBackgroundType, + setBackgroundValue, + setBackgroundOpacity, + setBorderRadius, + setBackgroundBorderRadius, + setBackgroundBlur, + setBackgroundNoise, + setImageOpacity, + setImageScale, + setImageBorder, + setImageShadow, + ] + ) return ( @@ -129,7 +131,7 @@ export function PresetSelector() { Apply pre-configured styles instantly

- +
{presets.map((preset) => ( ))}
- + {!uploadedImageUrl && (

@@ -204,6 +208,5 @@ export function PresetSelector() {

- ); + ) } - diff --git a/components/presets/index.ts b/components/presets/index.ts index a177917..0ae437e 100644 --- a/components/presets/index.ts +++ b/components/presets/index.ts @@ -1,2 +1 @@ -export { PresetSelector } from './PresetSelector'; - +export { PresetSelector } from './PresetSelector' diff --git a/components/templates/TemplatePreview.tsx b/components/templates/TemplatePreview.tsx index 46389e1..0566535 100644 --- a/components/templates/TemplatePreview.tsx +++ b/components/templates/TemplatePreview.tsx @@ -1,31 +1,31 @@ -"use client"; +'use client' -import type { Template } from "@/types/canvas"; +import type { Template } from '@/types/canvas' interface TemplatePreviewProps { - template: Template; - className?: string; + template: Template + className?: string } export function TemplatePreview({ template, className }: TemplatePreviewProps) { const getPreviewColor = (): string => { - if (template.background.type === "solid") { - return template.background.color || "#ffffff"; + if (template.background.type === 'solid') { + return template.background.color || '#ffffff' } - if (template.background.type === "gradient") { - return template.background.gradient?.colors[0] || "#ffffff"; + if (template.background.type === 'gradient') { + return template.background.gradient?.colors[0] || '#ffffff' } - return template.background.color || "#ffffff"; - }; + return template.background.color || '#ffffff' + } return (
- ); + ) } diff --git a/components/templates/TemplateSelector.tsx b/components/templates/TemplateSelector.tsx index 3267762..90c84fc 100644 --- a/components/templates/TemplateSelector.tsx +++ b/components/templates/TemplateSelector.tsx @@ -1,14 +1,14 @@ -"use client"; +'use client' -import { templates } from "@/lib/canvas/templates"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import type { Template } from "@/types/canvas"; -import { cn } from "@/lib/utils"; +import { templates } from '@/lib/canvas/templates' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import type { Template } from '@/types/canvas' +import { cn } from '@/lib/utils' interface TemplateSelectorProps { - selectedTemplateId?: string; - onSelectTemplate: (template: Template) => void; - className?: string; + selectedTemplateId?: string + onSelectTemplate: (template: Template) => void + className?: string } export function TemplateSelector({ @@ -17,17 +17,17 @@ export function TemplateSelector({ className, }: TemplateSelectorProps) { const getPreviewColor = (template: Template): string => { - if (template.background.type === "solid") { - return template.background.color || "#ffffff"; + if (template.background.type === 'solid') { + return template.background.color || '#ffffff' } - if (template.background.type === "gradient") { - return template.background.gradient?.colors[0] || "#ffffff"; + if (template.background.type === 'gradient') { + return template.background.gradient?.colors[0] || '#ffffff' } - return template.background.color || "#ffffff"; - }; + return template.background.color || '#ffffff' + } return ( -
+

Templates

@@ -39,8 +39,8 @@ export function TemplateSelector({ onSelectTemplate(template)} > @@ -51,14 +51,12 @@ export function TemplateSelector({ /> {template.name} {template.description && ( - - {template.description} - + {template.description} )} ))}

- ); + ) } diff --git a/components/text-overlay/index.ts b/components/text-overlay/index.ts index 94e2ab5..bd66a16 100644 --- a/components/text-overlay/index.ts +++ b/components/text-overlay/index.ts @@ -1,3 +1,2 @@ // Export text overlay components -export { TextOverlayControls } from './text-overlay-controls'; - +export { TextOverlayControls } from './text-overlay-controls' diff --git a/components/text-overlay/text-overlay-controls.tsx b/components/text-overlay/text-overlay-controls.tsx index 574f42c..5e3950b 100644 --- a/components/text-overlay/text-overlay-controls.tsx +++ b/components/text-overlay/text-overlay-controls.tsx @@ -1,43 +1,34 @@ -'use client'; +'use client' -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Slider } from '@/components/ui/slider'; +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Slider } from '@/components/ui/slider' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; -import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper'; -import { useImageStore } from '@/lib/store'; -import { Trash2, Eye, EyeOff } from 'lucide-react'; -import { fontFamilies, getAvailableFontWeights } from '@/lib/constants/fonts'; +} from '@/components/ui/select' +import { GlassInputWrapper } from '@/components/ui/glass-input-wrapper' +import { useImageStore } from '@/lib/store' +import { Trash2, Eye, EyeOff } from 'lucide-react' +import { fontFamilies, getAvailableFontWeights } from '@/lib/constants/fonts' export const TextOverlayControls = () => { - const { - textOverlays, - addTextOverlay, - updateTextOverlay, - removeTextOverlay, - clearTextOverlays, - } = useImageStore(); - - const [newText, setNewText] = useState(''); - const [selectedOverlayId, setSelectedOverlayId] = useState( - null - ); - - const selectedOverlay = textOverlays.find( - (overlay) => overlay.id === selectedOverlayId - ); + const { textOverlays, addTextOverlay, updateTextOverlay, removeTextOverlay, clearTextOverlays } = + useImageStore() + + const [newText, setNewText] = useState('') + const [selectedOverlayId, setSelectedOverlayId] = useState(null) + + const selectedOverlay = textOverlays.find((overlay) => overlay.id === selectedOverlayId) const handleAddText = () => { if (newText.trim()) { - const defaultFont = 'system'; - const availableWeights = getAvailableFontWeights(defaultFont); + const defaultFont = 'system' + const availableWeights = getAvailableFontWeights(defaultFont) addTextOverlay({ text: newText.trim(), position: { x: 50, y: 50 }, @@ -55,10 +46,10 @@ export const TextOverlayControls = () => { offsetX: 2, offsetY: 2, }, - }); - setNewText(''); + }) + setNewText('') } - }; + } const handleUpdatePosition = (axis: 'x' | 'y', value: number[]) => { if (selectedOverlay) { @@ -67,73 +58,73 @@ export const TextOverlayControls = () => { ...selectedOverlay.position, [axis]: value[0], }, - }); + }) } - }; + } const handleUpdateFontSize = (value: number[]) => { if (selectedOverlay) { updateTextOverlay(selectedOverlay.id, { fontSize: value[0], - }); + }) } - }; + } const handleUpdateOpacity = (value: number[]) => { if (selectedOverlay) { updateTextOverlay(selectedOverlay.id, { opacity: value[0], - }); + }) } - }; + } const handleUpdateText = (text: string) => { if (selectedOverlay) { - updateTextOverlay(selectedOverlay.id, { text }); + updateTextOverlay(selectedOverlay.id, { text }) } - }; + } const handleUpdateColor = (color: string) => { if (selectedOverlay) { - updateTextOverlay(selectedOverlay.id, { color }); + updateTextOverlay(selectedOverlay.id, { color }) } - }; + } const handleUpdateFontWeight = (weight: string) => { if (selectedOverlay) { - updateTextOverlay(selectedOverlay.id, { fontWeight: weight }); + updateTextOverlay(selectedOverlay.id, { fontWeight: weight }) } - }; + } const handleUpdateFontFamily = (fontFamily: string) => { if (selectedOverlay) { - const availableWeights = getAvailableFontWeights(fontFamily); - const currentWeight = selectedOverlay.fontWeight; + const availableWeights = getAvailableFontWeights(fontFamily) + const currentWeight = selectedOverlay.fontWeight // If the current weight is not available for the new font, default to the first available weight const newWeight = availableWeights.includes(currentWeight) ? currentWeight - : availableWeights[0] || 'normal'; + : availableWeights[0] || 'normal' updateTextOverlay(selectedOverlay.id, { fontFamily, fontWeight: newWeight, - }); + }) } - }; + } const handleUpdateOrientation = (orientation: 'horizontal' | 'vertical') => { if (selectedOverlay) { - updateTextOverlay(selectedOverlay.id, { orientation }); + updateTextOverlay(selectedOverlay.id, { orientation }) } - }; + } const handleUpdateTextShadow = ( updates: Partial<{ - enabled: boolean; - color: string; - blur: number; - offsetX: number; - offsetY: number; + enabled: boolean + color: string + blur: number + offsetX: number + offsetY: number }> ) => { if (selectedOverlay) { @@ -142,16 +133,16 @@ export const TextOverlayControls = () => { ...selectedOverlay.textShadow, ...updates, }, - }); + }) } - }; + } const handleToggleVisibility = (id: string) => { - const overlay = textOverlays.find((o) => o.id === id); + const overlay = textOverlays.find((o) => o.id === id) if (overlay) { - updateTextOverlay(id, { isVisible: !overlay.isVisible }); + updateTextOverlay(id, { isVisible: !overlay.isVisible }) } - }; + } return (
@@ -180,9 +171,7 @@ export const TextOverlayControls = () => { {textOverlays.length > 0 && (
-

- Manage Overlays -

+

Manage Overlays

{textOverlays.map((overlay) => (
{ variant="ghost" size="sm" onClick={(e) => { - e.stopPropagation(); - handleToggleVisibility(overlay.id); + e.stopPropagation() + handleToggleVisibility(overlay.id) }} className="h-6 w-6 p-0" > - {overlay.isVisible ? ( - - ) : ( - - )} + {overlay.isVisible ? : } {overlay.text}
- - - - {fontFamilies.map((font) => ( - - {font.name} - - ))} - - -
- - +
-

- {getAvailableFontWeights(selectedOverlay.fontFamily).length}{' '} - weight - {getAvailableFontWeights(selectedOverlay.fontFamily).length !== 1 - ? 's' - : ''}{' '} - available -

+ + +

+ {getAvailableFontWeights(selectedOverlay.fontFamily).length} weight + {getAvailableFontWeights(selectedOverlay.fontFamily).length !== 1 ? 's' : ''} available +

- - -
- Font Size -
- - {selectedOverlay.fontSize}px -
+ + +
+ Font Size +
+ + + {selectedOverlay.fontSize}px +
+
-
- Opacity -
- - {Math.round(selectedOverlay.opacity * 100)}% -
+
+ Opacity +
+ + + {Math.round(selectedOverlay.opacity * 100)}% +
+
- {/* Text Shadow Controls */} -
-
-

- Text Shadow -

- -
+ {/* Text Shadow Controls */} +
+
+

Text Shadow

+ +
- {selectedOverlay.textShadow.enabled && ( -
- {/* Shadow Color */} -
+ {selectedOverlay.textShadow.enabled && ( +
+ {/* Shadow Color */} +
+ handleUpdateTextShadow({ color: e.target.value })} + className="w-12 h-10 p-1 rounded-lg border border-border" + /> + - handleUpdateTextShadow({ color: e.target.value }) - } - className="w-12 h-10 p-1 rounded-lg border border-border" + onChange={(e) => handleUpdateTextShadow({ color: e.target.value })} + className="border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0" /> - - - handleUpdateTextShadow({ color: e.target.value }) - } - className="border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0" - /> - -
+ +
- {/* Shadow Blur */} -
- Blur -
- - handleUpdateTextShadow({ blur: value[0] }) - } - max={20} - min={0} - step={1} - /> - {selectedOverlay.textShadow.blur}px -
+ {/* Shadow Blur */} +
+ + Blur + +
+ handleUpdateTextShadow({ blur: value[0] })} + max={20} + min={0} + step={1} + /> + + {selectedOverlay.textShadow.blur}px +
+
- {/* Shadow Offset X */} -
- Offset X -
- - handleUpdateTextShadow({ offsetX: value[0] }) - } - max={20} - min={-20} - step={1} - /> - {selectedOverlay.textShadow.offsetX}px -
+ {/* Shadow Offset X */} +
+ + Offset X + +
+ handleUpdateTextShadow({ offsetX: value[0] })} + max={20} + min={-20} + step={1} + /> + + {selectedOverlay.textShadow.offsetX}px +
+
- {/* Shadow Offset Y */} -
- Offset Y -
- - handleUpdateTextShadow({ offsetY: value[0] }) - } - max={20} - min={-20} - step={1} - /> - {selectedOverlay.textShadow.offsetY}px -
+ {/* Shadow Offset Y */} +
+ + Offset Y + +
+ handleUpdateTextShadow({ offsetY: value[0] })} + max={20} + min={-20} + step={1} + /> + + {selectedOverlay.textShadow.offsetY}px +
- )} -
+
+ )} +
-
-

- Position -

- {/* X position */} -
- X Position -
- handleUpdatePosition('x', value)} - max={100} - min={0} - step={1} - /> - {Math.round(selectedOverlay.position.x)}% -
+
+

Position

+ {/* X position */} +
+ + X Position + +
+ handleUpdatePosition('x', value)} + max={100} + min={0} + step={1} + /> + + {Math.round(selectedOverlay.position.x)}% +
+
- {/* Y position */} -
- Y Position -
- handleUpdatePosition('y', value)} - max={100} - min={0} - step={1} - /> - {Math.round(selectedOverlay.position.y)}% -
+ {/* Y position */} +
+ + Y Position + +
+ handleUpdatePosition('y', value)} + max={100} + min={0} + step={1} + /> + + {Math.round(selectedOverlay.position.y)}% +
+
)}
- ); -}; - + ) +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx index fea92b8..297ba4f 100644 --- a/components/ui/accordion.tsx +++ b/components/ui/accordion.tsx @@ -1,14 +1,12 @@ -"use client" +'use client' -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { FaChevronDown } from "react-icons/fa" +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { FaChevronDown } from 'react-icons/fa' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Accordion({ - ...props -}: React.ComponentProps) { +function Accordion({ ...props }: React.ComponentProps) { return } @@ -19,7 +17,7 @@ function AccordionItem({ return ( ) @@ -35,7 +33,7 @@ function AccordionTrigger({ svg]:rotate-180", + 'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180', className )} {...props} @@ -58,7 +56,7 @@ function AccordionContent({ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" {...props} > -
{children}
+
{children}
) } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx index 0863e40..d98a1de 100644 --- a/components/ui/alert-dialog.tsx +++ b/components/ui/alert-dialog.tsx @@ -1,31 +1,23 @@ -"use client" +'use client' -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' -function AlertDialog({ - ...props -}: React.ComponentProps) { +function AlertDialog({ ...props }: React.ComponentProps) { return } function AlertDialogTrigger({ ...props }: React.ComponentProps) { - return ( - - ) + return } -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return } function AlertDialogOverlay({ @@ -36,7 +28,7 @@ function AlertDialogOverlay({ ) { +function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { +function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) @@ -99,7 +82,7 @@ function AlertDialogTitle({ return ( ) @@ -112,7 +95,7 @@ function AlertDialogDescription({ return ( ) @@ -122,12 +105,7 @@ function AlertDialogAction({ className, ...props }: React.ComponentProps) { - return ( - - ) + return } function AlertDialogCancel({ @@ -136,7 +114,7 @@ function AlertDialogCancel({ }: React.ComponentProps) { return ( ) diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx index 71e428b..be68eb0 100644 --- a/components/ui/avatar.tsx +++ b/components/ui/avatar.tsx @@ -1,34 +1,25 @@ -"use client" +'use client' -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Avatar({ - className, - ...props -}: React.ComponentProps) { +function Avatar({ className, ...props }: React.ComponentProps) { return ( ) } -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { +function AvatarImage({ className, ...props }: React.ComponentProps) { return ( ) @@ -41,10 +32,7 @@ function AvatarFallback({ return ( ) diff --git a/components/ui/button.tsx b/components/ui/button.tsx index ac85dc2..2f759ab 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,39 +1,40 @@ -"use client" +'use client' -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 ease-in-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 shadow-sm hover:shadow-md", + default: + 'bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 shadow-sm hover:shadow-md', destructive: - "bg-destructive text-white hover:bg-destructive/90 active:bg-destructive/80 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 shadow-sm hover:shadow-md", + 'bg-destructive text-white hover:bg-destructive/90 active:bg-destructive/80 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 shadow-sm hover:shadow-md', outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:bg-input/30 dark:border-input dark:hover:bg-input/50 hover:shadow-sm", + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:bg-input/30 dark:border-input dark:hover:bg-input/50 hover:shadow-sm', secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70 shadow-sm hover:shadow-md", + 'bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70 shadow-sm hover:shadow-md', ghost: - "hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:hover:bg-accent/50 active:scale-[0.98]", - link: "text-primary underline-offset-4 hover:underline", + 'hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:hover:bg-accent/50 active:scale-[0.98]', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, } ) @@ -44,11 +45,11 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( ) { +function Card({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ) } -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ) } -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function CardAction({ className, ...props }: React.ComponentProps<"div">) { +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return
} -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 015d5e6..fae1ab0 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,32 +1,24 @@ -"use client" +'use client' -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { FaTimes } from "react-icons/fa" +import * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { FaTimes } from 'react-icons/fa' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Dialog({ - ...props -}: React.ComponentProps) { +function Dialog({ ...props }: React.ComponentProps) { return } -function DialogTrigger({ - ...props -}: React.ComponentProps) { +function DialogTrigger({ ...props }: React.ComponentProps) { return } -function DialogPortal({ - ...props -}: React.ComponentProps) { +function DialogPortal({ ...props }: React.ComponentProps) { return } -function DialogClose({ - ...props -}: React.ComponentProps) { +function DialogClose({ ...props }: React.ComponentProps) { return } @@ -38,7 +30,7 @@ function DialogOverlay({ ) { +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { +function DialogTitle({ className, ...props }: React.ComponentProps) { return ( ) @@ -123,7 +109,7 @@ function DialogDescription({ return ( ) diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index 197c39a..280fb0a 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -1,34 +1,25 @@ -"use client" +'use client' -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { FaCheck, FaChevronRight, FaCircle } from "react-icons/fa" +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { FaCheck, FaChevronRight, FaCircle } from 'react-icons/fa' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function DropdownMenu({ - ...props -}: React.ComponentProps) { +function DropdownMenu({ ...props }: React.ComponentProps) { return } function DropdownMenuPortal({ ...props }: React.ComponentProps) { - return ( - - ) + return } function DropdownMenuTrigger({ ...props }: React.ComponentProps) { - return ( - - ) + return } function DropdownMenuContent({ @@ -42,7 +33,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - "bg-popover/95 backdrop-blur-xl border-border/50 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border p-1 shadow-md", + 'bg-popover/95 backdrop-blur-xl border-border/50 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border p-1 shadow-md', className )} {...props} @@ -51,22 +42,18 @@ function DropdownMenuContent({ ) } -function DropdownMenuGroup({ - ...props -}: React.ComponentProps) { - return ( - - ) +function DropdownMenuGroup({ ...props }: React.ComponentProps) { + return } function DropdownMenuItem({ className, inset, - variant = "default", + variant = 'default', ...props }: React.ComponentProps & { inset?: boolean - variant?: "default" | "destructive" + variant?: 'default' | 'destructive' }) { return ( ) { - return ( - - ) + return } function DropdownMenuRadioItem({ @@ -154,10 +136,7 @@ function DropdownMenuLabel({ ) @@ -170,31 +149,23 @@ function DropdownMenuSeparator({ return ( ) } -function DropdownMenuShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { return ( ) } -function DropdownMenuSub({ - ...props -}: React.ComponentProps) { +function DropdownMenuSub({ ...props }: React.ComponentProps) { return } @@ -230,7 +201,7 @@ function DropdownMenuSubContent({ { +interface GlassInputWrapperProps extends React.ComponentProps<'div'> { /** * Whether to apply the glass morphism effect * @default true @@ -11,39 +11,43 @@ interface GlassInputWrapperProps extends React.ComponentProps<"div"> { * Border intensity - controls the opacity of the border * @default "default" - moderate intensity */ - intensity?: "subtle" | "default" | "strong" + intensity?: 'subtle' | 'default' | 'strong' } /** * Glass morphism wrapper component for input fields * Provides a reusable glass morphism border effect with backdrop blur */ -function GlassInputWrapper({ - className, +function GlassInputWrapper({ + className, enabled = true, - intensity = "default", + intensity = 'default', children, - ...props + ...props }: GlassInputWrapperProps) { if (!enabled) { - return
{children}
+ return ( +
+ {children} +
+ ) } const intensityClasses = { subtle: { - border: "border-border/20", - bg: "bg-background/5", - glow: "from-foreground/8" + border: 'border-border/20', + bg: 'bg-background/5', + glow: 'from-foreground/8', }, default: { - border: "border-border/30", - bg: "bg-background/8", - glow: "from-foreground/12" + border: 'border-border/30', + bg: 'bg-background/8', + glow: 'from-foreground/12', }, strong: { - border: "border-border/40", - bg: "bg-background/12", - glow: "from-foreground/18" + border: 'border-border/40', + bg: 'bg-background/12', + glow: 'from-foreground/18', }, } @@ -53,42 +57,39 @@ function GlassInputWrapper({
{/* Inner glow effect */} -
{/* Content */} -
- {children} -
+
{children}
) } export { GlassInputWrapper } export type { GlassInputWrapperProps } - diff --git a/components/ui/hero-video-dialog.tsx b/components/ui/hero-video-dialog.tsx index 06b56ce..593e045 100644 --- a/components/ui/hero-video-dialog.tsx +++ b/components/ui/hero-video-dialog.tsx @@ -1,20 +1,20 @@ -"use client" +'use client' -import { useState } from "react" -import { Play, XIcon } from "lucide-react" -import { AnimatePresence, motion } from "motion/react" +import { useState } from 'react' +import { Play, XIcon } from 'lucide-react' +import { AnimatePresence, motion } from 'motion/react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' type AnimationStyle = - | "from-bottom" - | "from-center" - | "from-top" - | "from-left" - | "from-right" - | "fade" - | "top-in-bottom-out" - | "left-in-right-out" + | 'from-bottom' + | 'from-center' + | 'from-top' + | 'from-left' + | 'from-right' + | 'fade' + | 'top-in-bottom-out' + | 'left-in-right-out' interface HeroVideoProps { animationStyle?: AnimationStyle @@ -28,53 +28,53 @@ interface HeroVideoProps { } const animationVariants = { - "from-bottom": { - initial: { y: "100%", opacity: 0 }, + 'from-bottom': { + initial: { y: '100%', opacity: 0 }, animate: { y: 0, opacity: 1 }, - exit: { y: "100%", opacity: 0 }, + exit: { y: '100%', opacity: 0 }, }, - "from-center": { + 'from-center': { initial: { scale: 0.5, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.5, opacity: 0 }, }, - "from-top": { - initial: { y: "-100%", opacity: 0 }, + 'from-top': { + initial: { y: '-100%', opacity: 0 }, animate: { y: 0, opacity: 1 }, - exit: { y: "-100%", opacity: 0 }, + exit: { y: '-100%', opacity: 0 }, }, - "from-left": { - initial: { x: "-100%", opacity: 0 }, + 'from-left': { + initial: { x: '-100%', opacity: 0 }, animate: { x: 0, opacity: 1 }, - exit: { x: "-100%", opacity: 0 }, + exit: { x: '-100%', opacity: 0 }, }, - "from-right": { - initial: { x: "100%", opacity: 0 }, + 'from-right': { + initial: { x: '100%', opacity: 0 }, animate: { x: 0, opacity: 1 }, - exit: { x: "100%", opacity: 0 }, + exit: { x: '100%', opacity: 0 }, }, fade: { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, }, - "top-in-bottom-out": { - initial: { y: "-100%", opacity: 0 }, + 'top-in-bottom-out': { + initial: { y: '-100%', opacity: 0 }, animate: { y: 0, opacity: 1 }, - exit: { y: "100%", opacity: 0 }, + exit: { y: '100%', opacity: 0 }, }, - "left-in-right-out": { - initial: { x: "-100%", opacity: 0 }, + 'left-in-right-out': { + initial: { x: '-100%', opacity: 0 }, animate: { x: 0, opacity: 1 }, - exit: { x: "100%", opacity: 0 }, + exit: { x: '100%', opacity: 0 }, }, } export function HeroVideoDialog({ - animationStyle = "from-center", + animationStyle = 'from-center', videoSrc, thumbnailSrc, - thumbnailAlt = "Video thumbnail", + thumbnailAlt = 'Video thumbnail', className, open: controlledOpen, onOpenChange, @@ -91,7 +91,7 @@ export function HeroVideoDialog({ const selectedAnimation = animationVariants[animationStyle] return ( -
+
{showThumbnail && (
@@ -131,7 +131,7 @@ export function HeroVideoDialog({ role="button" tabIndex={0} onKeyDown={(e) => { - if (e.key === "Escape" || e.key === "Enter" || e.key === " ") { + if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { setIsVideoOpen(false) } }} @@ -141,7 +141,7 @@ export function HeroVideoDialog({ > diff --git a/components/ui/input.tsx b/components/ui/input.tsx index 8916905..af2b4a2 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -1,16 +1,16 @@ -import * as React from "react" +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Input({ className, type, ...props }: React.ComponentProps<"input">) { +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { return ( ) { +function Label({ className, ...props }: React.ComponentProps) { return ( -
+
- ); + ) } export const MovingBorder = ({ @@ -81,33 +75,27 @@ export const MovingBorder = ({ ry, ...otherProps }: { - children: React.ReactNode; - duration?: number; - rx?: string; - ry?: string; - [key: string]: any; + children: React.ReactNode + duration?: number + rx?: string + ry?: string + [key: string]: any }) => { - const pathRef = useRef(null); - const progress = useMotionValue(0); + const pathRef = useRef(null) + const progress = useMotionValue(0) useAnimationFrame((time) => { - const length = pathRef.current?.getTotalLength(); + const length = pathRef.current?.getTotalLength() if (length) { - const pxPerMillisecond = length / duration; - progress.set((time * pxPerMillisecond) % length); + const pxPerMillisecond = length / duration + progress.set((time * pxPerMillisecond) % length) } - }); + }) - const x = useTransform( - progress, - (val) => pathRef.current?.getPointAtLength(val).x, - ); - const y = useTransform( - progress, - (val) => pathRef.current?.getPointAtLength(val).y, - ); + const x = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).x) + const y = useTransform(progress, (val) => pathRef.current?.getPointAtLength(val).y) - const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`; + const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)` return ( <> @@ -119,26 +107,19 @@ export const MovingBorder = ({ height="100%" {...otherProps} > - + {children} - ); -}; + ) +} diff --git a/components/ui/optimized-image.tsx b/components/ui/optimized-image.tsx index 7716db8..6ea8f43 100644 --- a/components/ui/optimized-image.tsx +++ b/components/ui/optimized-image.tsx @@ -1,20 +1,20 @@ -"use client"; +'use client' -import { CldImage } from 'next-cloudinary'; +import { CldImage } from 'next-cloudinary' interface OptimizedImageProps { - src: string; - alt: string; - width?: number; - height?: number; - fill?: boolean; - className?: string; - priority?: boolean; - sizes?: string; - quality?: number | 'auto'; - crop?: 'fill' | 'fit' | 'scale' | 'crop'; - gravity?: 'auto' | 'center' | 'face'; - [key: string]: any; + src: string + alt: string + width?: number + height?: number + fill?: boolean + className?: string + priority?: boolean + sizes?: string + quality?: number | 'auto' + crop?: 'fill' | 'fit' | 'scale' | 'crop' + gravity?: 'auto' | 'center' | 'face' + [key: string]: any } /** @@ -36,11 +36,13 @@ export function OptimizedImage({ gravity = 'auto', ...props }: OptimizedImageProps) { - const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME; - + const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME + if (!cloudName) { - console.error('NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME is not set. All images require Cloudinary configuration.'); - return null; + console.error( + 'NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME is not set. All images require Cloudinary configuration.' + ) + return null } // Always use Cloudinary - src should be a Cloudinary public ID @@ -59,7 +61,7 @@ export function OptimizedImage({ unoptimized={false} {...props} /> - ); + ) } return ( @@ -77,6 +79,5 @@ export function OptimizedImage({ unoptimized={false} {...props} /> - ); + ) } - diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx index 73228b8..53ab867 100644 --- a/components/ui/popover.tsx +++ b/components/ui/popover.tsx @@ -1,25 +1,21 @@ -"use client" +'use client' -import * as React from "react" -import * as PopoverPrimitive from "@radix-ui/react-popover" +import * as React from 'react' +import * as PopoverPrimitive from '@radix-ui/react-popover' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Popover({ - ...props -}: React.ComponentProps) { +function Popover({ ...props }: React.ComponentProps) { return } -function PopoverTrigger({ - ...props -}: React.ComponentProps) { +function PopoverTrigger({ ...props }: React.ComponentProps) { return } function PopoverContent({ className, - align = "center", + align = 'center', sideOffset = 4, ...props }: React.ComponentProps) { @@ -30,7 +26,7 @@ function PopoverContent({ align={align} sideOffset={sideOffset} className={cn( - "bg-popover/95 backdrop-blur-xl border-border/50 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-lg border p-4 shadow-md outline-hidden", + 'bg-popover/95 backdrop-blur-xl border-border/50 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-lg border p-4 shadow-md outline-hidden', className )} {...props} @@ -39,9 +35,7 @@ function PopoverContent({ ) } -function PopoverAnchor({ - ...props -}: React.ComponentProps) { +function PopoverAnchor({ ...props }: React.ComponentProps) { return } diff --git a/components/ui/select.tsx b/components/ui/select.tsx index c7a1df4..1c2fabb 100644 --- a/components/ui/select.tsx +++ b/components/ui/select.tsx @@ -1,36 +1,30 @@ -"use client" +'use client' -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import * as React from 'react' +import * as SelectPrimitive from '@radix-ui/react-select' +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Select({ - ...props -}: React.ComponentProps) { +function Select({ ...props }: React.ComponentProps) { return } -function SelectGroup({ - ...props -}: React.ComponentProps) { +function SelectGroup({ ...props }: React.ComponentProps) { return } -function SelectValue({ - ...props -}: React.ComponentProps) { +function SelectValue({ ...props }: React.ComponentProps) { return } function SelectTrigger({ className, - size = "default", + size = 'default', children, ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: 'sm' | 'default' }) { return ( ) { return ( @@ -62,9 +56,9 @@ function SelectContent({ {children} @@ -87,14 +81,11 @@ function SelectContent({ ) } -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { +function SelectLabel({ className, ...props }: React.ComponentProps) { return ( ) @@ -131,7 +122,7 @@ function SelectSeparator({ return ( ) @@ -144,10 +135,7 @@ function SelectScrollUpButton({ return ( @@ -162,10 +150,7 @@ function SelectScrollDownButton({ return ( diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx index 275381c..db42857 100644 --- a/components/ui/separator.tsx +++ b/components/ui/separator.tsx @@ -1,13 +1,13 @@ -"use client" +'use client' -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from 'react' +import * as SeparatorPrimitive from '@radix-ui/react-separator' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' function Separator({ className, - orientation = "horizontal", + orientation = 'horizontal', decorative = true, ...props }: React.ComponentProps) { @@ -17,7 +17,7 @@ function Separator({ decorative={decorative} orientation={orientation} className={cn( - "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", + 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px', className )} {...props} diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx index 2f2f033..69fbc00 100644 --- a/components/ui/sheet.tsx +++ b/components/ui/sheet.tsx @@ -1,30 +1,24 @@ -"use client" +'use client' -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from 'react' +import * as SheetPrimitive from '@radix-ui/react-dialog' +import { XIcon } from 'lucide-react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' function Sheet({ ...props }: React.ComponentProps) { return } -function SheetTrigger({ - ...props -}: React.ComponentProps) { +function SheetTrigger({ ...props }: React.ComponentProps) { return } -function SheetClose({ - ...props -}: React.ComponentProps) { +function SheetClose({ ...props }: React.ComponentProps) { return } -function SheetPortal({ - ...props -}: React.ComponentProps) { +function SheetPortal({ ...props }: React.ComponentProps) { return } @@ -36,7 +30,7 @@ function SheetOverlay({ & { - side?: "top" | "right" | "bottom" | "left" + side?: 'top' | 'right' | 'bottom' | 'left' }) { return ( @@ -58,15 +52,15 @@ function SheetContent({ ) { +function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { +function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function SheetTitle({ - className, - ...props -}: React.ComponentProps) { +function SheetTitle({ className, ...props }: React.ComponentProps) { return ( ) @@ -121,7 +112,7 @@ function SheetDescription({ return ( ) diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index 5096b58..5c9897e 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -1,39 +1,34 @@ -"use client" - -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import { PanelLeftIcon } from "lucide-react" - -import { useIsMobile } from "@/hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" +'use client' + +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { PanelLeftIcon } from 'lucide-react' + +import { useIsMobile } from '@/hooks/use-mobile' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, -} from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" +} from '@/components/ui/sheet' +import { Skeleton } from '@/components/ui/skeleton' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_NAME = 'sidebar_state' const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "24rem" -const SIDEBAR_WIDTH_MOBILE = "20rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" +const SIDEBAR_WIDTH = '24rem' +const SIDEBAR_WIDTH_MOBILE = '20rem' +const SIDEBAR_WIDTH_ICON = '3rem' +const SIDEBAR_KEYBOARD_SHORTCUT = 'b' type SidebarContextProps = { - state: "expanded" | "collapsed" + state: 'expanded' | 'collapsed' open: boolean setOpen: (open: boolean) => void openMobile: boolean @@ -47,7 +42,7 @@ const SidebarContext = React.createContext(null) function useSidebar() { const context = React.useContext(SidebarContext) if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") + throw new Error('useSidebar must be used within a SidebarProvider.') } return context @@ -61,7 +56,7 @@ function SidebarProvider({ style, children, ...props -}: React.ComponentProps<"div"> & { +}: React.ComponentProps<'div'> & { defaultOpen?: boolean open?: boolean onOpenChange?: (open: boolean) => void @@ -75,7 +70,7 @@ function SidebarProvider({ const open = openProp ?? _open const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value + const openState = typeof value === 'function' ? value(open) : value if (setOpenProp) { setOpenProp(openState) } else { @@ -96,22 +91,19 @@ function SidebarProvider({ // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault() toggleSidebar() } } - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) }, [toggleSidebar]) // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" + const state = open ? 'expanded' : 'collapsed' const contextValue = React.useMemo( () => ({ @@ -133,13 +125,13 @@ function SidebarProvider({ data-slot="sidebar-wrapper" style={ { - "--sidebar-width": SIDEBAR_WIDTH, - "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, + '--sidebar-width': SIDEBAR_WIDTH, + '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, ...style, } as React.CSSProperties } className={cn( - "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", + 'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', className )} {...props} @@ -152,25 +144,25 @@ function SidebarProvider({ } function Sidebar({ - side = "left", - variant = "sidebar", - collapsible = "offcanvas", + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', className, children, ...props -}: React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" +}: React.ComponentProps<'div'> & { + side?: 'left' | 'right' + variant?: 'sidebar' | 'floating' | 'inset' + collapsible?: 'offcanvas' | 'icon' | 'none' }) { const { isMobile, state, openMobile, setOpenMobile } = useSidebar() - if (collapsible === "none") { + if (collapsible === 'none') { return (