├── types └── formidable.d.ts ├── .eslintignore ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-48x48.png ├── favicon-64x64.png ├── apple-touch-icon.png ├── test-bookshelf.jpg └── favicon.svg ├── api ├── jsconfig.json ├── .eslintrc.js ├── index.js ├── health-check.js └── preferences.js ├── postcss.config.js ├── .vercelignore ├── client ├── src │ ├── main.tsx │ ├── components │ │ ├── ui │ │ │ ├── aspect-ratio.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── textarea.tsx │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ ├── separator.tsx │ │ │ ├── progress.tsx │ │ │ ├── toaster.tsx │ │ │ ├── affiliate-disclosure.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── slider.tsx │ │ │ ├── switch.tsx │ │ │ ├── badge.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── popover.tsx │ │ │ ├── avatar.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── toggle.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── alert.tsx │ │ │ ├── resizable.tsx │ │ │ ├── ThemeToggle.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── tabs.tsx │ │ │ ├── star-rating.tsx │ │ │ ├── button.tsx │ │ │ ├── DonationButton.tsx │ │ │ ├── card.tsx │ │ │ ├── accordion.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── calendar.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── pagination.tsx │ │ │ ├── table.tsx │ │ │ ├── drawer.tsx │ │ │ ├── DonationModal.tsx │ │ │ ├── dialog.tsx │ │ │ ├── sheet.tsx │ │ │ ├── form.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── toast.tsx │ │ │ ├── command.tsx │ │ │ └── navigation-menu.tsx │ │ └── admin │ │ │ └── LoginForm.tsx │ ├── lib │ │ ├── utils.ts │ │ ├── queryClient.ts │ │ └── deviceId.ts │ ├── hooks │ │ ├── use-mobile.tsx │ │ └── use-toast.ts │ ├── pages │ │ ├── not-found.tsx │ │ ├── admin │ │ │ └── StatsPage.tsx │ │ └── debug.tsx │ ├── contexts │ │ ├── DeviceContext.tsx │ │ └── ThemeContext.tsx │ ├── index.css │ └── App.tsx └── index.html ├── vercel.json ├── server ├── env-routes.ts ├── simple-logger.ts ├── api-stats.ts ├── utils │ ├── match-quality-utils.ts │ └── book-utils.ts ├── db.ts ├── middleware │ └── deviceId.ts ├── simple-error-logger.ts ├── vite.ts ├── index.ts ├── openai-books.ts └── vision.ts ├── tests ├── setup │ ├── server.setup.ts │ └── client.setup.ts ├── server │ ├── simple.test.ts │ └── utils │ │ └── book-utils.test.ts ├── e2e │ └── simple.spec.ts └── client │ ├── utils │ └── math.test.ts │ └── lib │ ├── deviceId.test.ts │ └── utils.test.ts ├── migrations └── development │ ├── meta │ └── _journal.json │ ├── relations.ts │ ├── 0000_shiny_smiling_tiger.sql │ └── schema.ts ├── jest.config.server.mjs ├── .env.example ├── components.json ├── vitest.config.client.ts ├── .gitignore ├── drizzle.config.ts ├── .github └── workflows │ └── test.yml ├── tsconfig.json ├── vite.config.ts ├── playwright.config.ts ├── .eslintrc.json ├── scripts ├── setup-schemas.js └── security-check.cjs ├── LICENSE ├── tailwind.config.ts └── eslint.config.js /types/formidable.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'formidable'; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | build/ 4 | .vercel/ 5 | .next/ 6 | api/.eslintrc.js -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/favicon-48x48.png -------------------------------------------------------------------------------- /public/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/favicon-64x64.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/test-bookshelf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarinaWyss/ShelfScanner/HEAD/public/test-bookshelf.jpg -------------------------------------------------------------------------------- /api/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6" 5 | } 6 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | dist/index.js 4 | tests/ 5 | *.test.ts 6 | *.test.js 7 | .env.local 8 | .env.development 9 | *.log 10 | README.md -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App"; 3 | import "./index.css"; 4 | 5 | createRoot(document.getElementById("root")!).render(); 6 | -------------------------------------------------------------------------------- /client/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } 6 | -------------------------------------------------------------------------------- /client/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "npm run build", 3 | "outputDirectory": "dist/public", 4 | "rewrites": [ 5 | { 6 | "source": "/((?!api/.*).*)", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /server/env-routes.ts: -------------------------------------------------------------------------------- 1 | import { Express } from 'express'; 2 | 3 | /** 4 | * Register environment variable routes to securely expose 5 | * necessary environment variables to the client 6 | */ 7 | export function registerEnvRoutes(_app: Express) { 8 | // No ad configuration needed 9 | } -------------------------------------------------------------------------------- /tests/setup/server.setup.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | // Set test environment 4 | process.env.NODE_ENV = 'test'; 5 | process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test_db'; 6 | process.env.OPENAI_API_KEY = 'test-api-key'; 7 | process.env.GOOGLE_CLOUD_API_KEY = 'test-google-key'; -------------------------------------------------------------------------------- /migrations/development/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1749780243541, 9 | "tag": "0000_shiny_smiling_tiger", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /server/simple-logger.ts: -------------------------------------------------------------------------------- 1 | export function log(message: string, source = "server") { 2 | const formattedTime = new Date().toLocaleTimeString("en-US", { 3 | hour: "numeric", 4 | minute: "2-digit", 5 | second: "2-digit", 6 | hour12: true, 7 | }); 8 | 9 | console.log(`${formattedTime} [${source}] ${message}`); 10 | } -------------------------------------------------------------------------------- /client/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /tests/server/simple.test.ts: -------------------------------------------------------------------------------- 1 | describe('Simple Test', () => { 2 | it('should pass a basic test', () => { 3 | expect(2 + 2).toBe(4); 4 | }); 5 | 6 | it('should handle strings', () => { 7 | expect('hello').toBe('hello'); 8 | }); 9 | 10 | it('should handle arrays', () => { 11 | const arr = [1, 2, 3]; 12 | expect(arr).toHaveLength(3); 13 | expect(arr[0]).toBe(1); 14 | }); 15 | }); -------------------------------------------------------------------------------- /client/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /jest.config.server.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/tests/server'], 5 | testMatch: [ 6 | '**/tests/server/**/simple.test.ts' 7 | ], 8 | setupFilesAfterEnv: ['/tests/setup/server.setup.ts'], 9 | coverageDirectory: 'coverage/server', 10 | coverageReporters: ['text', 'lcov'], 11 | moduleFileExtensions: ['ts', 'js', 'json'], 12 | testTimeout: 10000, 13 | verbose: true 14 | }; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Database 2 | SUPABASE_ANON_KEY=anon_key 3 | SUPABASE_SERVICE_ROLE=service_role 4 | DATABASE_URL=supabase_url 5 | 6 | # Admin Access 7 | ADMIN_USERNAME=your_admin_username 8 | ADMIN_PASSWORD_HASH=your_hashed_password 9 | 10 | # API Keys 11 | OPENAI_API_KEY=your_openai_key_here 12 | GOOGLE_VISION_API_KEY=your_google_vision_key 13 | 14 | # Host information (for production) 15 | HOST=shelfscanner.io 16 | 17 | # Node Environment 18 | NODE_ENV=development 19 | -------------------------------------------------------------------------------- /api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | // eslint-disable-next-line no-undef 4 | module.exports = { 5 | env: { 6 | node: true, 7 | commonjs: true 8 | }, 9 | rules: { 10 | "no-undef": "off", 11 | "no-unused-vars": ["warn", { 12 | "argsIgnorePattern": "^_", 13 | "varsIgnorePattern": "^_" 14 | }], 15 | "@typescript-eslint/no-unused-vars": "off", 16 | "no-console": "off" 17 | }, 18 | parserOptions: { 19 | sourceType: "script" 20 | } 21 | }; -------------------------------------------------------------------------------- /tests/e2e/simple.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Simple E2E Tests', () => { 4 | test('should be able to visit google', async ({ page }) => { 5 | await page.goto('https://google.com'); 6 | await expect(page).toHaveTitle(/Google/); 7 | }); 8 | 9 | test('should handle basic interactions', async ({ page }) => { 10 | await page.goto('https://example.com'); 11 | await expect(page.getByText('Example Domain')).toBeVisible(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "client/src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /vitest.config.client.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | environment: 'jsdom', 9 | globals: true, 10 | setupFiles: ['./tests/setup/client.setup.ts'], 11 | include: [ 12 | 'tests/client/**/*.{test,spec}.{ts,tsx}' 13 | ], 14 | exclude: [ 15 | 'node_modules', 16 | 'dist', 17 | 'server', 18 | 'tests/server', 19 | 'tests/e2e' 20 | ] 21 | } 22 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | server/public 5 | vite.config.ts.* 6 | *.tar.gz 7 | 8 | # Development environment files 9 | .replit 10 | .config/ 11 | 12 | # Environment variables (if any get created) 13 | .env 14 | .env.local 15 | .env.production 16 | .env.development 17 | 18 | # Logs 19 | logs/ 20 | *.log 21 | 22 | # Editor files 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | 28 | # OS generated files 29 | Thumbs.db 30 | 31 | # Test artifacts 32 | /coverage/ 33 | /test-results/ 34 | /playwright-report/ 35 | /test-results-*/ 36 | *.log 37 | .env.test.localcoverage/ 38 | -------------------------------------------------------------------------------- /migrations/development/relations.ts: -------------------------------------------------------------------------------- 1 | import { relations } from "drizzle-orm/relations"; 2 | import { bookCacheInDevelopment, savedBooksInDevelopment } from "./schema"; 3 | 4 | export const savedBooksInDevelopmentRelations = relations(savedBooksInDevelopment, ({one}) => ({ 5 | bookCacheInDevelopment: one(bookCacheInDevelopment, { 6 | fields: [savedBooksInDevelopment.bookCacheId], 7 | references: [bookCacheInDevelopment.id] 8 | }), 9 | })); 10 | 11 | export const bookCacheInDevelopmentRelations = relations(bookCacheInDevelopment, ({many}) => ({ 12 | savedBooksInDevelopments: many(savedBooksInDevelopment), 13 | })); -------------------------------------------------------------------------------- /client/src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /server/api-stats.ts: -------------------------------------------------------------------------------- 1 | import { rateLimiter } from './rate-limiter'; 2 | 3 | /** 4 | * Returns the current status of API rate limits and usage 5 | * This is used for monitoring purposes 6 | */ 7 | export function getApiUsageStats(): Record { 8 | return { 9 | timestamp: new Date().toISOString(), 10 | stats: rateLimiter.getUsageStats(), 11 | // Include environment configuration status (without leaking actual API keys) 12 | config: { 13 | openaiEnabled: process.env.ENABLE_OPENAI !== 'false', 14 | openaiConfigured: !!process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY.length > 5, 15 | googleVisionConfigured: !!process.env.GOOGLE_VISION_API_KEY 16 | } 17 | }; 18 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import 'dotenv/config'; 3 | 4 | if (!process.env.DATABASE_URL) { 5 | throw new Error("DATABASE_URL must be set. Check your environment variables."); 6 | } 7 | 8 | // Determine schema based on environment 9 | const getSchema = () => { 10 | if (process.env.NODE_ENV === 'production') { 11 | return 'public'; 12 | } 13 | return 'development'; 14 | }; 15 | 16 | const currentSchema = getSchema(); 17 | 18 | export default defineConfig({ 19 | out: `./migrations/${currentSchema}`, 20 | schema: "./shared/schema.ts", 21 | dialect: "postgresql", 22 | dbCredentials: { 23 | url: process.env.DATABASE_URL, 24 | }, 25 | schemaFilter: [currentSchema], 26 | verbose: true, 27 | strict: true, 28 | }); 29 | -------------------------------------------------------------------------------- /client/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |