1+ import { randomBytes } from 'node:crypto'
2+ import readline from 'readline'
3+
4+ const PREVIOUS_USERS_BY_ID = {
5+ "a1b2c3d4" : {
6+ username : "carlos" ,
7+ posts : [ "p1k2l3m4" , "p0j1k2l3" , "p9i0j1k2" , "p8h9i0j1" , "p7g8h9i0" , "p6f7g8h9" , "p5e6f7g8" , "p4d5e6f7" , "p3c4d5e6" , "p2b3c4d5" , "p1a2b3c4" ] ,
8+ followings : [ "e5f6g7h8" , "i9j0k1l2" , "m3n4o5p6" , "f8g9h0i1" ] ,
9+ likedPosts : [ "p22a2b3c" , "p33a2b3c" , "p44a2b3c" , "p55k2l3m" , "p56l3m4n" ]
10+ } ,
11+ "e5f6g7h8" : {
12+ username : "ana" ,
13+ posts : [ "p23b3c4d" , "p22a2b3c" ] ,
14+ followings : [ "a1b2c3d4" , "m3n4o5p6" , "f8g9h0i1" ] ,
15+ likedPosts : [ "p1a2b3c4" , "p9i0j1k2" , "p46c4d5e" , "p52i0j1k" ]
16+ } ,
17+ "i9j0k1l2" : {
18+ username : "luis" ,
19+ posts : [ "p34b3c4d" , "p33a2b3c" ] ,
20+ followings : [ "a1b2c3d4" , "e5f6g7h8" , "f8g9h0i1" ] ,
21+ likedPosts : [ "p2b3c4d5" , "p23b3c4d" , "p55k2l3m" , "p1k2l3m4" ]
22+ } ,
23+ "m3n4o5p6" : {
24+ username : "sofia" ,
25+ posts : [ "p54k2l3m" , "p53j1k2l" , "p52i0j1k" , "p51h9i0j" , "p50g8h9i" , "p49f7g8h" , "p48e6f7g" , "p47d5e6f" , "p46c4d5e" , "p45b3c4d" , "p44a2b3c" ] ,
26+ followings : [ "a1b2c3d4" , "e5f6g7h8" , "i9j0k1l2" , "f8g9h0i1" ] ,
27+ likedPosts : [ "p1a2b3c4" , "p4d5e6f7" , "p22a2b3c" , "p34b3c4d" , "p56l3m4n" ]
28+ } ,
29+ "f8g9h0i1" : {
30+ username : "diego" ,
31+ posts : [ "p56l3m4n" , "p55k2l3m" ] ,
32+ followings : [ "a1b2c3d4" , "m3n4o5p6" , "i9j0k1l2" ] ,
33+ likedPosts : [ "p9i0j1k2" , "p44a2b3c" , "p52i0j1k" , "p1k2l3m4" , "p33a2b3c" ]
34+ } ,
35+ }
36+
37+ const PREVIOUS_USERS_BY_USERNAME = {
38+ "carlos" : "a1b2c3d4" ,
39+ "ana" : "e5f6g7h8" ,
40+ "luis" : "i9j0k1l2" ,
41+ "sofia" : "m3n4o5p6" ,
42+ "diego" : "f8g9h0i1"
43+ }
44+
45+ const PREVIOUS_POSTS = {
46+ "p1k2l3m4" : { text : "¡Viernes al fin!" , date : "2022-10-21T17:00:00Z" , userId : "a1b2c3d4" , likes : 2 } ,
47+ "p0j1k2l3" : { text : "Probando un nuevo restaurante coreano." , date : "2022-10-21T13:00:00Z" , userId : "a1b2c3d4" , likes : 0 } ,
48+ "p9i0j1k2" : { text : "Nueva configuración de escritorio lista." , date : "2022-10-20T11:10:00Z" , userId : "a1b2c3d4" , likes : 2 } ,
49+ "p8h9i0j1" : { text : "Extrañando las vacaciones." , date : "2022-10-19T14:20:00Z" , userId : "a1b2c3d4" , likes : 0 } ,
50+ "p7g8h9i0" : { text : "Cocinando pasta para la cena." , date : "2022-10-18T20:45:00Z" , userId : "a1b2c3d4" , likes : 0 } ,
51+ "p6f7g8h9" : { text : "Viendo una película clásica." , date : "2022-10-17T21:30:00Z" , userId : "a1b2c3d4" , likes : 0 } ,
52+ "p5e6f7g8" : { text : "El clima está increíble hoy." , date : "2022-10-16T12:00:00Z" , userId : "a1b2c3d4" , likes : 0 } ,
53+ "p4d5e6f7" : { text : "Hoy salí a correr 5km." , date : "2022-10-15T07:00:00Z" , userId : "a1b2c3d4" , likes : 1 } ,
54+ "p3c4d5e6" : { text : "¿Alguien recomienda un buen libro?" , date : "2022-10-14T18:00:00Z" , userId : "a1b2c3d4" , likes : 0 } ,
55+ "p2b3c4d5" : { text : "Aprendiendo Node.js desde cero." , date : "2022-10-13T10:15:00Z" , userId : "a1b2c3d4" , likes : 1 } ,
56+ "p1a2b3c4" : { text : "Disfrutando de un café por la mañana." , date : "2022-10-12T08:30:00Z" , userId : "a1b2c3d4" , likes : 2 } ,
57+ "p23b3c4d" : { text : "Mi gata no me deja trabajar." , date : "2024-01-16T10:00:00Z" , userId : "e5f6g7h8" , likes : 1 } ,
58+ "p22a2b3c" : { text : "El viaje a la montaña fue espectacular." , date : "2024-01-15T09:15:22Z" , userId : "e5f6g7h8" , likes : 2 } ,
59+ "p34b3c4d" : { text : "Terminando mi primer proyecto con React." , date : "2026-03-21T15:30:00Z" , userId : "i9j0k1l2" , likes : 1 } ,
60+ "p33a2b3c" : { text : "La inteligencia artificial es fascinante." , date : "2026-03-20T11:05:44Z" , userId : "i9j0k1l2" , likes : 2 } ,
61+ "p54k2l3m" : { text : "Feliz semana a todos." , date : "2015-05-20T08:00:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
62+ "p53j1k2l" : { text : "Escuchando jazz." , date : "2015-05-19T20:00:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
63+ "p52i0j1k" : { text : "Pintando con acuarelas." , date : "2015-05-18T17:45:00Z" , userId : "m3n4o5p6" , likes : 2 } ,
64+ "p51h9i0j" : { text : "Nuevo lienzo en blanco." , date : "2015-05-17T11:30:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
65+ "p50g8h9i" : { text : "Paz mental." , date : "2015-05-16T14:00:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
66+ "p49f7g8h" : { text : "Mi jardín está floreciendo." , date : "2015-05-15T09:00:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
67+ "p48e6f7g" : { text : "Mirando las estrellas." , date : "2015-05-14T22:00:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
68+ "p47d5e6f" : { text : "El té de jazmín es lo mejor." , date : "2015-05-13T10:10:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
69+ "p46c4d5e" : { text : "Leyendo bajo la lluvia." , date : "2015-05-12T16:20:00Z" , userId : "m3n4o5p6" , likes : 1 } ,
70+ "p45b3c4d" : { text : "Caminata nocturna." , date : "2015-05-11T23:00:00Z" , userId : "m3n4o5p6" , likes : 0 } ,
71+ "p44a2b3c" : { text : "Un pequeño poema para el alma." , date : "2015-05-10T22:45:10Z" , userId : "m3n4o5p6" , likes : 2 } ,
72+ "p56l3m4n" : { text : "La disciplina vence al talento." , date : "2025-02-28T09:30:00Z" , userId : "f8g9h0i1" , likes : 2 } ,
73+ "p55k2l3m" : { text : "Entrenando duro para el maratón." , date : "2025-02-27T08:00:15Z" , userId : "f8g9h0i1" , likes : 2 }
74+ }
75+
76+ const rl = readline . createInterface ( {
77+ input : process . stdin ,
78+ output : process . stdout
79+ } )
80+
81+ const createErrorMsg = msg => {
82+ return `\n****************************************************************\n` +
83+ `Error: ${ msg } ` +
84+ `\n****************************************************************\n`
85+ }
86+ const createSuccessMsg = msg => {
87+ return `\n----------------------------------------------------------------\n` +
88+ `Success: ${ msg } ` +
89+ `\n----------------------------------------------------------------\n`
90+ }
91+ const createMenuMsg = ( ) => {
92+ return "--------------------- ACTIONS ----------------------\n" +
93+ "| 1. Register user 2. Follow user |\n" +
94+ "| 3. Unfollow user 4. Create post |\n" +
95+ "| 5. Delete post 6. Like post |\n" +
96+ "| 7. Unlike post 8. View user feed |\n" +
97+ "| 9. View followings feed 10. Exit |\n" +
98+ "----------------------------------------------------"
99+ }
100+
101+ const createPostMsg = info => {
102+ return `~~~~~~~~~~~~~~~~~~~~~ POST ~~~~~~~~~~~~~~~~~~~~~~\n` +
103+ `${ info . text } \n` +
104+ `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n` +
105+ `Date: ${ new Date ( info . date ) . toLocaleString ( ) } Likes:${ info . likes } \n` +
106+ `User: ${ info . username } Post ID: ${ info . postId } \n` +
107+ `User ID: ${ info . userId } \n`
108+ }
109+
110+ const ask = q => new Promise ( r => rl . question ( q , r ) )
111+
112+ class Renderer {
113+ static error ( msg ) {
114+ console . error ( createErrorMsg ( msg ) )
115+ }
116+
117+ static success ( msg ) {
118+ console . log ( createSuccessMsg ( msg ) )
119+ }
120+
121+ static menu ( ) {
122+ console . log ( createMenuMsg ( ) )
123+ }
124+
125+ static post ( info ) {
126+ console . log ( createPostMsg ( info ) )
127+ }
128+ }
129+
130+ class Validator {
131+ static isValidUsername ( username ) {
132+ if ( ! username ) throw new Error ( "The username cannot be empty" )
133+ return username
134+ }
135+
136+ static isValidText ( text ) {
137+ if ( ! text || text . length > 200 ) throw new Error ( "The text is invalid" )
138+ return text
139+ }
140+ }
141+
142+ class IdHelper {
143+ static createId ( ) { return randomBytes ( 4 ) . toString ( "hex" ) }
144+
145+ static getValidId ( usersIds , postsId ) {
146+ let i = 0
147+
148+ while ( i < 10 ) {
149+ const id = IdHelper . createId ( )
150+ if ( ! usersIds [ id ] && ! postsId [ id ] ) return id
151+ i ++
152+ }
153+
154+ throw new Error ( "The ID could not be created correctly. Please try again later." )
155+ }
156+ }
157+
158+ class FeedHelper {
159+ static getPostsId ( usersId , users ) {
160+ const postsId = usersId . flatMap ( userId => users [ userId ] . posts )
161+ return postsId
162+ }
163+
164+ static getOrderPosts ( postsId , posts ) {
165+ const orderPostsId = [ ...postsId ] . sort ( ( postIdOne , postIdTwo ) => {
166+ if ( posts [ postIdOne ] . date > posts [ postIdTwo ] . date ) return - 1
167+ return 1
168+ } ) . slice ( 0 , 10 )
169+ return orderPostsId
170+ }
171+ }
172+
173+ class InputHelper {
174+ static async getExistingUsername ( users ) {
175+ const username = Validator . isValidUsername ( ( await ask ( "Enter the username: " ) ) . trim ( ) )
176+ if ( ! users [ username ] ) throw new Error ( "The username does not exists" )
177+ return username
178+ }
179+
180+ static async getExistingPostId ( postsId ) {
181+ const id = ( await ask ( "Enter the post id: " ) ) . trim ( )
182+ if ( ! postsId [ id ] ) throw new Error ( "There is no post with this ID" )
183+ return id
184+ }
185+ }
186+
187+ class User {
188+ constructor ( username ) {
189+ this . username = username
190+ this . posts = [ ]
191+ this . followings = [ ]
192+ this . likedPosts = [ ]
193+ }
194+ }
195+
196+ class Post {
197+ constructor ( userId , text ) {
198+ this . text = text
199+ this . userId = userId
200+ this . date = new Date ( )
201+ this . likes = 0
202+ }
203+ }
204+
205+ class UserService {
206+ constructor ( usersById , usersByUsername ) {
207+ this . usersById = usersById
208+ this . usersByUsername = usersByUsername
209+ }
210+
211+ registerUser ( userId , username ) {
212+ this . usersById [ userId ] = new User ( username )
213+ this . usersByUsername [ username ] = userId
214+ return Renderer . success ( "The user has successfully registered" )
215+ }
216+
217+ followUser ( sourceUserId , targetUserId ) {
218+ this . usersById [ sourceUserId ] . followings . push ( targetUserId )
219+ return Renderer . success ( `${ this . usersById [ sourceUserId ] . username } started following ${ this . usersById [ targetUserId ] . username } ` )
220+ }
221+
222+ unfollowUser ( sourceUserId , targetUserId ) {
223+ this . usersById [ sourceUserId ] . followings = this . usersById [ sourceUserId ] . followings . filter ( fId => fId !== targetUserId )
224+ return Renderer . success ( `${ this . usersById [ sourceUserId ] . username } has unfollowing ${ this . usersById [ targetUserId ] . username } ` )
225+ }
226+ }
227+
228+ class PostService {
229+ constructor ( usersById , posts ) {
230+ this . usersById = usersById
231+ this . posts = posts
232+ }
233+
234+ createPost ( userId , postId , text ) {
235+ this . posts [ postId ] = new Post ( userId , text )
236+ this . usersById [ userId ] . posts . push ( postId )
237+ return Renderer . success ( "The post has successfully crated" )
238+ }
239+
240+ deletePost ( userId , postId ) {
241+ for ( const userInfo of Object . values ( this . usersById ) ) {
242+ if ( userInfo . likedPosts . includes ( postId ) ) userInfo . likedPosts = userInfo . likedPosts . filter ( pId => pId !== postId )
243+ }
244+ delete this . posts [ postId ]
245+ this . usersById [ userId ] . posts = this . usersById [ userId ] . posts . filter ( pId => pId !== postId )
246+ return Renderer . success ( "The post has successfully deleted" )
247+ }
248+
249+ likePost ( userId , postId ) {
250+ this . posts [ postId ] . likes ++
251+ this . usersById [ userId ] . likedPosts . push ( postId )
252+ return Renderer . success ( `${ this . usersById [ userId ] . username } has liked the post` )
253+ }
254+
255+ unlikePost ( userId , postId ) {
256+ this . posts [ postId ] . likes --
257+ this . usersById [ userId ] . likedPosts = this . usersById [ userId ] . likedPosts . filter ( pId => pId !== postId )
258+ return Renderer . success ( `${ this . usersById [ userId ] . username } has unliked the post` )
259+ }
260+ }
261+
262+ class FeedService {
263+ constructor ( usersById , posts ) {
264+ this . usersById = usersById
265+ this . posts = posts
266+ }
267+
268+ viewFeed ( userId ) {
269+ const orderPostsId = FeedHelper . getOrderPosts ( this . usersById [ userId ] . posts , posts )
270+ for ( const postId of orderPostsId ) {
271+ Renderer . post ( {
272+ text : posts [ postId ] . text ,
273+ date : posts [ postId ] . date ,
274+ likes : posts [ postId ] . likes ,
275+ userId,
276+ username : this . usersById [ userId ] . username ,
277+ postId
278+ } )
279+ }
280+ }
281+
282+ viewFollowingsFeed ( userId ) {
283+ const orderPostsId = FeedHelper . getOrderPosts ( FeedHelper . getPostsId ( this . usersById [ userId ] . followings , this . usersById ) , this . posts )
284+ for ( const postId of orderPostsId ) {
285+ Renderer . post ( {
286+ text : this . posts [ postId ] . text ,
287+ date : this . posts [ postId ] . date ,
288+ likes : this . posts [ postId ] . likes ,
289+ userId : this . posts [ postId ] . userId ,
290+ username : this . usersById [ posts [ postId ] . userId ] . username ,
291+ postId
292+ } )
293+ }
294+ }
295+ }
296+
297+ class Program {
298+ constructor ( userService , postService , feedService ) {
299+ this . usersById = PREVIOUS_USERS_BY_ID
300+ this . usersByUsername = PREVIOUS_USERS_BY_USERNAME
301+ this . posts = PREVIOUS_POSTS
302+ this . userService = new userService ( this . usersById , this . usersByUsername )
303+ this . postService = new postService ( this . usersById , this . posts )
304+ this . feedService = new feedService ( this . usersById , this . posts )
305+ }
306+
307+ async start ( ) {
308+ let opt
309+ do {
310+ Renderer . menu ( )
311+ opt = ( await ask ( "Enter the number of the action: " ) ) . trim ( )
312+ try {
313+ await this . setOption ( opt )
314+ } catch ( e ) {
315+ Renderer . error ( e . message )
316+ }
317+ } while ( opt !== "10" )
318+ rl . close ( )
319+ }
320+
321+ async setOption ( opt ) {
322+ switch ( opt ) {
323+ case "1" :{
324+ const username = await Validator . isValidUsername ( ( await ask ( "Enter the username: " ) ) . trim ( ) )
325+ if ( this . usersByUsername [ username ] ) throw new Error ( "The username already exists" )
326+ const id = IdHelper . getValidId ( this . usersById , this . posts )
327+ this . userService . registerUser ( id , username )
328+ break }
329+ case "2" :{
330+ const sourceUserId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
331+ const targetUserId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
332+ if ( sourceUserId === targetUserId ) throw new Error ( "You can't follow yourself" )
333+ if ( this . usersById [ sourceUserId ] . followings . includes ( targetUserId ) ) throw new Error ( "You already follow this user" )
334+ this . userService . followUser ( sourceUserId , targetUserId )
335+ break }
336+ case "3" :{
337+ const sourceUserId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
338+ const targetUserId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
339+ if ( ! this . usersById [ sourceUserId ] . followings . includes ( targetUserId ) ) throw new Error ( "You don't follow this user" )
340+ this . userService . unfollowUser ( sourceUserId , targetUserId )
341+ break }
342+ case "4" :{
343+ const userId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
344+ const text = await Validator . isValidText ( ( await ask ( "Enter the text: " ) ) . trim ( ) )
345+ const postId = IdHelper . getValidId ( this . usersById , this . posts )
346+ this . postService . createPost ( userId , postId , text )
347+ break }
348+ case "5" :{
349+ const userId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
350+ const postId = await InputHelper . getExistingPostId ( this . posts )
351+ if ( ! this . usersById [ userId ] . posts . includes ( postId ) ) throw new Error ( "The user has no posts with that ID" )
352+ this . postService . deletePost ( userId , postId )
353+ break }
354+ case "6" :{
355+ const userId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
356+ const postId = await InputHelper . getExistingPostId ( this . posts )
357+ if ( this . usersById [ userId ] . posts . includes ( postId ) ) throw new Error ( "You can't like your own post" )
358+ if ( this . usersById [ userId ] . likedPosts . includes ( postId ) ) throw new Error ( "You've already liked that post" )
359+ this . postService . likePost ( userId , postId )
360+ break }
361+ case "7" :{
362+ const userId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
363+ const postId = await InputHelper . getExistingPostId ( this . posts )
364+ if ( ! this . usersById [ userId ] . likedPosts . includes ( postId ) ) throw new Error ( "You haven't liked that post" )
365+ this . postService . unlikePost ( userId , postId )
366+ break }
367+ case "8" :{
368+ const userId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
369+ if ( this . usersById [ userId ] . posts . length === 0 ) throw new Error ( "The user has no posts" )
370+ this . feedService . viewFeed ( userId )
371+ break }
372+ case "9" :{
373+ const userId = this . usersByUsername [ await InputHelper . getExistingUsername ( this . usersByUsername ) ]
374+ if ( this . usersById [ userId ] . followings . length === 0 ) throw new Error ( "The user doesn't follow anyone" )
375+ this . feedService . viewFollowingsFeed ( userId )
376+ break }
377+ case "0" :{
378+ console . log ( this . usersById )
379+ console . log ( this . posts )
380+ break }
381+ default :break ;
382+ }
383+ }
384+ }
385+
386+ const program = new Program ( UserService , PostService , FeedService )
387+ program . start ( )
0 commit comments