feat(gif): favourites and category shortcuts
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s

Favourites are stored server-side per user, so one made on the phone is there
on the desktop — the point of favouriting. The whole result is stored rather
than an id: the provider offers no lookup by id, so an id-only favourite could
not be rendered without re-finding it through search.

Category chips translate their label but not their query, which goes to a
provider that indexes in English.

The star sits beside the tile button rather than inside it: a button within a
button is invalid and swallows the click. Toggling is optimistic and reverts
on failure, and favourites skip both the loading skeleton and the infinite
scroll, which belong to provider-backed browsing only.

Server caps favourites per user and rejects non-http(s) URLs, which become
<img src> in everyone's picker.
This commit is contained in:
2026-08-31 13:12:50 -03:00
parent 0fc6abeb6e
commit fb662bfe12
9 changed files with 4290 additions and 24 deletions
+21
View File
@@ -566,3 +566,24 @@ export const spotifyConnections = sqliteTable('spotify_connections', {
spotifyUserId: text('spotify_user_id'),
createdAt: integer('created_at').notNull(),
});
/**
* Favourited GIFs, one row per user per GIF.
*
* Stores the whole result rather than an id: the provider offers no lookup by
* id, so a favourites tab that only kept ids could not render without
* re-searching for something the user may never find again.
*/
export const gifFavorites = sqliteTable('gif_favorites', {
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
gifId: text('gif_id').notNull(),
title: text('title').notNull(),
previewUrl: text('preview_url').notNull(),
url: text('url').notNull(),
width: integer('width').notNull(),
height: integer('height').notNull(),
createdAt: integer('created_at').notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.gifId] }),
userIdx: index('idx_gif_favorites_user_id').on(table.userId),
}));