build view count total
This commit is contained in:
@@ -36,6 +36,20 @@ export async function adminRoutes(app: FastifyInstance) {
|
||||
return { total: pending.length, indexed: done, no_text_found: failed };
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/stats
|
||||
* Aggregate share and view counts across all memes.
|
||||
*/
|
||||
app.get('/api/admin/stats', { preHandler: requireAuth }, async () => {
|
||||
const row = db
|
||||
.prepare('SELECT SUM(share_count) as total_shares, SUM(view_count) as total_views FROM memes')
|
||||
.get() as { total_shares: number | null; total_views: number | null };
|
||||
return {
|
||||
total_shares: row.total_shares ?? 0,
|
||||
total_views: row.total_views ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/reindex/status
|
||||
* Returns how many memes still need OCR indexing.
|
||||
|
||||
@@ -155,6 +155,10 @@ export const api = {
|
||||
reindex(): Promise<{ total: number; indexed: number; no_text_found: number }> {
|
||||
return apiFetch('/api/admin/reindex', { method: 'POST' });
|
||||
},
|
||||
|
||||
stats(): Promise<{ total_shares: number; total_views: number }> {
|
||||
return apiFetch('/api/admin/stats');
|
||||
},
|
||||
},
|
||||
|
||||
imageUrl(filePath: string): string {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { X, ScanText, Database, RefreshCw, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { useReindexStatus, useReindex, useCollections, useTags, useMemes } from '../hooks/useMemes';
|
||||
import React from 'react';
|
||||
import { X, ScanText, Database, RefreshCw, CheckCircle2, AlertCircle, Loader2, Share2, MousePointerClick } from 'lucide-react';
|
||||
import { useReindexStatus, useReindex, useCollections, useTags, useMemes, useAdminStats } from '../hooks/useMemes';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -11,6 +12,7 @@ export function SettingsModal({ onClose }: Props) {
|
||||
const { data: collections } = useCollections();
|
||||
const { data: tags } = useTags();
|
||||
const { data: allMemes } = useMemes({ parent_only: false, limit: 1 });
|
||||
const { data: adminStats } = useAdminStats();
|
||||
|
||||
async function handleReindex() {
|
||||
await reindex.mutateAsync();
|
||||
@@ -45,6 +47,18 @@ export function SettingsModal({ onClose }: Props) {
|
||||
<StatCard label="Collections" value={collections?.length ?? '—'} />
|
||||
<StatCard label="Tags" value={tags?.length ?? '—'} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<StatCard
|
||||
label="Total shares"
|
||||
value={adminStats?.total_shares ?? '—'}
|
||||
icon={<Share2 size={13} className="text-zinc-500" />}
|
||||
/>
|
||||
<StatCard
|
||||
label="Link clicks"
|
||||
value={adminStats?.total_views ?? '—'}
|
||||
icon={<MousePointerClick size={13} className="text-zinc-500" />}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-zinc-800" />
|
||||
@@ -130,9 +144,10 @@ export function SettingsModal({ onClose }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: number | string }) {
|
||||
function StatCard({ label, value, icon }: { label: string; value: number | string; icon?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-zinc-800/60 rounded-lg px-3 py-3 text-center">
|
||||
{icon && <div className="flex justify-center mb-1">{icon}</div>}
|
||||
<div className="text-lg font-semibold text-zinc-200">{value}</div>
|
||||
<div className="text-xs text-zinc-500 mt-0.5">{label}</div>
|
||||
</div>
|
||||
|
||||
@@ -106,6 +106,14 @@ export function useDeleteMeme() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminStats() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'stats'],
|
||||
queryFn: () => api.admin.stats(),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useReindexStatus() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'reindex-status'],
|
||||
|
||||
Reference in New Issue
Block a user