├── public ├── ads.txt ├── og │ └── default.png ├── images │ └── headshot.jpg ├── robots.txt ├── about.html ├── terms.html ├── contact.html ├── privacy-policy.html ├── jpg-to-excel.html ├── jpg-to-word.html ├── copy-text-from-image.html ├── extract-text-from-image.html ├── image-to-text.html └── sitemap.xml ├── specs ├── ai │ ├── evals │ │ └── goldens │ │ │ ├── sample1.json │ │ │ └── sample2.json │ └── prompt-contract.md ├── seo.spec.json ├── ads.spec.json ├── routes.spec.json ├── ux │ ├── tokens.json │ └── components │ │ └── uploader.contract.md ├── schemas │ └── ocr-result.schema.json ├── product │ └── ocr.feature └── api │ └── openapi.yml ├── src ├── config.ts ├── ads │ ├── shouldShowAds.ts │ ├── AutoAds.tsx │ └── InArticle.tsx ├── test │ ├── modal.inert.a11y.spec.tsx │ ├── utils.tsx │ ├── seo.head.spec.tsx │ ├── setupTests.ts │ ├── uploader.preview.a11y.spec.tsx │ ├── inarticle.integration.spec.tsx │ ├── a11y.spec.tsx │ ├── layout.accessibility.a11y.spec.tsx │ ├── deprecation.spec.tsx │ └── routes.spec.tsx ├── pages │ ├── About.tsx │ ├── Terms.tsx │ ├── PrivacyPolicy.tsx │ ├── Contact.tsx │ └── ImageToTextGuide.tsx ├── components │ └── TableWrap.tsx ├── structuredData.tsx ├── workers │ └── ocr.worker.ts ├── consent │ └── consent.ts ├── seo.tsx └── seo.spec.tsx ├── _deprecated └── App.tsx ├── postcss.config.js ├── .spectral.yml ├── .spectral.yaml ├── lib ├── motion.ts ├── version.ts └── env-guards.ts ├── vercel.json ├── vite-env.d.ts ├── components ├── icons │ ├── XCircleIcon.tsx │ ├── UploadIcon.tsx │ ├── DownloadIcon.tsx │ ├── MoonIcon.tsx │ ├── SunIcon.tsx │ ├── ButtonSpinnerIcon.tsx │ ├── ExclamationTriangleIcon.tsx │ └── CopyIcon.tsx ├── AdBlock.tsx ├── ui │ ├── PillarCard.tsx │ ├── Glass.tsx │ └── ScrollNav.tsx ├── ThemeToggle.tsx ├── FAQSchema.tsx ├── Spinner.tsx ├── a11y │ └── AnchorSection.tsx ├── AdGate.tsx ├── Toast.tsx ├── ResultDisplay.tsx ├── AdSlotLazy.tsx ├── AdSlot.tsx └── AuroraBackground.tsx ├── .gitignore ├── scripts ├── git-hooks │ ├── pre-push │ └── install.js ├── build-sitemap.mjs ├── validate-json.cjs └── check-legacy-shims.mjs ├── tsconfig.json ├── utils ├── env.ts ├── dateUtils.ts ├── fileUtils.ts ├── webVitals.ts └── fileValidation.ts ├── hooks ├── useObjectUrl.ts ├── useClipboard.ts ├── useTheme.ts ├── useDragDrop.ts ├── useLiveRegion.ts ├── useScrollSpy.ts ├── useWebVitals.ts ├── useFocusTrap.ts └── useLocalHistory.ts ├── docs ├── adr │ └── 0001-adopt-sdd.md ├── tests.md └── README.md ├── vercel.json.example ├── jest.config.js ├── index.tsx ├── pages ├── CopyTextFromImage.tsx ├── ImageToExcel.tsx ├── ImageToTextConverter.tsx └── _STRUCTURE.md ├── .github └── workflows │ ├── spec.yml │ └── quality.yml ├── app └── jpg-to-excel │ └── page.tsx ├── jest.setup.ts ├── LICENSE ├── __tests__ ├── builder-utils.test.ts ├── components │ ├── AdGate.test.tsx │ └── AdSlotLazy.test.tsx ├── GlassResultCard.test.tsx ├── SkeletonLoader.test.tsx ├── lib │ └── monetization.test.ts ├── ResultToolbar.test.tsx ├── ProgressBar.test.tsx ├── useShortcuts.test.ts └── GlassDropzone.test.tsx ├── tailwind.config.js ├── package.json ├── PR_NOTES_ADSENSE_READINESS.md ├── router.tsx └── vite.config.ts /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-2964937995247458, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /public/og/default.png: -------------------------------------------------------------------------------- 1 | TODO: Add 1200x630 default OG image here. Reference path: /og/default.png -------------------------------------------------------------------------------- /specs/ai/evals/goldens/sample1.json: -------------------------------------------------------------------------------- 1 | { 2 | "text": "Hello World!", 3 | "confidence": 1.0, 4 | "regions": [] 5 | } 6 | -------------------------------------------------------------------------------- /public/images/headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParthibanRajasekaran/text-from-image/HEAD/public/images/headshot.jpg -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const CONTACT_EMAIL = 2 | import.meta.env.VITE_CONTACT_EMAIL ?? 'rajasekaran.parthiban7@gmail.com'; 3 | -------------------------------------------------------------------------------- /_deprecated/App.tsx: -------------------------------------------------------------------------------- 1 | // Deprecated: Legacy App component. See SOLID_AUDIT_REPORT.md for details. 2 | 3 | // ...existing code... 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // postcss.config.js 2 | export default { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /specs/ai/evals/goldens/sample2.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "sample2.png", 3 | "text": "Quick brown fox", 4 | "confidence": 1.0, 5 | "regions": [] 6 | } 7 | -------------------------------------------------------------------------------- /.spectral.yml: -------------------------------------------------------------------------------- 1 | extends: ["spectral:oas"] 2 | rules: 3 | info-contact: off 4 | info-license: off 5 | openapi-tags: off 6 | operation-operationId: off 7 | -------------------------------------------------------------------------------- /specs/seo.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "requiredTags": [ 3 | "title", 4 | "description", 5 | "canonical", 6 | "og:image", 7 | "twitter:card" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.spectral.yaml: -------------------------------------------------------------------------------- 1 | extends: ["spectral:oas", "spectral:recommended"] 2 | rules: 3 | info-contact: off 4 | info-license: off 5 | openapi-tags: off 6 | operation-operationId: off 7 | -------------------------------------------------------------------------------- /src/ads/shouldShowAds.ts: -------------------------------------------------------------------------------- 1 | // Utility: Only show ads on guide pages, not tool pages 2 | export function shouldShowAds(route: string) { 3 | return /^\/(image-to-text|extract-text-from-image|copy-text-from-image|jpg-to-word|jpg-to-excel)$/.test(route); 4 | } 5 | -------------------------------------------------------------------------------- /lib/motion.ts: -------------------------------------------------------------------------------- 1 | import { useReducedMotion } from "framer-motion"; 2 | 3 | export const motionTransition = { 4 | type: "spring" as const, 5 | stiffness: 320, 6 | damping: 30 7 | }; 8 | 9 | export const useSafeMotion = () => useReducedMotion(); 10 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "npm run build", 3 | "outputDirectory": "dist", 4 | "framework": null, 5 | "rewrites": [ 6 | { 7 | "source": "/(.*)", 8 | "destination": "/index.html" 9 | } 10 | ], 11 | "ignoreCommand": "node scripts/should-build.mjs" 12 | } 13 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # Allow all crawlers 2 | User-agent: * 3 | Allow: / 4 | 5 | # Sitemaps 6 | Sitemap: https://freetextfromimage.com/sitemap.xml 7 | 8 | # Crawl-delay for courtesy (optional) 9 | Crawl-delay: 1 10 | 11 | # Disallow admin or backend paths (if any in future) 12 | # Disallow: /admin/ 13 | -------------------------------------------------------------------------------- /specs/ai/prompt-contract.md: -------------------------------------------------------------------------------- 1 | # AI Prompt Contract for OCR 2 | 3 | - Prompt must describe the image and request text extraction 4 | - Must support multi-language images 5 | - Should return text and confidence 6 | - Example prompt: 7 | "Extract all readable text from the uploaded image. Return the text and a confidence score." 8 | -------------------------------------------------------------------------------- /specs/ads.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolRoutesNoAds": [ 3 | "/", 4 | "/image-to-text-converter", 5 | "/image-to-excel" 6 | ], 7 | "guideRoutesOneSlot": [ 8 | "/image-to-text", 9 | "/extract-text-from-image", 10 | "/copy-text-from-image", 11 | "/jpg-to-word", 12 | "/jpg-to-excel" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /specs/routes.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "mustExist": [ 3 | "/", 4 | "/image-to-text", 5 | "/extract-text-from-image", 6 | "/copy-text-from-image", 7 | "/jpg-to-word", 8 | "/jpg-to-excel", 9 | "/image-to-excel", 10 | "/privacy-policy", 11 | "/terms", 12 | "/about", 13 | "/contact" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/test/modal.inert.a11y.spec.tsx: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest'; 2 | // If you have a modal component, import it here 3 | // import { Modal } from '../../components/Modal'; 4 | 5 | describe.skip('Modal accessibility and inert background', () => { 6 | // TODO: Implement when modal/dialog exists in the codebase 7 | // See README for details 8 | }); 9 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_UX_V2?: string; 5 | readonly VITE_COMMIT?: string; 6 | readonly VITE_BUILD_TIME?: string; 7 | readonly VITE_VERCEL_GIT_COMMIT_SHA?: string; 8 | // Add other env variables here as needed 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv; 13 | } 14 | -------------------------------------------------------------------------------- /specs/ux/tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "light": { 3 | "background": "#ffffff", 4 | "foreground": "#222222", 5 | "primary": "#2563eb", 6 | "secondary": "#64748b", 7 | "accent": "#f59e42" 8 | }, 9 | "dark": { 10 | "background": "#18181b", 11 | "foreground": "#f4f4f5", 12 | "primary": "#60a5fa", 13 | "secondary": "#94a3b8", 14 | "accent": "#fbbf24" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/icons/XCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const XCircleIcon: React.FC> = (props) => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | .env.local 16 | .env.*.local 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | .vercel 29 | -------------------------------------------------------------------------------- /scripts/git-hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Pre-push hook: Run tests before push 3 | # This hook runs `npm run test` and aborts the push if tests fail 4 | 5 | set -e 6 | 7 | echo "🔍 Running tests before push…" 8 | npm run test 9 | status=$? 10 | 11 | if [ $status -ne 0 ]; then 12 | echo "❌ Tests failed. Push aborted." 13 | exit $status 14 | fi 15 | 16 | echo "✅ Tests passed. Proceeding with push." 17 | exit 0 18 | -------------------------------------------------------------------------------- /components/icons/UploadIcon.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | export const UploadIcon: React.FC> = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /components/icons/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | export const DownloadIcon: React.FC> = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/test/utils.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, RenderOptions } from '@testing-library/react'; 3 | 4 | // If you use Providers (Theme, Router, etc.), wrap here 5 | export function customRender(ui: React.ReactElement, options?: RenderOptions) { 6 | return render(ui, { ...options }); 7 | } 8 | 9 | export function createMockFile(type: string = 'image/jpeg', name: string = 'photo.jpg') { 10 | return new File([new Uint8Array([1,2,3])], name, { type }); 11 | } 12 | -------------------------------------------------------------------------------- /components/icons/MoonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const MoonIcon: React.FC> = (props) => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /components/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SunIcon: React.FC> = (props) => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /components/icons/ButtonSpinnerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ButtonSpinnerIcon: React.FC> = (props) => ( 4 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /components/icons/ExclamationTriangleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ExclamationTriangleIcon: React.FC> = (props) => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /specs/ux/components/uploader.contract.md: -------------------------------------------------------------------------------- 1 | # Uploader Component Contract 2 | 3 | ## Props 4 | - `onFileSelect: (File) => void` — called when a file is selected 5 | - `accept: string` — accepted file types 6 | - `label: string` — visible label and accessible name 7 | - `preview: boolean` — show image preview 8 | 9 | ## Accessibility 10 | - The uploader must have an accessible name equal to the visible label 11 | - The preview must be focusable and have alt text 12 | - There must be exactly one
landmark on the page 13 | - If a modal is present, background must use `inert` 14 | -------------------------------------------------------------------------------- /src/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import SEO from '../components/SEO'; 2 | import { StructuredData } from '../structuredData'; 3 | 4 | export default function About() { 5 | return ( 6 |
7 | 8 |

About

9 |

Free Text From Image is a web app that helps you extract text from images easily and securely...

10 | {/* ...more content... */} 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/TableWrap.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | interface TableWrapProps { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | 8 | /** 9 | * TableWrap - Responsive table wrapper with consistent styling 10 | * Provides overflow scroll on small screens and border styling 11 | */ 12 | export function TableWrap({ children, className = '' }: TableWrapProps) { 13 | return ( 14 |
15 | 16 | {children} 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/Terms.tsx: -------------------------------------------------------------------------------- 1 | import SEO from '../components/SEO'; 2 | import { StructuredData } from '../structuredData'; 3 | 4 | export default function Terms() { 5 | return ( 6 |
7 | 8 |

Terms of Service

9 |

These terms govern your use of Free Text From Image...

10 | {/* ...more content... */} 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": [ 8 | "ES2022", 9 | "DOM", 10 | "DOM.Iterable" 11 | ], 12 | "skipLibCheck": true, 13 | "types": [ 14 | "node" 15 | ], 16 | "moduleResolution": "bundler", 17 | "isolatedModules": true, 18 | "moduleDetection": "force", 19 | "allowJs": true, 20 | "jsx": "react-jsx", 21 | "paths": { 22 | "@/*": [ 23 | "./*" 24 | ] 25 | }, 26 | "allowImportingTsExtensions": true, 27 | "noEmit": true 28 | } 29 | } -------------------------------------------------------------------------------- /src/pages/PrivacyPolicy.tsx: -------------------------------------------------------------------------------- 1 | import SEO from '../components/SEO'; 2 | import { StructuredData } from '../structuredData'; 3 | 4 | export default function PrivacyPolicy() { 5 | return ( 6 |
7 | 8 |

Privacy Policy

9 |

Your privacy is important to us. This page explains how we handle your data...

10 | {/* ...more content... */} 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/Contact.tsx: -------------------------------------------------------------------------------- 1 | import SEO from '../components/SEO'; 2 | import { StructuredData } from '../structuredData'; 3 | import { CONTACT_EMAIL } from '../config'; 4 | 5 | export default function Contact() { 6 | return ( 7 |
8 | 9 |

Contact

10 |

For support or feedback, email us at {CONTACT_EMAIL}.

11 | {/* Or add a simple form here, no backend required */} 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | export const CopyIcon: React.FC> = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /utils/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment variable utilities for Vite 3 | * Vite exposes env vars via import.meta.env 4 | */ 5 | 6 | export const isUXV2Enabled = (): boolean => { 7 | // In Vite, env vars are accessed via import.meta.env 8 | // VITE_ prefix is required for client-side access 9 | return import.meta.env.VITE_UX_V2 === '1' || import.meta.env.VITE_UX_V2 === 'true'; 10 | }; 11 | 12 | export const isUXV3Enabled = (): boolean => { 13 | // V3 is the premium futuristic UI with aurora background + glass cards 14 | // Reuses VITE_UX_V2 flag for now (can be split later with VITE_UX_V3) 15 | return import.meta.env.VITE_UX_V2 === '1' || import.meta.env.VITE_UX_V2 === 'true'; 16 | }; 17 | 18 | export const getEnv = (key: string, defaultValue: string = ''): string => { 19 | return (import.meta.env[key] as string) || defaultValue; 20 | }; 21 | -------------------------------------------------------------------------------- /specs/schemas/ocr-result.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "OCRResult", 4 | "type": "object", 5 | "required": ["text", "confidence", "regions"], 6 | "properties": { 7 | "text": { "type": "string" }, 8 | "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, 9 | "regions": { 10 | "type": "array", 11 | "items": { 12 | "type": "object", 13 | "required": ["bbox", "text"], 14 | "properties": { 15 | "bbox": { 16 | "type": "array", 17 | "items": { "type": "number" }, 18 | "minItems": 4, 19 | "maxItems": 4 20 | }, 21 | "text": { "type": "string" }, 22 | "confidence": { "type": "number", "minimum": 0, "maximum": 1 } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About | Free Text From Image 5 | 6 | 7 | 8 | 9 |
10 |

About

11 |
12 |

Free Text From Image is a free web app for extracting text from images. Our mission is to make OCR accessible to everyone, with privacy and accuracy as top priorities.

13 |

Our Team

14 |

Built by engineers and designers passionate about open access to information.

15 |

Contact

16 |

Email rajasekaran.parthiban7@gmail.com for questions or feedback.

17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /components/AdBlock.tsx: -------------------------------------------------------------------------------- 1 | // Renders a responsive AdSense unit only after consent. 2 | // Reserve height to avoid CLS. 3 | import { useEffect } from "react"; 4 | 5 | export default function AdBlock({ 6 | slot, 7 | consent, 8 | minHeight = 320 9 | }: { slot: string; consent: boolean; minHeight?: number }) { 10 | useEffect(() => { 11 | if (!consent) return; 12 | (window as any).adsbygoogle = (window as any).adsbygoogle || []; 13 | (window as any).adsbygoogle.push({}); 14 | }, [consent]); 15 | 16 | if (!consent) return
; // reserve space; no layout shift 17 | 18 | return ( 19 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /hooks/useObjectUrl.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | 3 | /** 4 | * Hook to manage object URLs for File/Blob with automatic cleanup 5 | * Prevents memory leaks by revoking URLs on change or unmount 6 | * 7 | * @param file - File or Blob to create URL for 8 | * @returns object URL string or null 9 | * 10 | * @example 11 | * const imageUrl = useObjectUrl(selectedFile); 12 | * return Preview; 13 | */ 14 | export function useObjectUrl(file: File | Blob | null): string | null { 15 | // Create URL synchronously with useMemo 16 | const url = useMemo(() => { 17 | return file ? URL.createObjectURL(file) : null; 18 | }, [file]); 19 | 20 | // Cleanup on unmount or file change 21 | useEffect(() => { 22 | return () => { 23 | if (url) { 24 | URL.revokeObjectURL(url); 25 | } 26 | }; 27 | }, [url]); 28 | 29 | return url; 30 | } 31 | -------------------------------------------------------------------------------- /components/ui/PillarCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Glass } from './Glass'; 3 | 4 | export interface PillarCardProps { 5 | /** Icon component or JSX element */ 6 | icon: React.ReactNode; 7 | /** Card title */ 8 | title: string; 9 | /** Card description/body text */ 10 | body: string; 11 | /** Optional additional className for the Glass container */ 12 | className?: string; 13 | } 14 | 15 | /** 16 | * PillarCard: Icon + Title + Body inside Glass container 17 | * Used for value pillars and feature highlights 18 | */ 19 | export function PillarCard({ icon, title, body, className = '' }: PillarCardProps) { 20 | return ( 21 | 22 |
{icon}
23 |

{title}

24 |

{body}

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /public/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Terms of Service | Free Text From Image 5 | 6 | 7 | 8 | 9 |
10 |

Terms of Service

11 |
12 |

Acceptance of Terms

13 |

By using Free Text From Image, you agree to these terms.

14 |

Use of Service

15 |

This service is provided as-is, without warranty. Do not use for illegal purposes.

16 |

Intellectual Property

17 |

All content and code are the property of their respective owners.

18 |

Contact

19 |

Questions? Email rajasekaran.parthiban7@gmail.com.

20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SunIcon } from './icons/SunIcon'; 3 | import { MoonIcon } from './icons/MoonIcon'; 4 | 5 | type Theme = 'light' | 'dark'; 6 | 7 | interface ThemeToggleProps { 8 | theme: Theme; 9 | toggleTheme: () => void; 10 | } 11 | 12 | export const ThemeToggle: React.FC = ({ theme, toggleTheme }) => { 13 | return ( 14 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /docs/adr/0001-adopt-sdd.md: -------------------------------------------------------------------------------- 1 | # ADR 0001: Adopt Spec-Driven Development (SDD) 2 | 3 | ## Status 4 | Accepted 5 | 6 | ## Context 7 | To improve reliability, accessibility, and maintainability, we are migrating to Spec-Driven Development (SDD). This approach introduces product, API, schema, UX, and AI specs as first-class citizens, with CI gates to enforce contracts and BDD scenarios. 8 | 9 | ## Decision 10 | - Add `/specs` folder for product features, schemas, API, UX contracts, and AI goldens. 11 | - Wire Spectral, oasdiff, jsonschema-diff, and Playwright BDD into CI. 12 | - Require all PRs to pass spec lint, schema validation, contract diff, and BDD tests. 13 | - Document spec usage and PR checklist in README. 14 | 15 | ## Consequences 16 | - Any breaking change to API/schema/UX contracts must bump version and update tests. 17 | - BDD scenarios must pass for all features. 18 | - App behavior remains unchanged; new preview respects CLS and a11y rules. 19 | - Future changes violating specs or contracts will fail CI. 20 | -------------------------------------------------------------------------------- /hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export interface UseClipboardOptions { 4 | disabled?: boolean; 5 | onFiles: (files: File[]) => void; 6 | } 7 | 8 | export function useClipboard({ disabled = false, onFiles }: UseClipboardOptions) { 9 | useEffect(() => { 10 | const handlePaste = (e: ClipboardEvent) => { 11 | if (disabled) return; 12 | const items = e.clipboardData?.items; 13 | if (!items) return; 14 | const files: File[] = []; 15 | for (const item of Array.from(items)) { 16 | if (item.type.startsWith('image/')) { 17 | const file = item.getAsFile(); 18 | if (file) { 19 | files.push(file); 20 | e.preventDefault(); 21 | } 22 | } 23 | } 24 | if (files.length > 0) { 25 | onFiles(files); 26 | } 27 | }; 28 | window.addEventListener('paste', handlePaste); 29 | return () => window.removeEventListener('paste', handlePaste); 30 | }, [disabled, onFiles]); 31 | } 32 | -------------------------------------------------------------------------------- /vercel.json.example: -------------------------------------------------------------------------------- 1 | { 2 | // Explicit build command ensures consistent builds 3 | // Docs: https://vercel.com/docs/build-step#build-command 4 | "buildCommand": "npm run build", 5 | 6 | // Output directory for Vite builds 7 | "outputDirectory": "dist", 8 | 9 | // Set to null to use custom Vite config 10 | // Docs: https://vercel.com/docs/frameworks 11 | "framework": null, 12 | 13 | // SPA fallback routing for client-side React Router 14 | // All routes fallback to index.html for client-side routing 15 | // Docs: https://vercel.com/docs/projects/project-configuration#rewrites 16 | "rewrites": [ 17 | { 18 | "source": "/(.*)", 19 | "destination": "/index.html" 20 | } 21 | ], 22 | 23 | // OPTIONAL: Disable auto-deploys for specific branches 24 | // Uncomment to prevent automatic deployments 25 | // Docs: https://vercel.com/docs/concepts/git#ignored-build-step 26 | // "git": { 27 | // "deploymentEnabled": { 28 | // "dev": false 29 | // } 30 | // } 31 | } 32 | -------------------------------------------------------------------------------- /components/ui/Glass.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface GlassProps extends React.HTMLAttributes { 4 | /** Additional CSS classes */ 5 | className?: string; 6 | /** Whether to include padding (default: true) */ 7 | withPadding?: boolean; 8 | children: React.ReactNode; 9 | } 10 | 11 | /** 12 | * Glass: Reusable glass-morphism container matching landing theme 13 | * - rounded-2xl border-white/10 bg-white/6 dark:bg-white/5 backdrop-blur-xl 14 | * - Subtle inner hairline + drop shadow 15 | * - Responsive padding optional 16 | */ 17 | export function Glass({ 18 | className = '', 19 | withPadding = true, 20 | children, 21 | ...props 22 | }: GlassProps) { 23 | const baseClasses = 24 | 'rounded-2xl border border-white/10 bg-white/6 dark:bg-white/5 backdrop-blur-xl shadow-lg'; 25 | const paddingClasses = withPadding ? 'p-6 md:p-8' : ''; 26 | 27 | return ( 28 |
29 | {children} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a timestamp into a human-readable relative time string 3 | * @param timestamp - Unix timestamp in milliseconds 4 | * @returns Formatted relative time string (e.g., "Just now", "5m ago", "2h ago", "3d ago", or date) 5 | */ 6 | export function formatTimestamp(timestamp: number): string { 7 | const date = new Date(timestamp); 8 | const now = new Date(); 9 | const diffMs = now.getTime() - date.getTime(); 10 | if (diffMs < 0) { 11 | // Timestamp is in the future 12 | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 13 | } 14 | const diffMins = Math.floor(diffMs / 60000); 15 | const diffHours = Math.floor(diffMs / 3600000); 16 | const diffDays = Math.floor(diffMs / 86400000); 17 | 18 | if (diffMins < 1) return 'Just now'; 19 | if (diffMins < 60) return `${diffMins}m ago`; 20 | if (diffHours < 24) return `${diffHours}h ago`; 21 | if (diffDays < 7) return `${diffDays}d ago`; 22 | 23 | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 24 | } 25 | -------------------------------------------------------------------------------- /public/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Contact | Free Text From Image 5 | 6 | 7 | 8 | 9 |
10 |

Contact

11 |
12 |

For support or feedback, email rajasekaran.parthiban7@gmail.com or use the form below.

13 |
14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /scripts/build-sitemap.mjs: -------------------------------------------------------------------------------- 1 | // Build sitemap.xml from route list 2 | import { writeFileSync } from 'fs'; 3 | 4 | // Use VITE_SITE_URL from env or fallback 5 | const siteUrl = process.env.VITE_SITE_URL || 'https://freetextfromimage.com'; 6 | 7 | // All public routes (guides + legal pages) 8 | const routes = [ 9 | '/', 10 | '/image-to-text', 11 | '/extract-text-from-image', 12 | '/copy-text-from-image', 13 | '/jpg-to-word', 14 | '/jpg-to-excel', 15 | '/image-to-excel', 16 | '/privacy-policy', 17 | '/terms', 18 | '/about', 19 | '/contact' 20 | ]; 21 | 22 | const xml = ` 23 | 24 | ${routes.map(r => ` 25 | ${siteUrl}${r} 26 | weekly 27 | ${r === '/' ? '1.0' : r.includes('privacy') || r.includes('terms') ? '0.3' : '0.8'} 28 | `).join('\n')} 29 | `; 30 | 31 | writeFileSync('./public/sitemap.xml', xml); 32 | console.log(`✓ Sitemap generated with ${routes.length} routes at ${siteUrl}`); 33 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | roots: [''], 6 | testMatch: ['**/__tests__/**/*.test.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 8 | setupFilesAfterEnv: ['/jest.setup.ts'], 9 | moduleNameMapper: { 10 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', 11 | '^@/(.*)$': '/$1', 12 | }, 13 | transform: { 14 | '^.+\\.tsx?$': [ 15 | 'ts-jest', 16 | { 17 | tsconfig: { 18 | jsx: 'react', 19 | esModuleInterop: true, 20 | allowSyntheticDefaultImports: true, 21 | }, 22 | }, 23 | ], 24 | }, 25 | collectCoverageFrom: [ 26 | 'components/**/*.{ts,tsx}', 27 | 'hooks/**/*.{ts,tsx}', 28 | 'utils/**/*.{ts,tsx}', 29 | '!**/*.d.ts', 30 | '!**/node_modules/**', 31 | ], 32 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 33 | coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], 34 | }; 35 | -------------------------------------------------------------------------------- /src/test/seo.head.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, it, expect } from 'vitest'; 3 | import { customRender } from './utils'; 4 | import { axe } from 'jest-axe'; 5 | import { SEO } from '../../components/SEO'; 6 | 7 | describe('SEO head tags', () => { 8 | it('sets title, description, canonical', async () => { 9 | customRender( 10 | 15 | ); 16 | expect(document.title).toBe('Test Title | TextFromImage'); 17 | const metaDesc = document.querySelector('meta[name="description"]'); 18 | expect(metaDesc).toBeTruthy(); 19 | expect(metaDesc?.getAttribute('content')).toBe('Test description for SEO.'); 20 | const canonical = document.querySelector('link[rel="canonical"]'); 21 | expect(canonical).toBeTruthy(); 22 | expect(canonical?.getAttribute('href')).toBe('https://freetextfromimage.com/test'); 23 | const results = await axe(document.head); 24 | expect(results.violations.length).toBe(0); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /components/FAQSchema.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface FAQItem { 4 | question: string; 5 | answer: string; 6 | } 7 | 8 | interface FAQSchemaProps { 9 | faqs: FAQItem[]; 10 | } 11 | 12 | /** 13 | * FAQPage JSON-LD Schema Component 14 | * 15 | * Generates structured data for SEO-rich FAQs 16 | * per schema.org/FAQPage spec 17 | * 18 | * @see https://schema.org/FAQPage 19 | * @see https://developers.google.com/search/docs/appearance/structured-data/faqpage 20 | */ 21 | export function FAQSchema({ faqs }: FAQSchemaProps) { 22 | if (!faqs || faqs.length === 0) { 23 | return null; 24 | } 25 | 26 | const schemaData = { 27 | '@context': 'https://schema.org', 28 | '@type': 'FAQPage', 29 | mainEntity: faqs.map((faq) => ({ 30 | '@type': 'Question', 31 | name: faq.question, 32 | acceptedAnswer: { 33 | '@type': 'Answer', 34 | text: faq.answer, 35 | }, 36 | })), 37 | }; 38 | 39 | return ( 40 |