diff --git a/CLAUDE.md b/CLAUDE.md index ada9025..e18f627 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,12 +11,29 @@ [x] Configure next/font [x] Set up TypeScript [x] initialize git repo with appropriate .gitignore -[ ] Create responsive layout with Tailwind CSS -[ ] Integrate localdb for backend photo index data -[ ] create service to index photos from local filesystem -[ ] Create photo gallery page +[x] Create responsive layout with Tailwind CSS +[x] Integrate localdb for backend photo index data +[x] create service to index photos from local filesystem +[x] Create photo gallery page [ ] Implement photo organization features (albums, tags, moving files) [ ] Optimize for performance and SEO -- I'll run dev myself \ No newline at end of file +# Claude Instructions +- Never automatically change or add files without user confirmation +- Never try to run the dev server, user will always manually run dev +- Run builds automatically to check for errors + +# Code Standards +- Files over 300 lines should be split into multiple files +- Use functional components with hooks +- Use Tailwind CSS for all styling, no custom CSS unless absolutely necessary +- Use next/image for all images +- Use next/font for all fonts +- Write clear, concise, and well-documented code +- Double check approach against current frameworks and libraries +- Always ask for user confirmation before making large changes + +# Audit +- When asked if there are libraries to accomblish custom functionality, check npm +- When asked for alternatives give multiple options with pros and cons \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7541176..bd7a126 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "next": "^15.5.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-share": "^5.2.2", "sharp": "^0.34.3" }, "devDependencies": { @@ -1258,6 +1259,12 @@ "node": ">=18" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1326,6 +1333,15 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1582,6 +1598,14 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "dependencies": { + "debug": "^2.1.3" + } + }, "node_modules/lightningcss": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", @@ -1920,6 +1944,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2207,6 +2237,19 @@ "react": "^19.1.1" } }, + "node_modules/react-share": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/react-share/-/react-share-5.2.2.tgz", + "integrity": "sha512-z0nbOX6X6vHHWAvXduNkYeJUKTKNpKM5Xpmc5a2BxjJhUWl+sE7AsSEMmYEUj2DuDjZr5m7KFIGF0sQPKcUN6w==", + "license": "MIT", + "dependencies": { + "classnames": "^2.3.2", + "jsonp": "^0.2.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index 587adbc..ac720b4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "next": "^15.5.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-share": "^5.2.2", "sharp": "^0.34.3" }, "devDependencies": { diff --git a/src/components/ImageModal.tsx b/src/components/ImageModal.tsx index 78ff839..e6cbec3 100644 --- a/src/components/ImageModal.tsx +++ b/src/components/ImageModal.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { createPortal } from 'react-dom' -import { IconX, IconZoomIn, IconZoomOut, IconMaximize, IconRotate, IconChevronLeft, IconChevronRight } from '@tabler/icons-react' +import { IconX, IconZoomIn, IconZoomOut, IconMaximize, IconRotate, IconChevronLeft, IconChevronRight, IconShare } from '@tabler/icons-react' import { Photo } from '@/types/photo' interface ImageModalProps { @@ -242,6 +242,91 @@ export default function ImageModal({ photo, isOpen, onClose, photos = [], onNavi const metaInfo = formatMetadata() + // Extract location information for social sharing + const getLocationInfo = () => { + let metadata: any = {} + try { + metadata = photo.metadata ? JSON.parse(photo.metadata) : {} + } catch (error) { + return null + } + + const exif = metadata.exif || {} + if (exif.gps && (exif.gps.latitude || exif.gps.longitude)) { + return { + latitude: exif.gps.latitude, + longitude: exif.gps.longitude, + locationString: `📍 ${exif.gps.latitude?.toFixed(4)}, ${exif.gps.longitude?.toFixed(4)}` + } + } + return null + } + + const locationInfo = getLocationInfo() + + // Build enhanced share quote with location + const buildShareQuote = () => { + const baseQuote = `Check out this photo: ${photo.filename}` + + if (locationInfo) { + return `${baseQuote} ${locationInfo.locationString}` + } + + if (metaInfo?.camera) { + return `${baseQuote} - Shot with ${metaInfo.camera}` + } + + return baseQuote + } + + // Native sharing with actual file + const handleNativeShare = async () => { + try { + // Check if Web Share API is supported + if (!navigator.share) { + // Fallback to copying URL to clipboard + await navigator.clipboard.writeText(`${window.location.origin}/api/photos/${photo.id}/full`) + alert('Photo URL copied to clipboard!') + return + } + + // Fetch the image as a blob + const response = await fetch(`/api/photos/${photo.id}/full`) + if (!response.ok) throw new Error('Failed to fetch image') + + const blob = await response.blob() + const file = new File([blob], photo.filename, { type: blob.type }) + + // Check if file sharing is supported + if (navigator.canShare && !navigator.canShare({ files: [file] })) { + // Fallback to sharing just text and URL + await navigator.share({ + title: photo.filename, + text: buildShareQuote(), + url: `${window.location.origin}/api/photos/${photo.id}/full` + }) + } else { + // Share with the actual file + await navigator.share({ + title: photo.filename, + text: buildShareQuote(), + files: [file] + }) + } + } catch (error) { + console.error('Sharing failed:', error) + + // Ultimate fallback - copy to clipboard + try { + await navigator.clipboard.writeText(`${window.location.origin}/api/photos/${photo.id}/full`) + alert('Sharing failed, but photo URL copied to clipboard!') + } catch (clipboardError) { + console.error('Clipboard access failed:', clipboardError) + alert('Sharing not supported on this device') + } + } + } + const modalContent = (
+ {/* Native Share Button */} + {mounted && ( + + )} +