Add native Web Share API for photo sharing

- Install react-share library and add native sharing to ImageModal
- Replace Facebook-specific sharing with Web Share API for broader compatibility
- Extract GPS location from EXIF data for enhanced share text
- Support file sharing with fallbacks to URL/clipboard for unsupported devices
- Add smart share text generation with location coordinates and camera info
- Perfect for private hosts - shares actual files without external URLs
- Update CLAUDE.md with completed roadmap items and coding standards

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-08-27 13:04:31 -05:00
parent 5c3ad988f5
commit 96e6f4676a
4 changed files with 163 additions and 6 deletions

View File

@ -11,12 +11,29 @@
[x] Configure next/font [x] Configure next/font
[x] Set up TypeScript [x] Set up TypeScript
[x] initialize git repo with appropriate .gitignore [x] initialize git repo with appropriate .gitignore
[ ] Create responsive layout with Tailwind CSS [x] Create responsive layout with Tailwind CSS
[ ] Integrate localdb for backend photo index data [x] Integrate localdb for backend photo index data
[ ] create service to index photos from local filesystem [x] create service to index photos from local filesystem
[ ] Create photo gallery page [x] Create photo gallery page
[ ] Implement photo organization features (albums, tags, moving files) [ ] Implement photo organization features (albums, tags, moving files)
[ ] Optimize for performance and SEO [ ] Optimize for performance and SEO
- I'll run dev myself # 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

43
package-lock.json generated
View File

@ -17,6 +17,7 @@
"next": "^15.5.0", "next": "^15.5.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-share": "^5.2.2",
"sharp": "^0.34.3" "sharp": "^0.34.3"
}, },
"devDependencies": { "devDependencies": {
@ -1258,6 +1259,12 @@
"node": ">=18" "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": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@ -1326,6 +1333,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/decompress-response": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@ -1582,6 +1598,14 @@
"jiti": "lib/jiti-cli.mjs" "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": { "node_modules/lightningcss": {
"version": "1.30.1", "version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@ -1920,6 +1944,12 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT" "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": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -2207,6 +2237,19 @@
"react": "^19.1.1" "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": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",

View File

@ -21,6 +21,7 @@
"next": "^15.5.0", "next": "^15.5.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-share": "^5.2.2",
"sharp": "^0.34.3" "sharp": "^0.34.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { createPortal } from 'react-dom' 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' import { Photo } from '@/types/photo'
interface ImageModalProps { interface ImageModalProps {
@ -242,6 +242,91 @@ export default function ImageModal({ photo, isOpen, onClose, photos = [], onNavi
const metaInfo = formatMetadata() 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 = ( const modalContent = (
<div <div
className="fixed inset-0 bg-black bg-opacity-90 z-[9999] flex flex-col" className="fixed inset-0 bg-black bg-opacity-90 z-[9999] flex flex-col"
@ -338,6 +423,17 @@ export default function ImageModal({ photo, isOpen, onClose, photos = [], onNavi
<IconRotate className="w-5 h-5" /> <IconRotate className="w-5 h-5" />
</button> </button>
{/* Native Share Button */}
{mounted && (
<button
onClick={(e) => { e.stopPropagation(); handleNativeShare() }}
className="p-2 rounded hover:bg-white hover:bg-opacity-10 transition-colors"
title={locationInfo ? "Share photo (includes location)" : "Share photo"}
>
<IconShare className="w-5 h-5" />
</button>
)}
<button <button
onClick={(e) => { e.stopPropagation(); onClose() }} onClick={(e) => { e.stopPropagation(); onClose() }}
className="p-2 rounded hover:bg-white hover:bg-opacity-10 transition-colors" className="p-2 rounded hover:bg-white hover:bg-opacity-10 transition-colors"