Enable users to create custom video playlists and save favorite videos

Adds database schemas and Drizzle ORM setup for user playlists and favorites features.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aa92e7e2-ec62-4c92-b21b-02ef78a664c2
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/aa92e7e2-ec62-4c92-b21b-02ef78a664c2/Xt4Awd6
This commit is contained in:
sebastjanartic 2025-08-04 20:37:50 +00:00
parent 68800e6eb4
commit 7a4f390fb5
5 changed files with 298 additions and 1 deletions

View File

@ -37,3 +37,6 @@ author = "agent"
task = "shell.exec"
args = "npm run dev"
waitForPort = 5000
[agent]
integrations = ["javascript_database==1.0.0", "javascript_log_in_with_replit==1.0.0"]

198
package-lock.json generated
View File

@ -40,6 +40,7 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/memoizee": "^0.4.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -54,8 +55,10 @@
"hls.js": "^1.6.7",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memoizee": "^0.4.17",
"memorystore": "^1.6.7",
"next-themes": "^0.4.6",
"openid-client": "^6.6.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"react": "^18.3.1",
@ -3465,6 +3468,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/memoizee": {
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.12.tgz",
"integrity": "sha512-EdtpwNYNhe3kZ+4TlXj/++pvBoU0KdrAICMzgI7vjWgu9sIvvUhu9XR8Ks4L6Wh3sxpZ22wkZR7yCLAqUjnZuQ==",
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -4112,6 +4121,19 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@ -5010,6 +5032,58 @@
"node": ">= 0.4"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/es6-weak-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
"integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
"license": "ISC",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.46",
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.1"
}
},
"node_modules/esbuild": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
@ -5080,6 +5154,21 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -5089,6 +5178,16 @@
"node": ">= 0.6"
}
},
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@ -5205,6 +5304,15 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
@ -5684,6 +5792,12 @@
"node": ">=0.12.0"
}
},
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -5714,6 +5828,15 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jose": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz",
"integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6040,6 +6163,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lru-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
"integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==",
"license": "MIT",
"dependencies": {
"es5-ext": "~0.10.2"
}
},
"node_modules/lucide-react": {
"version": "0.453.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz",
@ -6066,6 +6198,25 @@
"node": ">= 0.6"
}
},
"node_modules/memoizee": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
"integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"es5-ext": "^0.10.64",
"es6-weak-map": "^2.0.3",
"event-emitter": "^0.3.5",
"is-promise": "^2.2.2",
"lru-queue": "^0.1.0",
"next-tick": "^1.1.0",
"timers-ext": "^0.1.7"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/memorystore": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz",
@ -6266,6 +6417,12 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/node-gyp-build": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
@ -6304,6 +6461,15 @@
"node": ">=0.10.0"
}
},
"node_modules/oauth4webapi": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.1.tgz",
"integrity": "sha512-b39+drVyA4aNUptFOhkkmGWnG/BE7dT29SW/8PVYElqp7j/DBqzm5SS1G+MUD07XlTcBOAG+6Cb/35Cx2kHIuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -6361,6 +6527,19 @@
"node": ">= 0.8"
}
},
"node_modules/openid-client": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.6.2.tgz",
"integrity": "sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==",
"license": "MIT",
"dependencies": {
"jose": "^6.0.11",
"oauth4webapi": "^3.5.4"
},
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -7731,6 +7910,19 @@
"node": ">=0.8"
}
},
"node_modules/timers-ext": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz",
"integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -8246,6 +8438,12 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

View File

@ -42,6 +42,7 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
"@types/memoizee": "^0.4.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -56,8 +57,10 @@
"hls.js": "^1.6.7",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memoizee": "^0.4.17",
"memorystore": "^1.6.7",
"next-themes": "^0.4.6",
"openid-client": "^6.6.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"react": "^18.3.1",

15
server/db.ts Normal file
View File

@ -0,0 +1,15 @@
import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from "ws";
import * as schema from "@shared/schema";
neonConfig.webSocketConstructor = ws;
if (!process.env.DATABASE_URL) {
throw new Error(
"DATABASE_URL must be set. Did you forget to provision a database?",
);
}
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });

View File

@ -1,4 +1,4 @@
import { sql } from "drizzle-orm";
import { sql, relations } from "drizzle-orm";
import { pgTable, text, varchar, integer, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
@ -17,10 +17,88 @@ export const videos = pgTable("videos", {
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
// User playlists table
export const playlists = pgTable("playlists", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
name: text("name").notNull(),
description: text("description"),
isPublic: integer("is_public").notNull().default(0), // 0 = private, 1 = public
userId: varchar("user_id"), // For future user authentication
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
// Junction table for playlist-video relationships
export const playlistVideos = pgTable("playlist_videos", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
playlistId: varchar("playlist_id").notNull(),
videoId: varchar("video_id").notNull(),
position: integer("position").notNull().default(0), // Order within playlist
addedAt: timestamp("added_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
// Favorites table (simplified playlist for favorites)
export const favorites = pgTable("favorites", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
videoId: varchar("video_id").notNull(),
userId: varchar("user_id"), // For future user authentication
addedAt: timestamp("added_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
export const insertVideoSchema = createInsertSchema(videos).omit({
id: true,
createdAt: true,
});
export const insertPlaylistSchema = createInsertSchema(playlists).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertPlaylistVideoSchema = createInsertSchema(playlistVideos).omit({
id: true,
addedAt: true,
});
export const insertFavoriteSchema = createInsertSchema(favorites).omit({
id: true,
addedAt: true,
});
// Relations
export const playlistsRelations = relations(playlists, ({ many }) => ({
playlistVideos: many(playlistVideos),
}));
export const playlistVideosRelations = relations(playlistVideos, ({ one }) => ({
playlist: one(playlists, {
fields: [playlistVideos.playlistId],
references: [playlists.id],
}),
video: one(videos, {
fields: [playlistVideos.videoId],
references: [videos.id],
}),
}));
export const videosRelations = relations(videos, ({ many }) => ({
playlistVideos: many(playlistVideos),
favorites: many(favorites),
}));
export const favoritesRelations = relations(favorites, ({ one }) => ({
video: one(videos, {
fields: [favorites.videoId],
references: [videos.id],
}),
}));
export type InsertVideo = z.infer<typeof insertVideoSchema>;
export type Video = typeof videos.$inferSelect;
export type Playlist = typeof playlists.$inferSelect;
export type InsertPlaylist = z.infer<typeof insertPlaylistSchema>;
export type PlaylistVideo = typeof playlistVideos.$inferSelect;
export type InsertPlaylistVideo = z.infer<typeof insertPlaylistVideoSchema>;
export type Favorite = typeof favorites.$inferSelect;
export type InsertFavorite = z.infer<typeof insertFavoriteSchema>;