This commit adds support for reusing existing character images from exis
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { db } from './db';
|
||||
import { stories, pages, type Story, type Page } from './schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { generateComicSlug } from './slug-generator';
|
||||
|
||||
export async function createStory(data: { title: string; description?: string; userId?: string }): Promise<Story> {
|
||||
// Generate a unique slug
|
||||
let slug = generateComicSlug();
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
|
||||
// Ensure slug uniqueness
|
||||
while (attempts < maxAttempts) {
|
||||
const existing = await db.select().from(stories).where(eq(stories.slug, slug)).limit(1);
|
||||
if (existing.length === 0) break;
|
||||
slug = generateComicSlug();
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
// Fallback to a simple random slug if we can't generate a unique one
|
||||
slug = `story-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
|
||||
}
|
||||
|
||||
const [story] = await db.insert(stories).values({ ...data, slug }).returning();
|
||||
return story;
|
||||
}
|
||||
|
||||
export async function createPage(data: {
|
||||
storyId: string;
|
||||
pageNumber: number;
|
||||
prompt: string;
|
||||
characterImageUrls: string[];
|
||||
style: string;
|
||||
}): Promise<Page> {
|
||||
const [page] = await db.insert(pages).values(data).returning();
|
||||
return page;
|
||||
}
|
||||
|
||||
export async function updatePage(pageId: string, generatedImageUrl: string): Promise<void> {
|
||||
await db.update(pages)
|
||||
.set({ generatedImageUrl, updatedAt: new Date() })
|
||||
.where(eq(pages.id, pageId));
|
||||
}
|
||||
|
||||
export async function getStoryWithPages(storyId: string): Promise<{ story: Story; pages: Page[] } | null> {
|
||||
const storyResult = await db.select().from(stories).where(eq(stories.id, storyId)).limit(1);
|
||||
|
||||
if (storyResult.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storyPages = await db.select().from(pages)
|
||||
.where(eq(pages.storyId, storyId))
|
||||
.orderBy(pages.pageNumber);
|
||||
|
||||
return {
|
||||
story: storyResult[0],
|
||||
pages: storyPages,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStoryWithPagesBySlug(slug: string): Promise<{ story: Story; pages: Page[] } | null> {
|
||||
console.log("DB: Searching for slug:", slug);
|
||||
const storyResult = await db.select().from(stories).where(eq(stories.slug, slug)).limit(1);
|
||||
console.log("DB: Story result count:", storyResult.length);
|
||||
|
||||
if (storyResult.length === 0) {
|
||||
console.log("DB: No story found with slug:", slug);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("DB: Found story:", storyResult[0].id, storyResult[0].slug);
|
||||
const storyPages = await db.select().from(pages)
|
||||
.where(eq(pages.storyId, storyResult[0].id))
|
||||
.orderBy(pages.pageNumber);
|
||||
|
||||
console.log("DB: Found pages count:", storyPages.length);
|
||||
|
||||
return {
|
||||
story: storyResult[0],
|
||||
pages: storyPages,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStoryCharacterImages(storyId: string): Promise<string[]> {
|
||||
const storyPages = await db.select({ characterImageUrls: pages.characterImageUrls })
|
||||
.from(pages)
|
||||
.where(eq(pages.storyId, storyId));
|
||||
|
||||
// Flatten all character URLs from all pages and remove duplicates
|
||||
const allUrls = storyPages.flatMap(page => page.characterImageUrls);
|
||||
return [...new Set(allUrls)]; // Remove duplicates
|
||||
}
|
||||
|
||||
export async function getNextPageNumber(storyId: string): Promise<number> {
|
||||
const storyPages = await db.select({ pageNumber: pages.pageNumber })
|
||||
.from(pages)
|
||||
.where(eq(pages.storyId, storyId))
|
||||
.orderBy(pages.pageNumber);
|
||||
|
||||
if (storyPages.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(...storyPages.map(p => p.pageNumber)) + 1;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { neon } from "@neondatabase/serverless";
|
||||
import { drizzle } from "drizzle-orm/neon-http";
|
||||
import "../envConfig.ts";
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error("DATABASE_URL environment variable is not set");
|
||||
}
|
||||
|
||||
const sql = neon(process.env.DATABASE_URL);
|
||||
export const db = drizzle(sql);
|
||||
@@ -0,0 +1,44 @@
|
||||
import { pgTable, text, integer, timestamp, uuid, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// Stories table
|
||||
export const stories = pgTable('stories', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
title: text('title').notNull(),
|
||||
slug: text('slug').notNull().unique(),
|
||||
description: text('description'),
|
||||
userId: uuid('user_id'), // Optional - for future Clerk auth
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Pages table
|
||||
export const pages = pgTable('pages', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
storyId: uuid('story_id').references(() => stories.id, { onDelete: 'cascade' }).notNull(),
|
||||
pageNumber: integer('page_number').notNull(),
|
||||
prompt: text('prompt').notNull(),
|
||||
characterImageUrls: jsonb('character_image_urls').$type<string[]>().default([]).notNull(),
|
||||
generatedImageUrl: text('generated_image_url'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Relations
|
||||
export const storiesRelations = relations(stories, ({ many }) => ({
|
||||
pages: many(pages),
|
||||
}));
|
||||
|
||||
export const pagesRelations = relations(pages, ({ one }) => ({
|
||||
story: one(stories, {
|
||||
fields: [pages.storyId],
|
||||
references: [stories.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Types
|
||||
export type Story = typeof stories.$inferSelect;
|
||||
export type NewStory = typeof stories.$inferInsert;
|
||||
|
||||
export type Page = typeof pages.$inferSelect;
|
||||
export type NewPage = typeof pages.$inferInsert;
|
||||
@@ -0,0 +1,46 @@
|
||||
// Comic-themed words for generating beautiful slugs
|
||||
const COMIC_WORDS = {
|
||||
heroes: ['super', 'hero', 'captain', 'iron', 'spider', 'bat', 'wonder', 'flash', 'green', 'black', 'deadpool', 'wolverine', 'hulk', 'thor', 'captain'],
|
||||
villains: ['dark', 'shadow', 'evil', 'master', 'doctor', 'joker', 'lex', 'magneto', 'thanos', 'loki', 'venom', 'bane', 'riddler'],
|
||||
actions: ['strike', 'force', 'power', 'legend', 'saga', 'quest', 'battle', 'warrior', 'guardian', 'defender', 'avenger', 'justice'],
|
||||
settings: ['city', 'world', 'universe', 'realm', 'dimension', 'galaxy', 'earth', 'mars', 'moon', 'space', 'future', 'past'],
|
||||
styles: ['noir', 'manga', 'comic', 'graphic', 'epic', 'legend', 'myth', 'tale', 'story', 'chronicle', 'adventure']
|
||||
};
|
||||
|
||||
const NUMBERS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
|
||||
|
||||
export function generateComicSlug(): string {
|
||||
// Generate 2-3 random words from different categories
|
||||
const categories = Object.keys(COMIC_WORDS) as (keyof typeof COMIC_WORDS)[];
|
||||
const selectedCategories = categories.sort(() => 0.5 - Math.random()).slice(0, 2 + Math.floor(Math.random() * 2));
|
||||
|
||||
const words: string[] = [];
|
||||
selectedCategories.forEach(category => {
|
||||
const categoryWords = COMIC_WORDS[category];
|
||||
const randomWord = categoryWords[Math.floor(Math.random() * categoryWords.length)];
|
||||
words.push(randomWord);
|
||||
});
|
||||
|
||||
// Add a random number word sometimes
|
||||
if (Math.random() > 0.7) {
|
||||
const randomNumber = NUMBERS[Math.floor(Math.random() * NUMBERS.length)];
|
||||
words.push(randomNumber);
|
||||
}
|
||||
|
||||
// Generate short random string (4-5 chars)
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const randomString = Array.from({ length: 4 + Math.floor(Math.random() * 2) }, () =>
|
||||
chars[Math.floor(Math.random() * chars.length)]
|
||||
).join('');
|
||||
|
||||
// Combine words with hyphens and add random string
|
||||
const slugWords = words.join('-');
|
||||
return `${slugWords}-${randomString}`;
|
||||
}
|
||||
|
||||
export function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
Reference in New Issue
Block a user