HomePage.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { useState } from 'react';
  2. import { Link } from 'react-router-dom';
  3. import { useAuth } from '@picobaas/client/react';
  4. import { useImages } from '../hooks/useImages';
  5. import { PUBLIC_BUCKET } from '../config';
  6. import ImageUploader from '../components/ImageUploader';
  7. import type { UploadOptions } from '../types';
  8. export default function HomePage() {
  9. const { isAuthenticated } = useAuth();
  10. const { uploadImage } = useImages(PUBLIC_BUCKET);
  11. const [isUploading, setIsUploading] = useState(false);
  12. const [uploadResult, setUploadResult] = useState<{
  13. success: boolean;
  14. url?: string;
  15. error?: string;
  16. } | null>(null);
  17. const handleUpload = async (file: File, options: UploadOptions) => {
  18. setIsUploading(true);
  19. setUploadResult(null);
  20. try {
  21. const image = await uploadImage(file, options);
  22. if (image) {
  23. // Use short URL if available, otherwise fall back to raw URL
  24. const shareUrl = image.shortUrl
  25. ? `${window.location.origin}${image.shortUrl}`
  26. : image.url;
  27. setUploadResult({ success: true, url: shareUrl });
  28. }
  29. } catch (err) {
  30. setUploadResult({
  31. success: false,
  32. error: err instanceof Error ? err.message : 'Upload failed',
  33. });
  34. } finally {
  35. setIsUploading(false);
  36. }
  37. };
  38. const copyToClipboard = async (url: string) => {
  39. try {
  40. await navigator.clipboard.writeText(url);
  41. alert('Link copied to clipboard!');
  42. } catch {
  43. // Fallback
  44. const input = document.createElement('input');
  45. input.value = url;
  46. document.body.appendChild(input);
  47. input.select();
  48. document.execCommand('copy');
  49. document.body.removeChild(input);
  50. alert('Link copied to clipboard!');
  51. }
  52. };
  53. return (
  54. <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
  55. {/* Hero section */}
  56. <div className="text-center mb-12">
  57. <h1
  58. className="text-4xl sm:text-5xl font-bold mb-4"
  59. style={{ color: 'var(--text-primary)' }}
  60. >
  61. Share images instantly
  62. </h1>
  63. <p
  64. className="text-xl mb-6"
  65. style={{ color: 'var(--text-secondary)' }}
  66. >
  67. Upload an image and get a shareable link. No account required.
  68. </p>
  69. {!isAuthenticated && (
  70. <p className="text-sm" style={{ color: 'var(--text-muted)' }}>
  71. <Link
  72. to="/register"
  73. className="font-medium"
  74. style={{ color: 'var(--accent-600)' }}
  75. >
  76. Create an account
  77. </Link>{' '}
  78. to manage your uploads and access more features.
  79. </p>
  80. )}
  81. </div>
  82. {/* Upload result */}
  83. {uploadResult && (
  84. <div
  85. className={`mb-8 p-6 rounded-xl border ${
  86. uploadResult.success
  87. ? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800'
  88. : 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
  89. }`}
  90. >
  91. {uploadResult.success ? (
  92. <div className="text-center">
  93. <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 mb-4">
  94. <svg className="h-6 w-6 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  95. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
  96. </svg>
  97. </div>
  98. <h3 className="text-lg font-semibold text-green-900 dark:text-green-300 mb-2">
  99. Image uploaded successfully!
  100. </h3>
  101. <div className="flex gap-2 max-w-xl mx-auto">
  102. <input
  103. type="text"
  104. value={uploadResult.url}
  105. readOnly
  106. className="input flex-1 text-sm"
  107. />
  108. <button
  109. onClick={() => uploadResult.url && copyToClipboard(uploadResult.url)}
  110. className="btn btn-primary whitespace-nowrap"
  111. >
  112. Copy Link
  113. </button>
  114. </div>
  115. <div className="mt-4">
  116. <a
  117. href={uploadResult.url}
  118. target="_blank"
  119. rel="noopener noreferrer"
  120. className="text-green-700 dark:text-green-400 hover:opacity-80 text-sm font-medium"
  121. >
  122. Open image in new tab →
  123. </a>
  124. </div>
  125. <button
  126. onClick={() => setUploadResult(null)}
  127. className="mt-4 text-sm transition-opacity hover:opacity-80"
  128. style={{ color: 'var(--text-secondary)' }}
  129. >
  130. Upload another image
  131. </button>
  132. </div>
  133. ) : (
  134. <div className="text-center">
  135. <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
  136. <svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  137. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
  138. </svg>
  139. </div>
  140. <h3 className="text-lg font-semibold text-red-900 dark:text-red-300 mb-2">
  141. Upload failed
  142. </h3>
  143. <p className="text-red-700 dark:text-red-400 mb-4">{uploadResult.error}</p>
  144. <button
  145. onClick={() => setUploadResult(null)}
  146. className="btn btn-secondary"
  147. >
  148. Try again
  149. </button>
  150. </div>
  151. )}
  152. </div>
  153. )}
  154. {/* Upload area */}
  155. {!uploadResult?.success && (
  156. <div className="card p-8">
  157. <ImageUploader
  158. onUpload={handleUpload}
  159. isUploading={isUploading}
  160. />
  161. </div>
  162. )}
  163. {/* Features */}
  164. <div className="mt-16 grid md:grid-cols-3 gap-8">
  165. <div className="text-center">
  166. <div
  167. className="inline-flex items-center justify-center w-12 h-12 rounded-full mb-4"
  168. style={{ backgroundColor: 'var(--accent-100)' }}
  169. >
  170. <svg
  171. className="h-6 w-6"
  172. style={{ color: 'var(--accent-600)' }}
  173. fill="none"
  174. viewBox="0 0 24 24"
  175. stroke="currentColor"
  176. >
  177. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
  178. </svg>
  179. </div>
  180. <h3
  181. className="text-lg font-semibold mb-2"
  182. style={{ color: 'var(--text-primary)' }}
  183. >
  184. Instant Upload
  185. </h3>
  186. <p style={{ color: 'var(--text-secondary)' }}>
  187. Drop your image and get a link instantly. No sign-up needed.
  188. </p>
  189. </div>
  190. <div className="text-center">
  191. <div
  192. className="inline-flex items-center justify-center w-12 h-12 rounded-full mb-4"
  193. style={{ backgroundColor: 'var(--accent-100)' }}
  194. >
  195. <svg
  196. className="h-6 w-6"
  197. style={{ color: 'var(--accent-600)' }}
  198. fill="none"
  199. viewBox="0 0 24 24"
  200. stroke="currentColor"
  201. >
  202. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
  203. </svg>
  204. </div>
  205. <h3
  206. className="text-lg font-semibold mb-2"
  207. style={{ color: 'var(--text-primary)' }}
  208. >
  209. Auto Expiry
  210. </h3>
  211. <p style={{ color: 'var(--text-secondary)' }}>
  212. Set expiration times to automatically delete images after a period.
  213. </p>
  214. </div>
  215. <div className="text-center">
  216. <div
  217. className="inline-flex items-center justify-center w-12 h-12 rounded-full mb-4"
  218. style={{ backgroundColor: 'var(--accent-100)' }}
  219. >
  220. <svg
  221. className="h-6 w-6"
  222. style={{ color: 'var(--accent-600)' }}
  223. fill="none"
  224. viewBox="0 0 24 24"
  225. stroke="currentColor"
  226. >
  227. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
  228. </svg>
  229. </div>
  230. <h3
  231. className="text-lg font-semibold mb-2"
  232. style={{ color: 'var(--text-primary)' }}
  233. >
  234. Easy Sharing
  235. </h3>
  236. <p style={{ color: 'var(--text-secondary)' }}>
  237. Get shareable links for your images with just one click.
  238. </p>
  239. </div>
  240. </div>
  241. </div>
  242. );
  243. }