tercul-frontend/server/storage.ts
mukimovd 024e5d0ef5 Introduce the core functionality and basic structure of the platform
Sets up the project with initial files, components, routes, and UI elements.

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/affc56b0-365e-4ece-9cba-9e70bbbf0893.jpg
2025-05-01 03:05:33 +00:00

714 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
users, User, InsertUser,
authors, Author, InsertAuthor,
works, Work, InsertWork,
translations, Translation, InsertTranslation,
tags, Tag, InsertTag,
workTags,
bookmarks, Bookmark, InsertBookmark,
comments, Comment, InsertComment,
likes, Like, InsertLike,
collections, Collection, InsertCollection,
collectionItems, CollectionItem, InsertCollectionItem,
timelineEvents, TimelineEvent, InsertTimelineEvent,
blogPosts, BlogPost, InsertBlogPost,
readingProgress, ReadingProgress, InsertReadingProgress
} from "@shared/schema";
export interface IStorage {
// User methods
getUser(id: number): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
// Author methods
getAuthors(limit?: number, offset?: number): Promise<Author[]>;
getAuthorById(id: number): Promise<Author | undefined>;
getAuthorBySlug(slug: string): Promise<Author | undefined>;
createAuthor(author: InsertAuthor): Promise<Author>;
// Work methods
getWorks(limit?: number, offset?: number): Promise<Work[]>;
getWorkById(id: number): Promise<Work | undefined>;
getWorkBySlug(slug: string): Promise<Work | undefined>;
getWorksByAuthorId(authorId: number): Promise<Work[]>;
getWorksByTags(tagIds: number[]): Promise<Work[]>;
createWork(work: InsertWork): Promise<Work>;
// Translation methods
getTranslations(workId: number): Promise<Translation[]>;
getTranslationById(id: number): Promise<Translation | undefined>;
createTranslation(translation: InsertTranslation): Promise<Translation>;
// Tag methods
getTags(): Promise<Tag[]>;
getTagsByType(type: string): Promise<Tag[]>;
createTag(tag: InsertTag): Promise<Tag>;
addTagToWork(workId: number, tagId: number): Promise<void>;
getWorkTags(workId: number): Promise<Tag[]>;
// Bookmark methods
getBookmarksByUserId(userId: number): Promise<Bookmark[]>;
getBookmarkByUserAndWork(userId: number, workId: number): Promise<Bookmark | undefined>;
createBookmark(bookmark: InsertBookmark): Promise<Bookmark>;
deleteBookmark(id: number): Promise<void>;
// Comment methods
getCommentsByWorkId(workId: number): Promise<Comment[]>;
getCommentsByTranslationId(translationId: number): Promise<Comment[]>;
getCommentsByUserId(userId: number): Promise<Comment[]>;
createComment(comment: InsertComment): Promise<Comment>;
// Like methods
createLike(like: InsertLike): Promise<Like>;
deleteLike(id: number): Promise<void>;
getLikesByEntity(entityType: string, entityId: number): Promise<Like[]>;
getLikesByUserId(userId: number): Promise<Like[]>;
// Collection methods
getCollections(limit?: number, offset?: number): Promise<Collection[]>;
getCollectionsByUserId(userId: number): Promise<Collection[]>;
getCollectionById(id: number): Promise<Collection | undefined>;
getCollectionBySlug(slug: string): Promise<Collection | undefined>;
createCollection(collection: InsertCollection): Promise<Collection>;
addWorkToCollection(collectionItem: InsertCollectionItem): Promise<CollectionItem>;
getCollectionItems(collectionId: number): Promise<CollectionItem[]>;
// Timeline events
getTimelineEventsByAuthorId(authorId: number): Promise<TimelineEvent[]>;
createTimelineEvent(event: InsertTimelineEvent): Promise<TimelineEvent>;
// Blog posts
getBlogPosts(limit?: number, offset?: number): Promise<BlogPost[]>;
getBlogPostById(id: number): Promise<BlogPost | undefined>;
getBlogPostBySlug(slug: string): Promise<BlogPost | undefined>;
createBlogPost(post: InsertBlogPost): Promise<BlogPost>;
// Reading progress
getReadingProgress(userId: number, workId: number, translationId?: number): Promise<ReadingProgress | undefined>;
updateReadingProgress(progress: InsertReadingProgress): Promise<ReadingProgress>;
// Search and filter methods
searchWorks(query: string, limit?: number): Promise<Work[]>;
searchAuthors(query: string, limit?: number): Promise<Author[]>;
filterWorks(params: { language?: string, type?: string, yearStart?: number, yearEnd?: number, tags?: number[] }): Promise<Work[]>;
}
export class MemStorage implements IStorage {
private users: Map<number, User>;
private authors: Map<number, Author>;
private works: Map<number, Work>;
private translations: Map<number, Translation>;
private tags: Map<number, Tag>;
private workTagsRelations: Map<number, Set<number>>;
private bookmarks: Map<number, Bookmark>;
private comments: Map<number, Comment>;
private likes: Map<number, Like>;
private collections: Map<number, Collection>;
private collectionItems: Map<number, CollectionItem>;
private timelineEvents: Map<number, TimelineEvent>;
private blogPosts: Map<number, BlogPost>;
private readingProgresses: Map<string, ReadingProgress>;
private userId: number = 1;
private authorId: number = 1;
private workId: number = 1;
private translationId: number = 1;
private tagId: number = 1;
private bookmarkId: number = 1;
private commentId: number = 1;
private likeId: number = 1;
private collectionId: number = 1;
private collectionItemId: number = 1;
private timelineEventId: number = 1;
private blogPostId: number = 1;
private readingProgressId: number = 1;
constructor() {
this.users = new Map();
this.authors = new Map();
this.works = new Map();
this.translations = new Map();
this.tags = new Map();
this.workTagsRelations = new Map();
this.bookmarks = new Map();
this.comments = new Map();
this.likes = new Map();
this.collections = new Map();
this.collectionItems = new Map();
this.timelineEvents = new Map();
this.blogPosts = new Map();
this.readingProgresses = new Map();
// Initialize with some data
this.initializeData();
}
private initializeData() {
// Create some sample users
const user1 = this.createUser({
username: "admin",
password: "password",
email: "admin@example.com",
displayName: "Administrator"
});
// Create sample authors
const pushkin = this.createAuthor({
name: "Alexander Pushkin",
slug: "alexander-pushkin",
birthYear: 1799,
deathYear: 1837,
country: "Russia",
biography: "Russian poet, playwright, and novelist of the Romantic era. He is considered by many to be the greatest Russian poet and the founder of modern Russian literature.",
portrait: "https://images.unsplash.com/photo-1541688329375-fd29c1b13276?ixlib=rb-1.2.1&auto=format&fit=crop&w=256&h=256&q=80",
createdBy: user1.id
});
// Create sample tags
const lyricTag = this.createTag({ name: "Lyric", type: "genre" });
const loveTag = this.createTag({ name: "Love", type: "theme" });
const poetryTag = this.createTag({ name: "Poetry", type: "genre" });
const romanticismTag = this.createTag({ name: "Romanticism", type: "period" });
const novelTag = this.createTag({ name: "Novel", type: "genre" });
const verseTag = this.createTag({ name: "Verse", type: "form" });
const historyTag = this.createTag({ name: "Historical", type: "theme" });
const gotihicTag = this.createTag({ name: "Gothic", type: "genre" });
const fictionTag = this.createTag({ name: "Fiction", type: "genre" });
// Create sample works
const lovedYouWork = this.createWork({
title: "I Loved You",
slug: "i-loved-you",
authorId: pushkin.id,
year: 1829,
type: "poem",
language: "Russian",
content: "Я вас любил: любовь еще, быть может,\nВ душе моей угасла не совсем;\nНо пусть она вас больше не тревожит;\nЯ не хочу печалить вас ничем.\nЯ вас любил безмолвно, безнадежно,\nТо робостью, то ревностью томим;\nЯ вас любил так искренно, так нежно,\nКак дай вам Бог любимой быть другим.",
description: "A short lyrical poem expressing unselfish love and one of Pushkin's most famous works. The poem describes the narrator's admiration for a beloved from whom he has parted.",
createdBy: user1.id
});
this.addTagToWork(lovedYouWork.id, lyricTag.id);
this.addTagToWork(lovedYouWork.id, loveTag.id);
this.addTagToWork(lovedYouWork.id, poetryTag.id);
const eugeneOnegin = this.createWork({
title: "Eugene Onegin",
slug: "eugene-onegin",
authorId: pushkin.id,
year: 1833,
type: "poem",
language: "Russian",
content: "Sample content for Eugene Onegin...",
description: "A classic of Russian literature, this novel in verse follows the story of Eugene Onegin, a bored aristocrat who rejects the love of Tatyana and later comes to regret it.",
createdBy: user1.id
});
this.addTagToWork(eugeneOnegin.id, romanticismTag.id);
this.addTagToWork(eugeneOnegin.id, novelTag.id);
this.addTagToWork(eugeneOnegin.id, verseTag.id);
const bronzeHorseman = this.createWork({
title: "The Bronze Horseman",
slug: "the-bronze-horseman",
authorId: pushkin.id,
year: 1833,
type: "poem",
language: "Russian",
content: "Sample content for The Bronze Horseman...",
description: "A narrative poem depicting the equestrian statue of Peter the Great in Saint Petersburg and the great flood of 1824, widely considered one of Pushkin's masterpieces.",
createdBy: user1.id
});
this.addTagToWork(bronzeHorseman.id, poetryTag.id);
this.addTagToWork(bronzeHorseman.id, historyTag.id);
const queenOfSpades = this.createWork({
title: "The Queen of Spades",
slug: "the-queen-of-spades",
authorId: pushkin.id,
year: 1834,
type: "story",
language: "Russian",
content: "Sample content for The Queen of Spades...",
description: "A short story about human avarice, telling the tale of a young officer seeking the secret of an elderly countess's success at faro, a card game.",
createdBy: user1.id
});
this.addTagToWork(queenOfSpades.id, fictionTag.id);
this.addTagToWork(queenOfSpades.id, gotihicTag.id);
// Create sample translations
this.createTranslation({
workId: lovedYouWork.id,
title: "I Loved You",
language: "English",
content: "I loved you, and I probably still do,\nAnd for a while the feeling may remain...\nBut let my love no longer trouble you,\nI do not wish to cause you any pain.\nI loved you silently, without hope,\nAt times too jealous and at times too shy.\nI loved you truly, with a tender scope,\nAs may God grant you to be loved now by.",
translatorId: user1.id,
year: 2018,
notes: "Translated by John Fenwick in 2018. This translation attempts to preserve the rhyme scheme (ABAB CDCD) and structure of the original while conveying the tender sentiment of Pushkin's lyric."
});
// Create timeline events
this.createTimelineEvent({
authorId: pushkin.id,
year: 1799,
title: "Birth",
description: "Born in Moscow to Sergei Lvovich and Nadezhda Ossipovna Pushkin"
});
this.createTimelineEvent({
authorId: pushkin.id,
year: 1811,
title: "Education",
description: "Enrolled at the Imperial Lyceum at Tsarskoye Selo"
});
this.createTimelineEvent({
authorId: pushkin.id,
year: 1820,
title: "First major work",
description: "Published his first long poem 'Ruslan and Ludmila'"
});
this.createTimelineEvent({
authorId: pushkin.id,
year: 1837,
title: "Death",
description: "Died in St. Petersburg from wounds received in a duel with Georges d'Anthès"
});
}
// User methods
async getUser(id: number): Promise<User | undefined> {
return this.users.get(id);
}
async getUserByUsername(username: string): Promise<User | undefined> {
return Array.from(this.users.values()).find(user => user.username === username);
}
async createUser(insertUser: InsertUser): Promise<User> {
const id = this.userId++;
const now = new Date();
const user: User = {
...insertUser,
id,
role: 'user',
createdAt: now
};
this.users.set(id, user);
return user;
}
// Author methods
async getAuthors(limit: number = 100, offset: number = 0): Promise<Author[]> {
return Array.from(this.authors.values())
.sort((a, b) => a.name.localeCompare(b.name))
.slice(offset, offset + limit);
}
async getAuthorById(id: number): Promise<Author | undefined> {
return this.authors.get(id);
}
async getAuthorBySlug(slug: string): Promise<Author | undefined> {
return Array.from(this.authors.values()).find(author => author.slug === slug);
}
async createAuthor(insertAuthor: InsertAuthor): Promise<Author> {
const id = this.authorId++;
const now = new Date();
const author: Author = {
...insertAuthor,
id,
createdAt: now
};
this.authors.set(id, author);
return author;
}
// Work methods
async getWorks(limit: number = 100, offset: number = 0): Promise<Work[]> {
return Array.from(this.works.values())
.sort((a, b) => a.title.localeCompare(b.title))
.slice(offset, offset + limit);
}
async getWorkById(id: number): Promise<Work | undefined> {
return this.works.get(id);
}
async getWorkBySlug(slug: string): Promise<Work | undefined> {
return Array.from(this.works.values()).find(work => work.slug === slug);
}
async getWorksByAuthorId(authorId: number): Promise<Work[]> {
return Array.from(this.works.values()).filter(work => work.authorId === authorId);
}
async getWorksByTags(tagIds: number[]): Promise<Work[]> {
const tagSet = new Set(tagIds);
const workIds = new Set<number>();
this.workTagsRelations.forEach((tags, workId) => {
for (const tagId of tags) {
if (tagSet.has(tagId)) {
workIds.add(workId);
break;
}
}
});
return Array.from(workIds).map(id => this.works.get(id)!).filter(Boolean);
}
async createWork(insertWork: InsertWork): Promise<Work> {
const id = this.workId++;
const now = new Date();
const work: Work = {
...insertWork,
id,
createdAt: now
};
this.works.set(id, work);
return work;
}
// Translation methods
async getTranslations(workId: number): Promise<Translation[]> {
return Array.from(this.translations.values()).filter(
translation => translation.workId === workId
);
}
async getTranslationById(id: number): Promise<Translation | undefined> {
return this.translations.get(id);
}
async createTranslation(insertTranslation: InsertTranslation): Promise<Translation> {
const id = this.translationId++;
const now = new Date();
const translation: Translation = {
...insertTranslation,
id,
createdAt: now
};
this.translations.set(id, translation);
return translation;
}
// Tag methods
async getTags(): Promise<Tag[]> {
return Array.from(this.tags.values());
}
async getTagsByType(type: string): Promise<Tag[]> {
return Array.from(this.tags.values()).filter(tag => tag.type === type);
}
async createTag(insertTag: InsertTag): Promise<Tag> {
const id = this.tagId++;
const now = new Date();
const tag: Tag = {
...insertTag,
id,
createdAt: now
};
this.tags.set(id, tag);
return tag;
}
async addTagToWork(workId: number, tagId: number): Promise<void> {
if (!this.workTagsRelations.has(workId)) {
this.workTagsRelations.set(workId, new Set());
}
this.workTagsRelations.get(workId)!.add(tagId);
}
async getWorkTags(workId: number): Promise<Tag[]> {
const tagIds = this.workTagsRelations.get(workId) || new Set();
return Array.from(tagIds).map(id => this.tags.get(id)!).filter(Boolean);
}
// Bookmark methods
async getBookmarksByUserId(userId: number): Promise<Bookmark[]> {
return Array.from(this.bookmarks.values()).filter(
bookmark => bookmark.userId === userId
);
}
async getBookmarkByUserAndWork(userId: number, workId: number): Promise<Bookmark | undefined> {
return Array.from(this.bookmarks.values()).find(
bookmark => bookmark.userId === userId && bookmark.workId === workId
);
}
async createBookmark(insertBookmark: InsertBookmark): Promise<Bookmark> {
const id = this.bookmarkId++;
const now = new Date();
const bookmark: Bookmark = {
...insertBookmark,
id,
createdAt: now
};
this.bookmarks.set(id, bookmark);
return bookmark;
}
async deleteBookmark(id: number): Promise<void> {
this.bookmarks.delete(id);
}
// Comment methods
async getCommentsByWorkId(workId: number): Promise<Comment[]> {
return Array.from(this.comments.values()).filter(
comment => comment.workId === workId
);
}
async getCommentsByTranslationId(translationId: number): Promise<Comment[]> {
return Array.from(this.comments.values()).filter(
comment => comment.translationId === translationId
);
}
async getCommentsByUserId(userId: number): Promise<Comment[]> {
return Array.from(this.comments.values()).filter(
comment => comment.userId === userId
);
}
async createComment(insertComment: InsertComment): Promise<Comment> {
const id = this.commentId++;
const now = new Date();
const comment: Comment = {
...insertComment,
id,
createdAt: now
};
this.comments.set(id, comment);
return comment;
}
// Like methods
async createLike(insertLike: InsertLike): Promise<Like> {
const id = this.likeId++;
const now = new Date();
const like: Like = {
...insertLike,
id,
createdAt: now
};
this.likes.set(id, like);
return like;
}
async deleteLike(id: number): Promise<void> {
this.likes.delete(id);
}
async getLikesByEntity(entityType: string, entityId: number): Promise<Like[]> {
return Array.from(this.likes.values()).filter(
like => like.entityType === entityType && like.entityId === entityId
);
}
async getLikesByUserId(userId: number): Promise<Like[]> {
return Array.from(this.likes.values()).filter(
like => like.userId === userId
);
}
// Collection methods
async getCollections(limit: number = 100, offset: number = 0): Promise<Collection[]> {
return Array.from(this.collections.values())
.filter(collection => collection.isPublic)
.sort((a, b) => a.name.localeCompare(b.name))
.slice(offset, offset + limit);
}
async getCollectionsByUserId(userId: number): Promise<Collection[]> {
return Array.from(this.collections.values()).filter(
collection => collection.userId === userId
);
}
async getCollectionById(id: number): Promise<Collection | undefined> {
return this.collections.get(id);
}
async getCollectionBySlug(slug: string): Promise<Collection | undefined> {
return Array.from(this.collections.values()).find(
collection => collection.slug === slug
);
}
async createCollection(insertCollection: InsertCollection): Promise<Collection> {
const id = this.collectionId++;
const now = new Date();
const collection: Collection = {
...insertCollection,
id,
createdAt: now
};
this.collections.set(id, collection);
return collection;
}
async addWorkToCollection(insertCollectionItem: InsertCollectionItem): Promise<CollectionItem> {
const id = this.collectionItemId++;
const now = new Date();
const collectionItem: CollectionItem = {
...insertCollectionItem,
id,
addedAt: now
};
this.collectionItems.set(id, collectionItem);
return collectionItem;
}
async getCollectionItems(collectionId: number): Promise<CollectionItem[]> {
return Array.from(this.collectionItems.values())
.filter(item => item.collectionId === collectionId)
.sort((a, b) => a.order - b.order);
}
// Timeline events
async getTimelineEventsByAuthorId(authorId: number): Promise<TimelineEvent[]> {
return Array.from(this.timelineEvents.values())
.filter(event => event.authorId === authorId)
.sort((a, b) => a.year - b.year);
}
async createTimelineEvent(insertEvent: InsertTimelineEvent): Promise<TimelineEvent> {
const id = this.timelineEventId++;
const now = new Date();
const event: TimelineEvent = {
...insertEvent,
id,
createdAt: now
};
this.timelineEvents.set(id, event);
return event;
}
// Blog posts
async getBlogPosts(limit: number = 100, offset: number = 0): Promise<BlogPost[]> {
return Array.from(this.blogPosts.values())
.filter(post => post.publishedAt !== null)
.sort((a, b) => (b.publishedAt?.getTime() || 0) - (a.publishedAt?.getTime() || 0))
.slice(offset, offset + limit);
}
async getBlogPostById(id: number): Promise<BlogPost | undefined> {
return this.blogPosts.get(id);
}
async getBlogPostBySlug(slug: string): Promise<BlogPost | undefined> {
return Array.from(this.blogPosts.values()).find(post => post.slug === slug);
}
async createBlogPost(insertPost: InsertBlogPost): Promise<BlogPost> {
const id = this.blogPostId++;
const now = new Date();
const post: BlogPost = {
...insertPost,
id,
createdAt: now
};
this.blogPosts.set(id, post);
return post;
}
// Reading progress
async getReadingProgress(userId: number, workId: number, translationId?: number): Promise<ReadingProgress | undefined> {
const key = `${userId}-${workId}-${translationId || 0}`;
return this.readingProgresses.get(key);
}
async updateReadingProgress(insertProgress: InsertReadingProgress): Promise<ReadingProgress> {
const key = `${insertProgress.userId}-${insertProgress.workId}-${insertProgress.translationId || 0}`;
const now = new Date();
const existing = this.readingProgresses.get(key);
if (existing) {
const updated: ReadingProgress = {
...existing,
progress: insertProgress.progress,
lastReadAt: now
};
this.readingProgresses.set(key, updated);
return updated;
} else {
const id = this.readingProgressId++;
const progress: ReadingProgress = {
...insertProgress,
id,
lastReadAt: now
};
this.readingProgresses.set(key, progress);
return progress;
}
}
// Search and filter methods
async searchWorks(query: string, limit: number = 10): Promise<Work[]> {
const lowerQuery = query.toLowerCase();
return Array.from(this.works.values())
.filter(work =>
work.title.toLowerCase().includes(lowerQuery) ||
work.description?.toLowerCase().includes(lowerQuery)
)
.slice(0, limit);
}
async searchAuthors(query: string, limit: number = 10): Promise<Author[]> {
const lowerQuery = query.toLowerCase();
return Array.from(this.authors.values())
.filter(author =>
author.name.toLowerCase().includes(lowerQuery) ||
author.biography?.toLowerCase().includes(lowerQuery)
)
.slice(0, limit);
}
async filterWorks(params: {
language?: string,
type?: string,
yearStart?: number,
yearEnd?: number,
tags?: number[]
}): Promise<Work[]> {
let filtered = Array.from(this.works.values());
if (params.language) {
filtered = filtered.filter(work => work.language === params.language);
}
if (params.type) {
filtered = filtered.filter(work => work.type === params.type);
}
if (params.yearStart) {
filtered = filtered.filter(work => work.year !== undefined && work.year >= (params.yearStart || 0));
}
if (params.yearEnd) {
filtered = filtered.filter(work => work.year !== undefined && work.year <= (params.yearEnd || 3000));
}
if (params.tags && params.tags.length > 0) {
const taggedWorks = await this.getWorksByTags(params.tags);
const taggedWorkIds = new Set(taggedWorks.map(work => work.id));
filtered = filtered.filter(work => taggedWorkIds.has(work.id));
}
return filtered;
}
}
export const storage = new MemStorage();