From 9d24f47e68b28c046adb45a748f8f13cfc7a2e23 Mon Sep 17 00:00:00 2001 From: mukimovd <41473651-mukimovd@users.noreply.replit.com> Date: Thu, 8 May 2025 00:05:04 +0000 Subject: [PATCH] Serve demo content from a separate JSON file for easier data management Implements JsonStorage class to load and manage sample data from sampleData.json, replacing MemStorage. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/3575e342-a373-4351-a35e-5c684b14fd0f.jpg --- server/jsonStorage.ts | 819 ++++++++++++++++++++++++++++++++++++++++++ server/storage.ts | 4 +- 2 files changed, 822 insertions(+), 1 deletion(-) create mode 100644 server/jsonStorage.ts diff --git a/server/jsonStorage.ts b/server/jsonStorage.ts new file mode 100644 index 0000000..fef8d36 --- /dev/null +++ b/server/jsonStorage.ts @@ -0,0 +1,819 @@ +import fs from 'fs'; +import path from 'path'; +import { IStorage } from './storage'; +import { + User, Author, Work, Translation, Tag, Bookmark, Comment, + Like, Collection, CollectionItem, TimelineEvent, BlogPost, + ReadingProgress, InsertUser, InsertAuthor, InsertWork, + InsertTranslation, InsertTag, InsertBookmark, InsertComment, + InsertLike, InsertCollection, InsertCollectionItem, InsertTimelineEvent, + InsertBlogPost, InsertReadingProgress +} from '../shared/schema'; + +export class JsonStorage implements IStorage { + private data: { + users: Map; + authors: Map; + works: Map; + translations: Map; + tags: Map; + workTagsRelations: Map>; + blogPostTagsRelations: Map>; + bookmarks: Map; + comments: Map; + likes: Map; + collections: Map; + collectionItems: Map; + timelineEvents: Map; + blogPosts: Map; + readingProgresses: Map; + }; + + private counters: { + userId: number; + authorId: number; + workId: number; + translationId: number; + tagId: number; + bookmarkId: number; + commentId: number; + likeId: number; + collectionId: number; + collectionItemId: number; + timelineEventId: number; + blogPostId: number; + readingProgressId: number; + }; + + private dataFilePath: string; + + constructor(dataFilePath: string = path.join(process.cwd(), 'data', 'sampleData.json')) { + this.dataFilePath = dataFilePath; + this.data = { + users: new Map(), + authors: new Map(), + works: new Map(), + translations: new Map(), + tags: new Map(), + workTagsRelations: new Map>(), + blogPostTagsRelations: new Map>(), + bookmarks: new Map(), + comments: new Map(), + likes: new Map(), + collections: new Map(), + collectionItems: new Map(), + timelineEvents: new Map(), + blogPosts: new Map(), + readingProgresses: new Map(), + }; + + this.counters = { + userId: 1, + authorId: 1, + workId: 1, + translationId: 1, + tagId: 1, + bookmarkId: 1, + commentId: 1, + likeId: 1, + collectionId: 1, + collectionItemId: 1, + timelineEventId: 1, + blogPostId: 1, + readingProgressId: 1, + }; + + this.loadDataFromFile(); + } + + private loadDataFromFile() { + try { + console.log(`Loading data from ${this.dataFilePath}`); + if (!fs.existsSync(this.dataFilePath)) { + console.error(`Data file not found: ${this.dataFilePath}`); + return; + } + + const jsonData = JSON.parse(fs.readFileSync(this.dataFilePath, 'utf8')); + + // Load users + if (jsonData.users) { + jsonData.users.forEach((user: User) => { + this.data.users.set(user.id, { + ...user, + createdAt: new Date(user.createdAt) + }); + this.counters.userId = Math.max(this.counters.userId, user.id + 1); + }); + } + + // Load authors + if (jsonData.authors) { + jsonData.authors.forEach((author: Author) => { + this.data.authors.set(author.id, { + ...author, + createdAt: new Date(author.createdAt) + }); + this.counters.authorId = Math.max(this.counters.authorId, author.id + 1); + }); + } + + // Load works + if (jsonData.works) { + jsonData.works.forEach((work: Work) => { + this.data.works.set(work.id, { + ...work, + createdAt: new Date(work.createdAt) + }); + this.counters.workId = Math.max(this.counters.workId, work.id + 1); + }); + } + + // Load translations + if (jsonData.translations) { + jsonData.translations.forEach((translation: Translation) => { + this.data.translations.set(translation.id, { + ...translation, + createdAt: new Date(translation.createdAt) + }); + this.counters.translationId = Math.max(this.counters.translationId, translation.id + 1); + }); + } + + // Load tags + if (jsonData.tags) { + jsonData.tags.forEach((tag: Tag) => { + this.data.tags.set(tag.id, { + ...tag, + createdAt: new Date(tag.createdAt) + }); + this.counters.tagId = Math.max(this.counters.tagId, tag.id + 1); + }); + } + + // Load work-tag relations + if (jsonData.workTagsRelations) { + for (const [workId, tagIds] of Object.entries(jsonData.workTagsRelations)) { + this.data.workTagsRelations.set( + parseInt(workId), + new Set((tagIds as number[]).map(id => id)) + ); + } + } + + // Load blog post-tag relations + if (jsonData.blogPostTagsRelations) { + for (const [postId, tagIds] of Object.entries(jsonData.blogPostTagsRelations)) { + this.data.blogPostTagsRelations.set( + parseInt(postId), + new Set((tagIds as number[]).map(id => id)) + ); + } + } + + // Load bookmarks + if (jsonData.bookmarks) { + jsonData.bookmarks.forEach((bookmark: Bookmark) => { + this.data.bookmarks.set(bookmark.id, { + ...bookmark, + createdAt: new Date(bookmark.createdAt) + }); + this.counters.bookmarkId = Math.max(this.counters.bookmarkId, bookmark.id + 1); + }); + } + + // Load comments + if (jsonData.comments) { + jsonData.comments.forEach((comment: Comment) => { + this.data.comments.set(comment.id, { + ...comment, + createdAt: new Date(comment.createdAt) + }); + this.counters.commentId = Math.max(this.counters.commentId, comment.id + 1); + }); + } + + // Load likes + if (jsonData.likes) { + jsonData.likes.forEach((like: Like) => { + this.data.likes.set(like.id, { + ...like, + createdAt: new Date(like.createdAt) + }); + this.counters.likeId = Math.max(this.counters.likeId, like.id + 1); + }); + } + + // Load collections + if (jsonData.collections) { + jsonData.collections.forEach((collection: Collection) => { + this.data.collections.set(collection.id, { + ...collection, + createdAt: new Date(collection.createdAt) + }); + this.counters.collectionId = Math.max(this.counters.collectionId, collection.id + 1); + }); + } + + // Load collection items + if (jsonData.collectionItems) { + jsonData.collectionItems.forEach((item: CollectionItem) => { + this.data.collectionItems.set(item.id, { + ...item, + createdAt: new Date(item.createdAt) + }); + this.counters.collectionItemId = Math.max(this.counters.collectionItemId, item.id + 1); + }); + } + + // Load timeline events + if (jsonData.timelineEvents) { + jsonData.timelineEvents.forEach((event: TimelineEvent) => { + this.data.timelineEvents.set(event.id, { + ...event, + createdAt: new Date(event.createdAt) + }); + this.counters.timelineEventId = Math.max(this.counters.timelineEventId, event.id + 1); + }); + } + + // Load blog posts + if (jsonData.blogPosts) { + jsonData.blogPosts.forEach((post: BlogPost) => { + this.data.blogPosts.set(post.id, { + ...post, + createdAt: new Date(post.createdAt), + publishedAt: post.publishedAt ? new Date(post.publishedAt) : null + }); + this.counters.blogPostId = Math.max(this.counters.blogPostId, post.id + 1); + }); + } + + // Load reading progresses + if (jsonData.readingProgresses) { + jsonData.readingProgresses.forEach((progress: ReadingProgress) => { + const key = `${progress.userId}-${progress.workId}-${progress.translationId || 0}`; + this.data.readingProgresses.set(key, { + ...progress, + lastReadAt: new Date(progress.lastReadAt) + }); + this.counters.readingProgressId = Math.max(this.counters.readingProgressId, progress.id + 1); + }); + } + + console.log('Data loaded successfully'); + console.log(`Users: ${this.data.users.size}`); + console.log(`Authors: ${this.data.authors.size}`); + console.log(`Works: ${this.data.works.size}`); + console.log(`Translations: ${this.data.translations.size}`); + } catch (error) { + console.error('Error loading data from file:', error); + } + } + + private saveDataToFile() { + try { + const jsonData = { + users: Array.from(this.data.users.values()), + authors: Array.from(this.data.authors.values()), + works: Array.from(this.data.works.values()), + translations: Array.from(this.data.translations.values()), + tags: Array.from(this.data.tags.values()), + workTagsRelations: Object.fromEntries( + Array.from(this.data.workTagsRelations.entries()).map( + ([workId, tagIds]) => [workId, Array.from(tagIds)] + ) + ), + blogPostTagsRelations: Object.fromEntries( + Array.from(this.data.blogPostTagsRelations.entries()).map( + ([postId, tagIds]) => [postId, Array.from(tagIds)] + ) + ), + bookmarks: Array.from(this.data.bookmarks.values()), + comments: Array.from(this.data.comments.values()), + likes: Array.from(this.data.likes.values()), + collections: Array.from(this.data.collections.values()), + collectionItems: Array.from(this.data.collectionItems.values()), + timelineEvents: Array.from(this.data.timelineEvents.values()), + blogPosts: Array.from(this.data.blogPosts.values()), + readingProgresses: Array.from(this.data.readingProgresses.values()) + }; + + fs.writeFileSync(this.dataFilePath, JSON.stringify(jsonData, null, 2), 'utf8'); + console.log(`Data saved to ${this.dataFilePath}`); + } catch (error) { + console.error('Error saving data to file:', error); + } + } + + // User methods + async getUser(id: number): Promise { + return this.data.users.get(id); + } + + async getUserByUsername(username: string): Promise { + return Array.from(this.data.users.values()).find(user => user.username === username); + } + + async createUser(insertUser: InsertUser): Promise { + const id = this.counters.userId++; + const now = new Date(); + const user: User = { + ...insertUser, + id, + role: 'user', + createdAt: now + }; + this.data.users.set(id, user); + this.saveDataToFile(); + return user; + } + + // Author methods + async getAuthors(limit: number = 100, offset: number = 0): Promise { + return Array.from(this.data.authors.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .slice(offset, offset + limit); + } + + async getAuthorById(id: number): Promise { + return this.data.authors.get(id); + } + + async getAuthorBySlug(slug: string): Promise { + return Array.from(this.data.authors.values()).find(author => author.slug === slug); + } + + async createAuthor(insertAuthor: InsertAuthor): Promise { + const id = this.counters.authorId++; + const now = new Date(); + const author: Author = { + ...insertAuthor, + id, + createdAt: now + }; + this.data.authors.set(id, author); + this.saveDataToFile(); + return author; + } + + // Work methods + async getWorks(limit: number = 100, offset: number = 0): Promise { + return Array.from(this.data.works.values()) + .sort((a, b) => a.title.localeCompare(b.title)) + .slice(offset, offset + limit); + } + + async getWorkById(id: number): Promise { + return this.data.works.get(id); + } + + async getWorkBySlug(slug: string): Promise { + return Array.from(this.data.works.values()).find(work => work.slug === slug); + } + + async getWorksByAuthorId(authorId: number): Promise { + return Array.from(this.data.works.values()).filter(work => work.authorId === authorId); + } + + async getWorksByTags(tagIds: number[]): Promise { + const tagSet = new Set(tagIds); + const workIds = new Set(); + + this.data.workTagsRelations.forEach((tags, workId) => { + for (const tagId of tags) { + if (tagSet.has(tagId)) { + workIds.add(workId); + break; + } + } + }); + + return Array.from(workIds).map(id => this.data.works.get(id)!).filter(Boolean); + } + + async createWork(insertWork: InsertWork): Promise { + const id = this.counters.workId++; + const now = new Date(); + const work: Work = { + ...insertWork, + id, + createdAt: now + }; + this.data.works.set(id, work); + this.saveDataToFile(); + return work; + } + + // Translation methods + async getTranslations(workId: number): Promise { + return Array.from(this.data.translations.values()).filter( + translation => translation.workId === workId + ); + } + + async getTranslationById(id: number): Promise { + return this.data.translations.get(id); + } + + async createTranslation(insertTranslation: InsertTranslation): Promise { + const id = this.counters.translationId++; + const now = new Date(); + const translation: Translation = { + ...insertTranslation, + id, + createdAt: now + }; + this.data.translations.set(id, translation); + this.saveDataToFile(); + return translation; + } + + // Tag methods + async getTags(): Promise { + return Array.from(this.data.tags.values()); + } + + async getTagsByType(type: string): Promise { + return Array.from(this.data.tags.values()).filter(tag => tag.type === type); + } + + async createTag(insertTag: InsertTag): Promise { + const id = this.counters.tagId++; + const now = new Date(); + const tag: Tag = { + ...insertTag, + id, + createdAt: now + }; + this.data.tags.set(id, tag); + this.saveDataToFile(); + return tag; + } + + async addTagToWork(workId: number, tagId: number): Promise { + if (!this.data.workTagsRelations.has(workId)) { + this.data.workTagsRelations.set(workId, new Set()); + } + this.data.workTagsRelations.get(workId)!.add(tagId); + this.saveDataToFile(); + } + + async getWorkTags(workId: number): Promise { + const tagIds = this.data.workTagsRelations.get(workId) || new Set(); + return Array.from(tagIds).map(id => this.data.tags.get(id)!).filter(Boolean); + } + + async getBlogPostTags(postId: number): Promise { + const tagIds = this.data.blogPostTagsRelations.get(postId) || new Set(); + return Array.from(tagIds).map(id => this.data.tags.get(id)!).filter(Boolean); + } + + async addTagToBlogPost(postId: number, tagId: number): Promise { + console.log(`Adding tag ${tagId} to blog post ${postId}`); + if (!this.data.blogPostTagsRelations.has(postId)) { + console.log(`Creating new tag set for blog post ${postId}`); + this.data.blogPostTagsRelations.set(postId, new Set()); + } + this.data.blogPostTagsRelations.get(postId)!.add(tagId); + console.log(`After adding tag, blog post ${postId} has tags:`, + Array.from(this.data.blogPostTagsRelations.get(postId) || new Set())); + this.saveDataToFile(); + } + + // Bookmark methods + async getBookmarksByUserId(userId: number): Promise { + return Array.from(this.data.bookmarks.values()).filter(bookmark => bookmark.userId === userId); + } + + async getBookmarkByUserAndWork(userId: number, workId: number): Promise { + return Array.from(this.data.bookmarks.values()).find( + bookmark => bookmark.userId === userId && bookmark.workId === workId + ); + } + + async createBookmark(insertBookmark: InsertBookmark): Promise { + const id = this.counters.bookmarkId++; + const now = new Date(); + const bookmark: Bookmark = { + ...insertBookmark, + id, + createdAt: now + }; + this.data.bookmarks.set(id, bookmark); + this.saveDataToFile(); + return bookmark; + } + + async deleteBookmark(id: number): Promise { + this.data.bookmarks.delete(id); + this.saveDataToFile(); + } + + // Comment methods + async getCommentsByWorkId(workId: number): Promise { + return Array.from(this.data.comments.values()).filter( + comment => comment.workId === workId + ); + } + + async getCommentsByTranslationId(translationId: number): Promise { + return Array.from(this.data.comments.values()).filter( + comment => comment.translationId === translationId + ); + } + + async getCommentsByUserId(userId: number): Promise { + return Array.from(this.data.comments.values()).filter( + comment => comment.userId === userId + ); + } + + async getCommentsByEntityId(entityType: string, entityId: number): Promise { + return Array.from(this.data.comments.values()).filter( + comment => comment.entityType === entityType && comment.entityId === entityId + ); + } + + async createComment(insertComment: InsertComment): Promise { + const id = this.counters.commentId++; + const now = new Date(); + const comment: Comment = { + ...insertComment, + id, + createdAt: now + }; + this.data.comments.set(id, comment); + this.saveDataToFile(); + return comment; + } + + // Like methods + async createLike(insertLike: InsertLike): Promise { + const id = this.counters.likeId++; + const now = new Date(); + const like: Like = { + ...insertLike, + id, + createdAt: now + }; + this.data.likes.set(id, like); + this.saveDataToFile(); + return like; + } + + async deleteLike(id: number): Promise { + this.data.likes.delete(id); + this.saveDataToFile(); + } + + async getLikesByEntity(entityType: string, entityId: number): Promise { + return Array.from(this.data.likes.values()).filter( + like => like.entityType === entityType && like.entityId === entityId + ); + } + + async getLikesByUserId(userId: number): Promise { + return Array.from(this.data.likes.values()).filter( + like => like.userId === userId + ); + } + + // Collection methods + async getCollections(limit: number = 100, offset: number = 0): Promise { + return Array.from(this.data.collections.values()) + .sort((a, b) => a.title.localeCompare(b.title)) + .slice(offset, offset + limit); + } + + async getCollectionsByUserId(userId: number): Promise { + return Array.from(this.data.collections.values()).filter( + collection => collection.userId === userId + ); + } + + async getCollectionById(id: number): Promise { + return this.data.collections.get(id); + } + + async getCollectionBySlug(slug: string): Promise { + return Array.from(this.data.collections.values()).find( + collection => collection.slug === slug + ); + } + + async createCollection(insertCollection: InsertCollection): Promise { + const id = this.counters.collectionId++; + const now = new Date(); + const collection: Collection = { + ...insertCollection, + id, + createdAt: now + }; + this.data.collections.set(id, collection); + this.saveDataToFile(); + return collection; + } + + async addWorkToCollection(insertCollectionItem: InsertCollectionItem): Promise { + const id = this.counters.collectionItemId++; + const now = new Date(); + const collectionItem: CollectionItem = { + ...insertCollectionItem, + id, + createdAt: now + }; + this.data.collectionItems.set(id, collectionItem); + this.saveDataToFile(); + return collectionItem; + } + + async getCollectionItems(collectionId: number): Promise { + return Array.from(this.data.collectionItems.values()).filter( + item => item.collectionId === collectionId + ); + } + + // Timeline events + async getTimelineEventsByAuthorId(authorId: number): Promise { + return Array.from(this.data.timelineEvents.values()) + .filter(event => event.authorId === authorId) + .sort((a, b) => a.year - b.year); + } + + async createTimelineEvent(insertEvent: InsertTimelineEvent): Promise { + const id = this.counters.timelineEventId++; + const now = new Date(); + const event: TimelineEvent = { + ...insertEvent, + id, + createdAt: now + }; + this.data.timelineEvents.set(id, event); + this.saveDataToFile(); + return event; + } + + // Blog posts + async getBlogPosts(limit: number = 100, offset: number = 0): Promise { + return Array.from(this.data.blogPosts.values()) + .sort((a, b) => { + const dateA = a.publishedAt || a.createdAt; + const dateB = b.publishedAt || b.createdAt; + return dateB.getTime() - dateA.getTime(); // newest first + }) + .slice(offset, offset + limit); + } + + async getBlogPostById(id: number): Promise { + return this.data.blogPosts.get(id); + } + + async getBlogPostBySlug(slug: string): Promise { + return Array.from(this.data.blogPosts.values()).find( + post => post.slug === slug + ); + } + + async createBlogPost(insertPost: InsertBlogPost): Promise { + const id = this.counters.blogPostId++; + const now = new Date(); + const post: BlogPost = { + ...insertPost, + id, + createdAt: now + }; + this.data.blogPosts.set(id, post); + console.log('Created blog post with ID:', id); + this.saveDataToFile(); + return post; + } + + // Reading progress + async getReadingProgress(userId: number, workId: number, translationId?: number): Promise { + const key = `${userId}-${workId}-${translationId || 0}`; + return this.data.readingProgresses.get(key); + } + + async updateReadingProgress(insertProgress: InsertReadingProgress): Promise { + const key = `${insertProgress.userId}-${insertProgress.workId}-${insertProgress.translationId || 0}`; + + if (this.data.readingProgresses.has(key)) { + const existing = this.data.readingProgresses.get(key)!; + const updated: ReadingProgress = { + ...existing, + progress: insertProgress.progress ?? existing.progress, + lastReadAt: new Date() + }; + this.data.readingProgresses.set(key, updated); + this.saveDataToFile(); + return updated; + } else { + const id = this.counters.readingProgressId++; + const progress: ReadingProgress = { + id, + workId: insertProgress.workId, + progress: insertProgress.progress ?? 0, + userId: insertProgress.userId, + translationId: insertProgress.translationId ?? null, + lastReadAt: new Date() + }; + this.data.readingProgresses.set(key, progress); + this.saveDataToFile(); + return progress; + } + } + + // Search and filter methods + async searchWorks(query: string, limit: number = 10): Promise { + const searchTerms = query.toLowerCase().split(/\s+/); + const works = Array.from(this.data.works.values()); + + const scoredWorks = works.map(work => { + let score = 0; + const title = work.title.toLowerCase(); + const description = work.description?.toLowerCase() || ''; + + searchTerms.forEach(term => { + if (title.includes(term)) score += 10; + if (description.includes(term)) score += 5; + }); + + return { work, score }; + }); + + return scoredWorks + .filter(item => item.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map(item => item.work); + } + + async searchAuthors(query: string, limit: number = 10): Promise { + const searchTerms = query.toLowerCase().split(/\s+/); + const authors = Array.from(this.data.authors.values()); + + const scoredAuthors = authors.map(author => { + let score = 0; + const name = author.name.toLowerCase(); + const biography = author.biography?.toLowerCase() || ''; + const country = author.country?.toLowerCase() || ''; + + searchTerms.forEach(term => { + if (name.includes(term)) score += 10; + if (country.includes(term)) score += 7; + if (biography.includes(term)) score += 3; + }); + + return { author, score }; + }); + + return scoredAuthors + .filter(item => item.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map(item => item.author); + } + + async filterWorks(params: { + language?: string, + type?: string, + yearStart?: number, + yearEnd?: number, + tags?: number[] + }): Promise { + let filteredWorks = Array.from(this.data.works.values()); + + if (params.language) { + filteredWorks = filteredWorks.filter(work => work.language === params.language); + } + + if (params.type) { + filteredWorks = filteredWorks.filter(work => work.type === params.type); + } + + if (params.yearStart) { + filteredWorks = filteredWorks.filter(work => work.year! >= params.yearStart!); + } + + if (params.yearEnd) { + filteredWorks = filteredWorks.filter(work => work.year! <= params.yearEnd!); + } + + if (params.tags && params.tags.length > 0) { + const taggedWorkIds = new Set(); + this.data.workTagsRelations.forEach((tagIds, workId) => { + for (const tagId of params.tags!) { + if (tagIds.has(tagId)) { + taggedWorkIds.add(workId); + break; + } + } + }); + + filteredWorks = filteredWorks.filter(work => taggedWorkIds.has(work.id)); + } + + return filteredWorks; + } +} \ No newline at end of file diff --git a/server/storage.ts b/server/storage.ts index 4b6101b..1cc3de3 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -1186,4 +1186,6 @@ As we continue to develop the Tercul platform, we're committed to addressing the } } -export const storage = new MemStorage(); +// Now using JsonStorage instead of MemStorage +import { JsonStorage } from './jsonStorage'; +export const storage = new JsonStorage();