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
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:
@@ -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),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user