build share count

This commit is contained in:
2026-03-28 22:02:37 -05:00
parent d1bfba89a8
commit d3fb42e8ed
6 changed files with 36 additions and 4 deletions

View File

@@ -66,6 +66,10 @@ if (!memesCols.find((c) => c.name === 'ocr_text')) {
db.exec('ALTER TABLE memes ADD COLUMN ocr_text TEXT');
}
if (!memesCols.find((c) => c.name === 'share_count')) {
db.exec('ALTER TABLE memes ADD COLUMN share_count INTEGER NOT NULL DEFAULT 0');
}
// Indexes that depend on migrated columns — created after columns are guaranteed to exist
db.exec(`
CREATE INDEX IF NOT EXISTS idx_memes_collection_id ON memes(collection_id);

View File

@@ -214,6 +214,14 @@ export async function memesRoutes(app: FastifyInstance) {
}
);
// Record a share — no auth required, public action
app.post<{ Params: { id: string } }>('/api/memes/:id/share', async (req, reply) => {
const meme = getMemeById(req.params.id);
if (!meme) return reply.status(404).send({ error: 'Not found' });
db.prepare('UPDATE memes SET share_count = share_count + 1 WHERE id = ?').run(meme.id);
return { share_count: (meme.share_count ?? 0) + 1 };
});
// Delete meme (children cascade)
app.delete<{ Params: { id: string } }>(
'/api/memes/:id',

View File

@@ -11,6 +11,7 @@ export interface Meme {
parent_id: string | null;
collection_id: number | null;
ocr_text: string | null;
share_count: number;
created_at: string;
tags: string[];
}

View File

@@ -11,6 +11,7 @@ export interface Meme {
parent_id: string | null;
collection_id: number | null;
ocr_text: string | null;
share_count: number;
created_at: string;
tags: string[];
children?: Meme[];
@@ -96,6 +97,10 @@ export const api = {
});
},
share(id: string): Promise<{ share_count: number }> {
return apiFetch(`/api/memes/${id}/share`, { method: 'POST' });
},
move(id: string, collection_id: number): Promise<Meme> {
return apiFetch(`/api/memes/${id}/collection`, {
method: 'PUT',

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { X, Minimize2, Trash2, Edit2, Check, Layers, FolderOpen, Inbox, ScanText, ChevronDown, ChevronUp, ExternalLink, Image, Info } from 'lucide-react';
import { X, Minimize2, Trash2, Edit2, Check, Layers, FolderOpen, Inbox, ScanText, ChevronDown, ChevronUp, ExternalLink, Image, Info, Share2 } from 'lucide-react';
import { useMeme, useDeleteMeme, useUpdateMeme, useMoveMeme, useCollections } from '../hooks/useMemes';
import { useAuth } from '../hooks/useAuth';
import { SharePanel } from './SharePanel';
@@ -221,7 +221,7 @@ export function MemeDetail({ memeId, onClose }: Props) {
{displayMeme && (
<section>
<h3 className="text-xs font-semibold text-zinc-500 uppercase tracking-wider mb-2">Share</h3>
<SharePanel meme={displayMeme} />
<SharePanel meme={displayMeme} onShared={refetch} />
</section>
)}
@@ -333,6 +333,13 @@ export function MemeDetail({ memeId, onClose }: Props) {
<dt className="text-zinc-500">Type</dt>
<dd className="text-zinc-300">{meme.mime_type.replace('image/', '').replace('video/', '')}</dd>
</div>
<div className="flex justify-between">
<dt className="text-zinc-500">Shared</dt>
<dd className="text-zinc-300 flex items-center gap-1">
<Share2 size={11} className="text-zinc-500" />
{meme.share_count ?? 0} time{(meme.share_count ?? 0) !== 1 ? 's' : ''}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-zinc-500">Uploaded</dt>
<dd className="text-zinc-300 text-right text-xs">{formatDate(meme.created_at)}</dd>

View File

@@ -5,19 +5,24 @@ import { api } from '../api/client';
interface Props {
meme: Meme;
onShared?: () => void; // called after a share is recorded so parent can refetch
}
export function SharePanel({ meme }: Props) {
export function SharePanel({ meme, onShared }: Props) {
const [copied, setCopied] = useState(false);
// /m/:id gives crawlers OG meta tags so SMS/Telegram show a rich preview card
const shareUrl = `${window.location.origin}/m/${meme.id}`;
const imageUrl = `${window.location.origin}${api.imageUrl(meme.file_path)}`;
function recordShare() {
api.memes.share(meme.id).then(() => onShared?.()).catch(() => {});
}
async function copyLink() {
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
recordShare();
}
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(meme.title)}`;
@@ -42,6 +47,7 @@ export function SharePanel({ meme }: Props) {
href={telegramUrl}
target="_blank"
rel="noopener noreferrer"
onClick={recordShare}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#229ED9]/20 hover:bg-[#229ED9]/30 text-[#229ED9] text-sm font-medium transition-colors"
title="Share on Telegram"
>
@@ -51,6 +57,7 @@ export function SharePanel({ meme }: Props) {
<a
href={smsUrl}
onClick={recordShare}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-green-900/30 hover:bg-green-900/50 text-green-400 text-sm font-medium transition-colors"
title="Share via SMS"
>