|
|
@@ -4,17 +4,21 @@ import { useAuth } from '@picobaas/client/react';
|
|
|
import { useImages } from '../hooks/useImages';
|
|
|
import { PUBLIC_BUCKET } from '../config';
|
|
|
import ImageUploader from '../components/ImageUploader';
|
|
|
-import type { UploadOptions } from '../types';
|
|
|
+import { formatExpiryStatus } from '../utils/formatters';
|
|
|
+import type { ImageFile, UploadOptions } from '../types';
|
|
|
|
|
|
export default function HomePage() {
|
|
|
const { isAuthenticated } = useAuth();
|
|
|
- const { uploadImage } = useImages(PUBLIC_BUCKET);
|
|
|
+ const { uploadImage, deleteImage } = useImages(PUBLIC_BUCKET);
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
+ const [isDeleting, setIsDeleting] = useState(false);
|
|
|
const [uploadResult, setUploadResult] = useState<{
|
|
|
success: boolean;
|
|
|
url?: string;
|
|
|
+ image?: ImageFile;
|
|
|
error?: string;
|
|
|
} | null>(null);
|
|
|
+ const [copied, setCopied] = useState(false);
|
|
|
|
|
|
const handleUpload = async (file: File, options: UploadOptions) => {
|
|
|
setIsUploading(true);
|
|
|
@@ -27,7 +31,7 @@ export default function HomePage() {
|
|
|
const shareUrl = image.shortUrl
|
|
|
? `${window.location.origin}${image.shortUrl}`
|
|
|
: image.url;
|
|
|
- setUploadResult({ success: true, url: shareUrl });
|
|
|
+ setUploadResult({ success: true, url: shareUrl, image });
|
|
|
}
|
|
|
} catch (err) {
|
|
|
setUploadResult({
|
|
|
@@ -39,10 +43,25 @@ export default function HomePage() {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleDelete = async () => {
|
|
|
+ if (!uploadResult?.image) return;
|
|
|
+
|
|
|
+ setIsDeleting(true);
|
|
|
+ try {
|
|
|
+ await deleteImage(uploadResult.image);
|
|
|
+ setUploadResult(null);
|
|
|
+ } catch (err) {
|
|
|
+ alert(err instanceof Error ? err.message : 'Delete failed');
|
|
|
+ } finally {
|
|
|
+ setIsDeleting(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const copyToClipboard = async (url: string) => {
|
|
|
try {
|
|
|
await navigator.clipboard.writeText(url);
|
|
|
- alert('Link copied to clipboard!');
|
|
|
+ setCopied(true);
|
|
|
+ setTimeout(() => setCopied(false), 2000);
|
|
|
} catch {
|
|
|
// Fallback
|
|
|
const input = document.createElement('input');
|
|
|
@@ -51,7 +70,8 @@ export default function HomePage() {
|
|
|
input.select();
|
|
|
document.execCommand('copy');
|
|
|
document.body.removeChild(input);
|
|
|
- alert('Link copied to clipboard!');
|
|
|
+ setCopied(true);
|
|
|
+ setTimeout(() => setCopied(false), 2000);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -104,33 +124,113 @@ export default function HomePage() {
|
|
|
<h3 className="text-lg font-semibold text-green-900 dark:text-green-300 mb-2">
|
|
|
Image uploaded successfully!
|
|
|
</h3>
|
|
|
+
|
|
|
+ {/* Share URL with copy button */}
|
|
|
+ <p className="text-sm text-green-700 dark:text-green-400 mb-3">
|
|
|
+ Share this link with anyone:
|
|
|
+ </p>
|
|
|
<div className="flex gap-2 max-w-xl mx-auto">
|
|
|
<input
|
|
|
type="text"
|
|
|
value={uploadResult.url}
|
|
|
readOnly
|
|
|
- className="input flex-1 text-sm"
|
|
|
+ className="input flex-1 text-sm font-mono"
|
|
|
/>
|
|
|
<button
|
|
|
onClick={() => uploadResult.url && copyToClipboard(uploadResult.url)}
|
|
|
className="btn btn-primary whitespace-nowrap"
|
|
|
>
|
|
|
- Copy Link
|
|
|
+ {copied ? (
|
|
|
+ <>
|
|
|
+ <svg className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
|
+ </svg>
|
|
|
+ Copied!
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <svg className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
|
+ </svg>
|
|
|
+ Copy Link
|
|
|
+ </>
|
|
|
+ )}
|
|
|
</button>
|
|
|
</div>
|
|
|
- <div className="mt-4">
|
|
|
+
|
|
|
+ {/* Expiration warning */}
|
|
|
+ {uploadResult.image?.expiresAt && (
|
|
|
+ <div className="mt-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 max-w-xl mx-auto">
|
|
|
+ <div className="flex items-center justify-center gap-2 text-amber-700 dark:text-amber-400">
|
|
|
+ <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
+ </svg>
|
|
|
+ <span className="text-sm font-medium">
|
|
|
+ {(() => {
|
|
|
+ const status = formatExpiryStatus(uploadResult.image!.expiresAt!);
|
|
|
+ return status.isExpired
|
|
|
+ ? 'This image has expired'
|
|
|
+ : `This image will be deleted in ${status.label.replace(' left', '')}`;
|
|
|
+ })()}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Action buttons */}
|
|
|
+ <div className="mt-4 flex flex-col sm:flex-row items-center justify-center gap-3">
|
|
|
<a
|
|
|
href={uploadResult.url}
|
|
|
target="_blank"
|
|
|
rel="noopener noreferrer"
|
|
|
className="text-green-700 dark:text-green-400 hover:opacity-80 text-sm font-medium"
|
|
|
>
|
|
|
- Open image in new tab →
|
|
|
+ Open image in new tab
|
|
|
</a>
|
|
|
+ <span className="hidden sm:inline text-gray-300 dark:text-gray-600">|</span>
|
|
|
+ <button
|
|
|
+ onClick={handleDelete}
|
|
|
+ disabled={isDeleting}
|
|
|
+ className="text-red-600 dark:text-red-400 hover:opacity-80 text-sm font-medium disabled:opacity-50"
|
|
|
+ >
|
|
|
+ {isDeleting ? 'Deleting...' : 'Delete image'}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* Register prompt for non-authenticated users */}
|
|
|
+ {!isAuthenticated && (
|
|
|
+ <div className="mt-6 p-4 rounded-lg border max-w-xl mx-auto" style={{ backgroundColor: 'var(--surface-secondary)', borderColor: 'var(--border-color)' }}>
|
|
|
+ <div className="flex items-start gap-3">
|
|
|
+ <div className="flex-shrink-0">
|
|
|
+ <svg className="h-6 w-6" style={{ color: 'var(--accent-600)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div className="text-left">
|
|
|
+ <h4 className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
|
|
|
+ Want to keep your image permanently?
|
|
|
+ </h4>
|
|
|
+ <p className="text-sm mt-1" style={{ color: 'var(--text-secondary)' }}>
|
|
|
+ Create a free account to manage your uploads, remove expiration, and access more features.
|
|
|
+ </p>
|
|
|
+ <Link
|
|
|
+ to="/register"
|
|
|
+ className="inline-flex items-center gap-1 mt-2 text-sm font-medium"
|
|
|
+ style={{ color: 'var(--accent-600)' }}
|
|
|
+ >
|
|
|
+ Create an account
|
|
|
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
|
+ </svg>
|
|
|
+ </Link>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
<button
|
|
|
onClick={() => setUploadResult(null)}
|
|
|
- className="mt-4 text-sm transition-opacity hover:opacity-80"
|
|
|
+ className="mt-6 text-sm transition-opacity hover:opacity-80"
|
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
|
>
|
|
|
Upload another image
|