├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── example.env ├── netlify.toml ├── netlify └── functions │ ├── bookmarks │ ├── delete.ts │ ├── get.ts │ ├── index.ts │ ├── post.ts │ └── put.ts │ ├── posts │ ├── helper.ts │ └── index.ts │ ├── projects │ ├── delete.ts │ ├── get.ts │ ├── index.ts │ ├── post.ts │ └── put.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── _redirects ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.spec.ts │ ├── app.module.ts │ ├── core │ │ ├── contracts │ │ │ ├── mapper.contract.ts │ │ │ └── usecase.contract.ts │ │ ├── params │ │ │ ├── no-param.paylod.ts │ │ │ └── param.payload.ts │ │ └── types │ │ │ └── types.ts │ ├── data │ │ ├── data.ioc.ts │ │ ├── data.module.ts │ │ ├── datasources │ │ │ ├── local │ │ │ │ └── .gitKeep │ │ │ └── remote │ │ │ │ ├── .gitKeep │ │ │ │ └── repo-implementations │ │ │ │ ├── bookmark │ │ │ │ ├── bookmark.repository.spec.ts │ │ │ │ └── bookmark.repository.ts │ │ │ │ ├── post │ │ │ │ ├── post.repository.spec.ts │ │ │ │ └── post.repository.ts │ │ │ │ └── project │ │ │ │ ├── project.repository.spec.ts │ │ │ │ └── project.repository.ts │ │ ├── interactors │ │ │ ├── contracts │ │ │ │ ├── ibookmark.interactor.ts │ │ │ │ ├── ipost.interactor.ts │ │ │ │ └── iproject.interactor.ts │ │ │ └── implementations │ │ │ │ ├── bookmark │ │ │ │ ├── bookmark.interactor.spec.ts │ │ │ │ └── bookmark.interactor.ts │ │ │ │ ├── post │ │ │ │ ├── post.interactor.spec.ts │ │ │ │ └── post.interactor.ts │ │ │ │ └── project │ │ │ │ ├── project.interactor.spec.ts │ │ │ │ └── project.interactor.ts │ │ └── requests │ │ │ ├── bookmark.request.ts │ │ │ ├── posts.request.ts │ │ │ └── project.request.ts │ ├── domain │ │ ├── domain.module.ts │ │ ├── entities │ │ │ ├── bookmark.entity.ts │ │ │ ├── index.ts │ │ │ ├── post.entity.ts │ │ │ └── project.entity.ts │ │ ├── repositories │ │ │ ├── ibookmark.repository.ts │ │ │ ├── ipost.repository.ts │ │ │ └── iproject.repository.ts │ │ └── usecases │ │ │ ├── bookmark-usecases │ │ │ ├── create-usecase │ │ │ │ ├── create-bookmark.usecase.spec.ts │ │ │ │ └── create-bookmark.usecase.ts │ │ │ ├── get-many-usecase │ │ │ │ ├── get-many-bookmarks.uscecase.spec.ts │ │ │ │ └── get-many-bookmarks.usecase.ts │ │ │ ├── get-one-usecase │ │ │ │ ├── get-one-bookmark.usecase.spec.ts │ │ │ │ └── get-one-bookmark.usecase.ts │ │ │ ├── remove-usecase │ │ │ │ ├── remove-bookmark.usecase.spec.ts │ │ │ │ └── remove-bookmark.usecase.ts │ │ │ └── update-usecase │ │ │ │ ├── update-bookmark.usecase.spec.ts │ │ │ │ └── update-bookmark.usecase.ts │ │ │ ├── posts-usecases │ │ │ ├── create-usecase │ │ │ │ ├── create-post.usecase.spec.ts │ │ │ │ └── create-post.usecase.ts │ │ │ ├── get-many-usecase │ │ │ │ ├── get-many-posts.usecase.spec.ts │ │ │ │ └── get-many-posts.usecase.ts │ │ │ ├── get-one-usecase │ │ │ │ ├── get-one-post.usecase.spec.ts │ │ │ │ └── get-one-post.usecase.ts │ │ │ ├── remove-usecase │ │ │ │ ├── remove-post.usecase.spec.ts │ │ │ │ └── remove-post.usecase.ts │ │ │ └── update-usecase │ │ │ │ ├── update-post.usecase.spec.ts │ │ │ │ └── update-post.usecase.ts │ │ │ └── projects-usecases │ │ │ ├── create-usecase │ │ │ ├── create-project.usecase.spec.ts │ │ │ └── create-project.usecase.ts │ │ │ ├── get-many-usecase │ │ │ ├── get-many-project.usecase.spec.ts │ │ │ └── get-many-project.usecase.ts │ │ │ ├── get-one-usecase │ │ │ ├── get-one-project.usecase.spec.ts │ │ │ └── get-one-project.usecase.ts │ │ │ ├── remove-usecase │ │ │ ├── remove-project.usecase.spec.ts │ │ │ └── remove-project.usecase.ts │ │ │ └── update-usecase │ │ │ ├── update-project.usecase.spec.ts │ │ │ └── update-project.usecase.ts │ └── presenter │ │ ├── components │ │ ├── components.module.spec.ts │ │ ├── components.module.ts │ │ ├── layouts │ │ │ ├── editor-header │ │ │ │ ├── editor-header.component.html │ │ │ │ ├── editor-header.component.spec.ts │ │ │ │ └── editor-header.component.ts │ │ │ ├── editor-layout │ │ │ │ ├── editor-layout.component.html │ │ │ │ ├── editor-layout.component.spec.ts │ │ │ │ └── editor-layout.component.ts │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.spec.ts │ │ │ │ └── footer.component.ts │ │ │ └── header │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.spec.ts │ │ │ │ └── header.component.ts │ │ └── shared │ │ │ ├── button │ │ │ ├── button.component.html │ │ │ ├── button.component.spec.ts │ │ │ └── button.component.ts │ │ │ ├── form-field │ │ │ ├── form-field.component.html │ │ │ ├── form-field.component.spec.ts │ │ │ └── form-field.component.ts │ │ │ ├── link │ │ │ ├── link.component.html │ │ │ ├── link.component.spec.ts │ │ │ └── link.component.ts │ │ │ ├── modal │ │ │ ├── modal.component.html │ │ │ ├── modal.component.spec.ts │ │ │ ├── modal.component.ts │ │ │ └── modal.service.ts │ │ │ ├── not-found │ │ │ ├── not-found.component.html │ │ │ ├── not-found.component.spec.ts │ │ │ └── not-found.component.ts │ │ │ ├── skeleton-loader │ │ │ ├── blog-skeleton │ │ │ │ ├── blog-skeleton.component.html │ │ │ │ ├── blog-skeleton.component.spec.ts │ │ │ │ └── blog-skeleton.component.ts │ │ │ ├── bookmarks-skeleton │ │ │ │ ├── bookmarks-skeleton.component.html │ │ │ │ ├── bookmarks-skeleton.component.spec.ts │ │ │ │ └── bookmarks-skeleton.component.ts │ │ │ ├── projects-skeleton │ │ │ │ ├── projects-skeleton.component.html │ │ │ │ ├── projects-skeleton.component.spec.ts │ │ │ │ └── projects-skeleton.component.ts │ │ │ ├── skeleton-loader.component.html │ │ │ ├── skeleton-loader.component.spec.ts │ │ │ └── skeleton-loader.component.ts │ │ │ ├── table │ │ │ ├── table.component.html │ │ │ ├── table.component.spec.ts │ │ │ └── table.component.ts │ │ │ ├── tag │ │ │ ├── tag.component.spec.ts │ │ │ └── tag.component.ts │ │ │ ├── theme-switcher │ │ │ ├── dark-theme-switch │ │ │ │ └── dark-theme-switch.component.ts │ │ │ ├── light-theme-switch │ │ │ │ └── light-theme-switch.component.ts │ │ │ ├── theme-switcher.component.html │ │ │ ├── theme-switcher.component.spec.ts │ │ │ ├── theme-switcher.component.ts │ │ │ └── theme-switcher.service.ts │ │ │ └── toast │ │ │ ├── toast.component.html │ │ │ ├── toast.component.spec.ts │ │ │ ├── toast.component.ts │ │ │ └── toast.service.ts │ │ ├── pages │ │ ├── blog │ │ │ ├── blog.component.html │ │ │ ├── blog.component.spec.ts │ │ │ ├── blog.component.ts │ │ │ └── post │ │ │ │ ├── post.component.html │ │ │ │ ├── post.component.spec.ts │ │ │ │ └── post.component.ts │ │ ├── bookmarks │ │ │ ├── bookmark │ │ │ │ ├── bookmark.component.html │ │ │ │ ├── bookmark.component.spec.ts │ │ │ │ └── bookmark.component.ts │ │ │ ├── bookmarks.component.html │ │ │ ├── bookmarks.component.spec.ts │ │ │ └── bookmarks.component.ts │ │ ├── editor │ │ │ ├── components │ │ │ │ ├── bookmark-form │ │ │ │ │ ├── bookmark-form.component.html │ │ │ │ │ ├── bookmark-form.component.spec.ts │ │ │ │ │ └── bookmark-form.component.ts │ │ │ │ ├── post-form │ │ │ │ │ ├── post-form.component.html │ │ │ │ │ ├── post-form.component.spec.ts │ │ │ │ │ └── post-form.component.ts │ │ │ │ └── project-form │ │ │ │ │ ├── project-form.component.html │ │ │ │ │ ├── project-form.component.spec.ts │ │ │ │ │ └── project-form.component.ts │ │ │ ├── editor-routing.module.ts │ │ │ ├── editor.component.html │ │ │ ├── editor.component.spec.ts │ │ │ ├── editor.component.ts │ │ │ ├── editor.functions.ts │ │ │ ├── editor.module.ts │ │ │ └── pages │ │ │ │ ├── editor-bookmarks │ │ │ │ ├── editor-bookmarks.component.html │ │ │ │ ├── editor-bookmarks.component.spec.ts │ │ │ │ └── editor-bookmarks.component.ts │ │ │ │ ├── editor-home │ │ │ │ ├── editor-home.component.html │ │ │ │ ├── editor-home.component.spec.ts │ │ │ │ └── editor-home.component.ts │ │ │ │ ├── editor-posts │ │ │ │ ├── editor-posts.component.html │ │ │ │ ├── editor-posts.component.spec.ts │ │ │ │ └── editor-posts.component.ts │ │ │ │ └── editor-projects │ │ │ │ ├── editor-projects.component.html │ │ │ │ ├── editor-projects.component.spec.ts │ │ │ │ └── editor-projects.component.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.spec.ts │ │ │ └── home.component.ts │ │ ├── page-seo.service.ts │ │ ├── pages.module.ts │ │ ├── post-detail │ │ │ ├── post-detail.component.html │ │ │ ├── post-detail.component.spec.ts │ │ │ └── post-detail.component.ts │ │ └── projects │ │ │ ├── project │ │ │ ├── project.component.html │ │ │ ├── project.component.spec.ts │ │ │ └── project.component.ts │ │ │ ├── projects.component.html │ │ │ ├── projects.component.spec.ts │ │ │ └── projects.component.ts │ │ ├── presenter.module.spec.ts │ │ └── presenter.module.ts ├── assets │ ├── .gitkeep │ └── img │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── blog-thumbnail.jpg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── jamie.png │ │ ├── projects │ │ └── paymed-login.svg │ │ └── site.webmanifest ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Netlify 45 | .netlify 46 | 47 | # Env 48 | .env 49 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | }, 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | MONGODB_URL= 2 | MONGODB_NAME= 3 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # netlify toml configuration 2 | [build] 3 | base = "./" 4 | publish = "./dist/angular-clean-architecture-serverless/" 5 | command = "ng build" 6 | 7 | [functions] 8 | directory = "/netlify/functions/" 9 | external_node_modules = ["mongodb", "netlify"] 10 | -------------------------------------------------------------------------------- /netlify/functions/bookmarks/delete.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from 'mongodb'; 2 | 3 | const remove = async (bookmarkId: string) => { 4 | if (!bookmarkId) throw new Error('Please provide a book to remove'); 5 | const client = new MongoClient(process.env['MONGODB_URL']); 6 | 7 | try { 8 | const database = client.db(process.env['MONGODB_NAME']); 9 | const result = await database.collection("bookmarks") 10 | .deleteOne({ "_id": new ObjectId(bookmarkId) }); 11 | return result; 12 | 13 | } catch (error) { 14 | 15 | console.log(error); 16 | throw new Error(error.message); 17 | } 18 | finally { 19 | await client.close(); 20 | } 21 | } 22 | 23 | 24 | export { remove }; 25 | -------------------------------------------------------------------------------- /netlify/functions/bookmarks/get.ts: -------------------------------------------------------------------------------- 1 | import { HandlerEvent } from "@netlify/functions"; 2 | import { MongoClient, ObjectId } from 'mongodb' 3 | 4 | const get = async (event: HandlerEvent) => { 5 | if (Object.keys(event.queryStringParameters).length > 0) { 6 | return await getOne(event); 7 | } 8 | return await getAll(event); 9 | } 10 | 11 | const getAll = async (event: HandlerEvent) => { 12 | const client = new MongoClient(process.env['MONGODB_URL']); 13 | try { 14 | await client.connect(); 15 | const db = client.db(process.env['MONGODB_NAME']); 16 | return await db.collection('bookmarks').find({}).sort({ created_at: -1 }).toArray(); 17 | } catch (err) { 18 | throw new Error(`${err.message}`); 19 | } finally { 20 | await client.close(); 21 | } 22 | } 23 | 24 | const getOne = async (event: HandlerEvent) => { 25 | const id = event.queryStringParameters.id; 26 | 27 | const client = new MongoClient(process.env['MONGODB_URL']); 28 | try { 29 | await client.connect(); 30 | const db = client.db(process.env['MONGODB_NAME']); 31 | return await db.collection('bookmarks').findOne({ "_id": new ObjectId(id) }); 32 | } catch (err) { 33 | console.log(err); 34 | throw new Error(`${err.message}`); 35 | } finally { 36 | await client.close(); 37 | } 38 | } 39 | 40 | export { get }; 41 | -------------------------------------------------------------------------------- /netlify/functions/bookmarks/index.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | 3 | import { remove } from './delete'; 4 | import { get } from './get'; 5 | import { post } from './post'; 6 | import { put } from './put'; 7 | 8 | const handler: Handler = async (event, context) => { 9 | let body; 10 | try { 11 | switch (event.httpMethod) { 12 | case 'GET': 13 | body = await get(event); 14 | break; 15 | 16 | case 'POST': 17 | body = await post(event); 18 | break; 19 | 20 | case 'PUT': 21 | body = await put(event); 22 | break; 23 | 24 | case 'DELETE': 25 | body = await remove(event.queryStringParameters.bookmarkId); 26 | break; 27 | 28 | default: 29 | return { 30 | statusCode: 405, 31 | body: JSON.stringify({ message: 'Method not supported' }), 32 | }; 33 | } 34 | 35 | return { 36 | statusCode: 200, 37 | body: JSON.stringify(body), 38 | }; 39 | } catch (err: any) { 40 | return { 41 | statusCode: 500, 42 | body: JSON.stringify({ message: err.toString() }), 43 | }; 44 | } 45 | }; 46 | 47 | export { handler }; 48 | -------------------------------------------------------------------------------- /netlify/functions/bookmarks/post.ts: -------------------------------------------------------------------------------- 1 | import { HandlerEvent } from "@netlify/functions"; 2 | import { MongoClient, ObjectId } from 'mongodb' 3 | 4 | const post = async (event: HandlerEvent) => { 5 | if (!event.body) throw new Error('Please provide a book to save'); 6 | 7 | const client = new MongoClient(process.env['MONGODB_URL']); 8 | 9 | try { 10 | const database = client.db(process.env['MONGODB_NAME']); 11 | const body = JSON.parse(event.body); 12 | return await database.collection("bookmarks").insertOne( 13 | { 14 | ...body, 15 | created_at: new Date() 16 | } 17 | ); 18 | 19 | } catch (error) { 20 | console.log(error); 21 | throw new Error(error.message); 22 | } 23 | finally { 24 | await client.close(); 25 | } 26 | } 27 | 28 | export { post }; 29 | -------------------------------------------------------------------------------- /netlify/functions/bookmarks/put.ts: -------------------------------------------------------------------------------- 1 | import { HandlerEvent } from "@netlify/functions"; 2 | import { MongoClient, ObjectId } from 'mongodb' 3 | 4 | 5 | const put = async (event: HandlerEvent) => { 6 | if (!event.body) throw new Error('Please provide a bookmark to save'); 7 | 8 | const client = new MongoClient(process.env['MONGODB_URL']); 9 | const bookmark: any = JSON.parse(event.body); 10 | 11 | try { 12 | const { _id, ...restBookmark } = bookmark; 13 | const database = client.db(process.env['MONGODB_NAME']); 14 | const result = await database.collection("bookmarks").updateOne( 15 | { _id: new ObjectId(_id) }, 16 | { $set: restBookmark, }, 17 | { upsert: false } 18 | ); 19 | 20 | return result; 21 | 22 | } catch (error) { 23 | console.log(error); 24 | throw new Error(error.message); 25 | } 26 | finally { 27 | await client.close(); 28 | } 29 | } 30 | 31 | export { put }; 32 | -------------------------------------------------------------------------------- /netlify/functions/posts/index.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | 3 | import { remove, post, put, get } from '../posts/helper'; 4 | 5 | const handler: Handler = async (event, context) => { 6 | 7 | let body; 8 | try { 9 | switch (event.httpMethod) { 10 | case 'GET': 11 | body = await get(event); 12 | break; 13 | 14 | case 'POST': 15 | body = await post(event); 16 | break; 17 | 18 | case 'PUT': 19 | body = await put(event); 20 | break; 21 | 22 | case 'DELETE': 23 | body = await remove(event.queryStringParameters.postId); 24 | break; 25 | 26 | default: 27 | return { 28 | statusCode: 405, 29 | body: JSON.stringify({ message: 'Method not supported' }) 30 | } 31 | } 32 | 33 | return { 34 | statusCode: 200, 35 | body: JSON.stringify(body) 36 | }; 37 | 38 | } catch (err: any) { 39 | 40 | return { 41 | statusCode: 500, 42 | body: JSON.stringify({ message: err.toString() }) 43 | } 44 | } 45 | 46 | } 47 | 48 | export { handler }; 49 | -------------------------------------------------------------------------------- /netlify/functions/projects/delete.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from 'mongodb' 2 | 3 | const remove = async (projectId: string) => { 4 | if (!projectId) throw new Error('Please provide a project to remove'); 5 | const client = new MongoClient(process.env['MONGODB_URL']); 6 | 7 | try { 8 | const database = client.db(process.env['MONGODB_NAME']); 9 | const result = await database.collection("projects") 10 | .deleteOne({ "_id": new ObjectId(projectId) }); 11 | return result; 12 | 13 | } catch (error) { 14 | 15 | console.log(error); 16 | throw new Error(error.message); 17 | } 18 | finally { 19 | await client.close(); 20 | } 21 | } 22 | 23 | 24 | export { remove }; 25 | -------------------------------------------------------------------------------- /netlify/functions/projects/get.ts: -------------------------------------------------------------------------------- 1 | import { HandlerEvent } from "@netlify/functions"; 2 | import { MongoClient, ObjectId } from 'mongodb' 3 | 4 | const get = async (event: HandlerEvent) => { 5 | if (Object.keys(event.queryStringParameters).length > 0) { 6 | return await getOne(event); 7 | } 8 | return await getAll(event); 9 | } 10 | 11 | const getAll = async (event: HandlerEvent) => { 12 | const client = new MongoClient(process.env['MONGODB_URL']); 13 | try { 14 | await client.connect(); 15 | const db = client.db(process.env['MONGODB_NAME']); 16 | return await db.collection('projects').find({}).sort({ created_at: -1 }).toArray(); 17 | 18 | } catch (err) { 19 | throw new Error(`${err.message}`); 20 | } finally { 21 | await client.close(); 22 | } 23 | } 24 | 25 | const getOne = async (event: HandlerEvent) => { 26 | const id = event.queryStringParameters.id; 27 | 28 | const client = new MongoClient(process.env['MONGODB_URL']); 29 | try { 30 | await client.connect(); 31 | const db = client.db(process.env['MONGODB_NAME']); 32 | return await db.collection('projects').findOne({ "_id": new ObjectId(id) }); 33 | } catch (err) { 34 | console.log(err); 35 | throw new Error(`${err.message}`); 36 | } finally { 37 | await client.close(); 38 | } 39 | } 40 | 41 | export { get }; 42 | -------------------------------------------------------------------------------- /netlify/functions/projects/index.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | import { get } from '../../functions/projects/get'; 3 | import { post } from '../../functions/projects/post'; 4 | import { put } from '../../functions/projects/put'; 5 | import { remove } from '../../functions/projects/delete'; 6 | 7 | 8 | const handler: Handler = async (event, context) => { 9 | let body; 10 | try { 11 | switch (event.httpMethod) { 12 | case 'GET': 13 | body = await get(event); 14 | break; 15 | 16 | case 'POST': 17 | body = await post(event); 18 | break; 19 | 20 | case 'PUT': 21 | body = await put(event); 22 | break; 23 | 24 | case 'DELETE': 25 | body = await remove(event.queryStringParameters.projectId); 26 | break; 27 | 28 | default: 29 | return { 30 | statusCode: 405, 31 | body: JSON.stringify({ message: 'Method not supported' }) 32 | } 33 | } 34 | 35 | return { 36 | statusCode: 200, 37 | body: JSON.stringify(body) 38 | }; 39 | 40 | } catch (err: any) { 41 | 42 | return { 43 | statusCode: 500, 44 | body: JSON.stringify({ message: err.toString() }) 45 | } 46 | } 47 | } 48 | 49 | export { handler }; 50 | -------------------------------------------------------------------------------- /netlify/functions/projects/post.ts: -------------------------------------------------------------------------------- 1 | import { HandlerEvent } from "@netlify/functions"; 2 | import { MongoClient, ObjectId } from 'mongodb' 3 | 4 | const post = async (event: HandlerEvent) => { 5 | if (!event.body) throw new Error('Please provide a project to save'); 6 | 7 | const client = new MongoClient(process.env['MONGODB_URL']); 8 | 9 | try { 10 | const database = client.db(process.env['MONGODB_NAME']); 11 | const body = JSON.parse(event.body); 12 | return await database.collection("projects").insertOne( 13 | { 14 | ...body, 15 | created_at: new Date() 16 | } 17 | ); 18 | 19 | } catch (error) { 20 | console.log(error); 21 | throw new Error(error.message); 22 | } 23 | finally { 24 | await client.close(); 25 | } 26 | } 27 | 28 | export { post }; 29 | -------------------------------------------------------------------------------- /netlify/functions/projects/put.ts: -------------------------------------------------------------------------------- 1 | import { HandlerEvent } from "@netlify/functions"; 2 | import { MongoClient, ObjectId } from 'mongodb' 3 | 4 | 5 | const put = async (event: HandlerEvent) => { 6 | if (!event.body) throw new Error('Please provide a project to update'); 7 | 8 | const client = new MongoClient(process.env['MONGODB_URL']); 9 | const project: any = JSON.parse(event.body); 10 | 11 | try { 12 | const { _id, ...restProject } = project; 13 | const database = client.db(process.env['MONGODB_NAME']); 14 | const result = await database.collection("projects").updateOne( 15 | { _id: new ObjectId(_id) }, 16 | { $set: restProject }, 17 | ); 18 | 19 | return result; 20 | 21 | } catch (error) { 22 | console.log(error); 23 | throw new Error(error.message); 24 | } 25 | finally { 26 | await client.close(); 27 | } 28 | } 29 | 30 | export { put }; 31 | -------------------------------------------------------------------------------- /netlify/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "isolatedModules": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-clean-architecture-serverless", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^15.1.0", 14 | "@angular/common": "^15.1.0", 15 | "@angular/compiler": "^15.1.0", 16 | "@angular/core": "^15.1.0", 17 | "@angular/forms": "^15.1.0", 18 | "@angular/platform-browser": "^15.1.0", 19 | "@angular/platform-browser-dynamic": "^15.1.0", 20 | "@angular/router": "^15.1.0", 21 | "@netlify/functions": "^1.4.0", 22 | "emoji-toolkit": "^6.6.0", 23 | "marked": "^4.2.12", 24 | "mongodb": "^4.12.1", 25 | "ngx-markdown": "^15.0.0-next.0", 26 | "prismjs": "^1.29.0", 27 | "rxjs": "~7.8.0", 28 | "tslib": "^2.3.0", 29 | "zone.js": "~0.12.0" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^15.1.1", 33 | "@angular/cli": "~15.1.1", 34 | "@angular/compiler-cli": "^15.1.0", 35 | "@tailwindcss/typography": "^0.5.9", 36 | "@types/jasmine": "~4.3.0", 37 | "@types/marked": "^4.0.8", 38 | "autoprefixer": "^10.4.13", 39 | "jasmine-core": "~4.5.0", 40 | "karma": "~6.4.0", 41 | "karma-chrome-launcher": "~3.1.0", 42 | "karma-coverage": "~2.2.0", 43 | "karma-jasmine": "~5.1.0", 44 | "karma-jasmine-html-reporter": "~2.0.0", 45 | "postcss": "^8.4.21", 46 | "tailwindcss": "^3.2.4", 47 | "typescript": "~4.9.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { EditorLayoutComponent } from './presenter/components/layouts/editor-layout/editor-layout.component'; 2 | import { PostDetailComponent } from './presenter/pages/post-detail/post-detail.component'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | 6 | import { BlogComponent } from './presenter/pages/blog/blog.component'; 7 | import { BookmarksComponent } from './presenter/pages/bookmarks/bookmarks.component'; 8 | import { HomeComponent } from './presenter/pages/home/home.component'; 9 | import { ProjectsComponent } from './presenter/pages/projects/projects.component'; 10 | import { NotFoundComponent } from './presenter/components/shared/not-found/not-found.component'; 11 | 12 | 13 | const routes: Routes = [ 14 | { path: '', component: HomeComponent }, 15 | { path: 'blog', component: BlogComponent }, 16 | { path: 'blog/:postId', component: PostDetailComponent }, 17 | 18 | { path: 'projects', component: ProjectsComponent }, 19 | { path: 'bookmarks', component: BookmarksComponent }, 20 | { 21 | path: 'editor', 22 | loadChildren: () => import('./presenter/pages/editor/editor.module').then((c) => c.EditorModule), 23 | component: EditorLayoutComponent 24 | }, 25 | { path: '**', component: NotFoundComponent }, 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' })], 30 | exports: [RouterModule] 31 | }) 32 | export class AppRoutingModule { } 33 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { PageSeoService } from './presenter/pages/page-seo.service'; 4 | import { ThemeSwitcherService } from './presenter/components/shared/theme-switcher/theme-switcher.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | }) 10 | export class AppComponent implements OnInit { 11 | 12 | 13 | public themePref$ = this.themeService.pref$; 14 | 15 | public constructor( 16 | private themeService: ThemeSwitcherService, 17 | private seoService: PageSeoService 18 | ) { } 19 | 20 | 21 | ngOnInit(): void { 22 | this.seoService.setSEO(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/app.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | describe('AppModule', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [AppModule], 9 | }); 10 | }); 11 | 12 | it('should initialize', () => { 13 | const module = TestBed.inject(AppModule); 14 | 15 | expect(module).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | 8 | import { MarkdownModule } from 'ngx-markdown'; 9 | 10 | import { ToastService } from 'src/app/presenter/components/shared/toast/toast.service'; 11 | 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { AppComponent } from './app.component'; 14 | 15 | import { DATA_BOOKMARK_IOC, DATA_POST_IOC, DATA_PROJECT_IOC } from './data/data.ioc'; 16 | import { PresenterModule } from './presenter/presenter.module'; 17 | 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | ], 23 | imports: [ 24 | BrowserModule, 25 | BrowserAnimationsModule, 26 | FormsModule, 27 | ReactiveFormsModule, 28 | HttpClientModule, 29 | RouterModule, 30 | MarkdownModule.forRoot(), 31 | AppRoutingModule, 32 | PresenterModule, 33 | ], 34 | providers: [ 35 | ...DATA_BOOKMARK_IOC, 36 | ...DATA_PROJECT_IOC, 37 | ...DATA_POST_IOC, 38 | ToastService 39 | ], 40 | bootstrap: [AppComponent], 41 | exports: [] 42 | }) 43 | export class AppModule { 44 | } 45 | -------------------------------------------------------------------------------- /src/app/core/contracts/mapper.contract.ts: -------------------------------------------------------------------------------- 1 | export abstract class Mapper { 2 | 3 | protected abstract toEntity(param: R): E; 4 | 5 | protected abstract toRequest(param: E): R; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/app/core/contracts/usecase.contract.ts: -------------------------------------------------------------------------------- 1 | export interface Usecase { 2 | execute(param: T): R; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/core/params/no-param.paylod.ts: -------------------------------------------------------------------------------- 1 | export class NoParam { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/app/core/params/param.payload.ts: -------------------------------------------------------------------------------- 1 | export class Param { 2 | 3 | payload: T; 4 | 5 | constructor(payload: T) { 6 | this.payload = payload; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/core/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface Result { 2 | acknowledged: boolean; 3 | insertedId?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/data/data.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | @NgModule({ 4 | providers: [], 5 | exports: [] 6 | }) 7 | export class DataModule { } 8 | -------------------------------------------------------------------------------- /src/app/data/datasources/local/.gitKeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/app/data/datasources/local/.gitKeep -------------------------------------------------------------------------------- /src/app/data/datasources/remote/.gitKeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/app/data/datasources/remote/.gitKeep -------------------------------------------------------------------------------- /src/app/data/datasources/remote/repo-implementations/bookmark/bookmark.repository.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { map, Observable } from 'rxjs'; 5 | 6 | import { Result } from 'src/app/core/types/types'; 7 | import { BookmarkEntity } from 'src/app/domain/entities'; 8 | import { IBookmarkRepository } from 'src/app/domain/repositories/ibookmark.repository'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class BookmarkRepository extends IBookmarkRepository { 12 | 13 | public baseUrl = window.location.origin + '/.netlify/functions/bookmarks'; 14 | 15 | constructor(private http: HttpClient) { 16 | super(); 17 | } 18 | 19 | public all(): Observable { 20 | return this.http.get(`${this.baseUrl}`); 21 | } 22 | 23 | public createBookmark(bookmark: BookmarkEntity): Observable { 24 | return this.http.post(`${this.baseUrl}`, bookmark); 25 | } 26 | 27 | public getBookmark(id: string): Observable { 28 | return this.http 29 | .get(`${this.baseUrl}?id=${id}`); 30 | } 31 | 32 | public updateBookmark(id: string, bookmark: BookmarkEntity): Observable { 33 | return this.http.put(`${this.baseUrl}?bookmarkId=${id}`, bookmark); 34 | } 35 | 36 | public removeBookmark(id: string): Observable { 37 | return this.http.delete(`${this.baseUrl}?bookmarkId=${id}`); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/data/datasources/remote/repo-implementations/post/post.repository.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Result } from 'src/app/core/types/types'; 7 | import { PostEntity } from 'src/app/domain/entities'; 8 | import { IPostRepository } from 'src/app/domain/repositories/ipost.repository'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class PostRepository implements IPostRepository { 12 | 13 | public baseUrl = window.location.origin + '/.netlify/functions/posts'; 14 | 15 | constructor(private http: HttpClient) { } 16 | 17 | all(): Observable { 18 | return this.http 19 | .get(`${this.baseUrl}`); 20 | } 21 | 22 | createPost(post: PostEntity): Observable { 23 | return this.http.post(`${this.baseUrl}`, post); 24 | } 25 | 26 | getPost(id: string): Observable { 27 | return this.http 28 | .get(`${this.baseUrl}?id=${id}`); 29 | } 30 | 31 | updatePost(id: string, post: PostEntity): Observable { 32 | return this.http.put(`${this.baseUrl}?postId=${id}`, post); 33 | } 34 | 35 | removePost(id: string): Observable { 36 | return this.http.delete(`${this.baseUrl}?postId=${id}`); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/data/datasources/remote/repo-implementations/project/project.repository.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { map, Observable, of } from 'rxjs'; 4 | 5 | import { Result } from 'src/app/core/types/types'; 6 | import { ProjectEntity } from 'src/app/domain/entities'; 7 | import { IProjectRepository } from 'src/app/domain/repositories/iproject.repository'; 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export class ProjectRepository implements IProjectRepository { 11 | 12 | public baseUrl = window.location.origin + '/.netlify/functions/projects'; 13 | 14 | constructor(private http: HttpClient) { } 15 | 16 | all(): Observable { 17 | return this.http 18 | .get(`${this.baseUrl}`); 19 | } 20 | 21 | createProject(project: ProjectEntity): Observable { 22 | return this.http.post(`${this.baseUrl}`, project); 23 | } 24 | 25 | getProject(id: string): Observable { 26 | return this.http 27 | .get(`${this.baseUrl}?id=${id}`); 28 | } 29 | 30 | updateProject(id: string, project: ProjectEntity): Observable { 31 | return this.http.put(`${this.baseUrl}?projectId=${id}`, project); 32 | } 33 | 34 | removeProject(id: string): Observable { 35 | return this.http.delete(`${this.baseUrl}?projectId=${id}`); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/data/interactors/contracts/ibookmark.interactor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Result } from './../../../core/types/types'; 4 | import { BookmarkRequest } from 'src/app/data/requests/bookmark.request'; 5 | 6 | 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export abstract class IBookmarkInteractor { 10 | 11 | abstract save(bookmark: BookmarkRequest): Observable; 12 | 13 | abstract getMany(): Observable; 14 | 15 | abstract getOne(slug: string): Observable 16 | 17 | abstract create(bookmark: BookmarkRequest): Observable; 18 | 19 | abstract update(bookmark: BookmarkRequest): Observable; 20 | 21 | abstract delete(slug: string): Observable; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/data/interactors/contracts/ipost.interactor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { Result } from './../../../core/types/types'; 5 | import { PostInteractor } from '../implementations/post/post.interactor'; 6 | import { PostRequest } from '../../requests/posts.request'; 7 | 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export abstract class IPostInteractor { 11 | 12 | abstract getMany(): Observable; 13 | 14 | abstract getOne(slug: string): Observable 15 | 16 | abstract create(post: PostRequest): Observable; 17 | 18 | abstract update(post: PostRequest): Observable; 19 | 20 | abstract delete(slug: string): Observable; 21 | 22 | abstract savePost(formValues: PostRequest): Observable; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/data/interactors/contracts/iproject.interactor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { Result } from '../../../core/types/types'; 5 | import { ProjectRequest } from '../../requests/project.request'; 6 | 7 | 8 | 9 | @Injectable({ providedIn: 'root', }) 10 | export abstract class IProjectInteractor { 11 | 12 | abstract save(project: ProjectRequest): Observable; 13 | 14 | abstract getMany(): Observable; 15 | 16 | abstract getOne(slug: string): Observable 17 | 18 | abstract create(project: ProjectRequest): Observable; 19 | 20 | abstract update(project: ProjectRequest): Observable; 21 | 22 | abstract delete(slug: string): Observable; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/data/interactors/implementations/bookmark/bookmark.interactor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { RemoveBookmarkUsecase } from '../../../../domain/usecases/bookmark-usecases/remove-usecase/remove-bookmark.usecase'; 5 | import { UpdateBookmarkUsecase } from '../../../../domain/usecases/bookmark-usecases/update-usecase/update-bookmark.usecase'; 6 | import { GetOneBookmarkUsecase } from '../../../../domain/usecases/bookmark-usecases/get-one-usecase/get-one-bookmark.usecase'; 7 | import { GetManyBookmarksUsecase } from '../../../../domain/usecases/bookmark-usecases/get-many-usecase/get-many-bookmarks.usecase'; 8 | import { Result } from '../../../../core/types/types'; 9 | import { Param } from 'src/app/core/params/param.payload'; 10 | import { BookmarkRequest } from '../../../requests/bookmark.request'; 11 | import { CreateBookmarkUsecase } from '../../../../domain/usecases/bookmark-usecases/create-usecase/create-bookmark.usecase'; 12 | import { IBookmarkInteractor } from '../../contracts/ibookmark.interactor'; 13 | import { NoParam } from 'src/app/core/params/no-param.paylod'; 14 | 15 | 16 | @Injectable({ providedIn: 'root' }) 17 | export class BookmarkInteractor extends IBookmarkInteractor { 18 | 19 | constructor( 20 | private createBookmarkUsecase: CreateBookmarkUsecase, 21 | private getManyBookmarkUsecase: GetManyBookmarksUsecase, 22 | private getOneBookmarkUsecase: GetOneBookmarkUsecase, 23 | private updateBookmarkUsecase: UpdateBookmarkUsecase, 24 | private removeBookmarkUsecase: RemoveBookmarkUsecase 25 | ) { 26 | super(); 27 | } 28 | 29 | public save(bookmark: BookmarkRequest): Observable { 30 | if (bookmark._id) return this.update(bookmark); 31 | 32 | return this.create(bookmark); 33 | } 34 | 35 | public create(bookmark: BookmarkRequest): Observable { 36 | return this.createBookmarkUsecase.execute(new Param(bookmark)); 37 | } 38 | 39 | public getMany(): Observable { 40 | return this.getManyBookmarkUsecase.execute(new NoParam()); 41 | } 42 | 43 | public getOne(slug: string): Observable { 44 | return this.getOneBookmarkUsecase.execute(new Param(slug)); 45 | } 46 | 47 | public update(bookmark: BookmarkRequest): Observable { 48 | return this.updateBookmarkUsecase.execute(new Param(bookmark)); 49 | } 50 | 51 | public delete(slug: string): Observable { 52 | return this.removeBookmarkUsecase.execute(new Param(slug)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/app/data/interactors/implementations/post/post.interactor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | 4 | import { Result } from '../../../../core/types/types'; 5 | import { IPostInteractor } from '../../contracts/ipost.interactor'; 6 | import { PostRequest } from '../../../requests/posts.request'; 7 | import { Param } from 'src/app/core/params/param.payload'; 8 | import { NoParam } from 'src/app/core/params/no-param.paylod'; 9 | import { CreatePostUsecase } from '../../../../domain/usecases/posts-usecases/create-usecase/create-post.usecase'; 10 | import { UpdatePostUsecase } from '../../../../domain/usecases/posts-usecases/update-usecase/update-post.usecase'; 11 | import { RemovePostUsecase } from '../../../../domain/usecases/posts-usecases/remove-usecase/remove-post.usecase'; 12 | import { GetManyPostUsecase } from '../../../../domain/usecases/posts-usecases/get-many-usecase/get-many-posts.usecase'; 13 | import { GetOnePostUsecase } from '../../../../domain/usecases/posts-usecases/get-one-usecase/get-one-post.usecase'; 14 | 15 | 16 | @Injectable({ providedIn: 'root' }) 17 | export class PostInteractor extends IPostInteractor { 18 | 19 | constructor( 20 | private getOnePostUsecase: GetOnePostUsecase, 21 | private getManyPostUsecase: GetManyPostUsecase, 22 | private removePostUsecase: RemovePostUsecase, 23 | private updatePostUsecase: UpdatePostUsecase, 24 | private createPostUsecase: CreatePostUsecase 25 | ) { 26 | super(); 27 | } 28 | 29 | public savePost(post: PostRequest): Observable { 30 | if (post._id) return this.update(post); 31 | 32 | return this.create(post); 33 | } 34 | 35 | public create(post: PostRequest): Observable { 36 | return this.createPostUsecase.execute(new Param(post)); 37 | } 38 | 39 | public getMany(): Observable { 40 | const result = this.getManyPostUsecase.execute(new NoParam()); 41 | return result; 42 | } 43 | 44 | public getOne(slug: string): Observable { 45 | return this.getOnePostUsecase.execute(new Param(slug)); 46 | } 47 | 48 | public update(post: PostRequest): Observable { 49 | return this.updatePostUsecase.execute(new Param({ ...post, id: post._id })); 50 | } 51 | 52 | public delete(slug: string): Observable { 53 | return this.removePostUsecase.execute(new Param(slug)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/app/data/interactors/implementations/project/project.interactor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | 4 | import { Result } from '../../../../core/types/types'; 5 | import { Param } from 'src/app/core/params/param.payload'; 6 | import { NoParam } from 'src/app/core/params/no-param.paylod'; 7 | import { ProjectRequest } from '../../../requests/project.request'; 8 | import { IProjectInteractor } from '../../contracts/iproject.interactor'; 9 | import { GetOneProjectUsecase } from 'src/app/domain/usecases/projects-usecases/get-one-usecase/get-one-project.usecase'; 10 | import { GetManyProjectUsecase } from 'src/app/domain/usecases/projects-usecases/get-many-usecase/get-many-project.usecase'; 11 | import { RemoveProjectUsecase } from 'src/app/domain/usecases/projects-usecases/remove-usecase/remove-project.usecase'; 12 | import { CreateProjectUsecase } from 'src/app/domain/usecases/projects-usecases/create-usecase/create-project.usecase'; 13 | import { UpdateProjectUsecase } from '../../../../domain/usecases/projects-usecases/update-usecase/update-project.usecase'; 14 | 15 | 16 | @Injectable({ providedIn: 'root' }) 17 | export class ProjectInteractor extends IProjectInteractor { 18 | 19 | constructor( 20 | private getOneProjectUsecase: GetOneProjectUsecase, 21 | private getManyProjectUsecase: GetManyProjectUsecase, 22 | private removeProjectUsecase: RemoveProjectUsecase, 23 | private updateProjectUsecase: UpdateProjectUsecase, 24 | private createProjectUsecase: CreateProjectUsecase 25 | ) { 26 | super(); 27 | } 28 | 29 | public save(project: ProjectRequest): Observable { 30 | if (project._id) return this.update(project); 31 | 32 | return this.create(project); 33 | } 34 | 35 | public create(project: ProjectRequest): Observable { 36 | return this.createProjectUsecase.execute(new Param(project)); 37 | } 38 | 39 | public getMany(): Observable { 40 | return this.getManyProjectUsecase.execute(new NoParam()); 41 | } 42 | 43 | public getOne(slug: string): Observable { 44 | return this.getOneProjectUsecase.execute(new Param(slug)); 45 | } 46 | 47 | public update(project: ProjectRequest): Observable { 48 | return this.updateProjectUsecase.execute(new Param({ ...project, id: project._id })); 49 | } 50 | 51 | public delete(slug: string): Observable { 52 | return this.removeProjectUsecase.execute(new Param(slug)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/app/data/requests/bookmark.request.ts: -------------------------------------------------------------------------------- 1 | import { BookmarkEntity } from './../../domain/entities/bookmark.entity'; 2 | 3 | 4 | export class BookmarkRequest extends BookmarkEntity { 5 | _id?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/data/requests/posts.request.ts: -------------------------------------------------------------------------------- 1 | import { PostEntity } from 'src/app/domain/entities'; 2 | 3 | export class PostRequest extends PostEntity { 4 | _id?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/data/requests/project.request.ts: -------------------------------------------------------------------------------- 1 | import { ProjectEntity } from './../../domain/entities/project.entity'; 2 | 3 | export class ProjectRequest extends ProjectEntity { 4 | _id?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/domain/domain.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | @NgModule() 4 | export class DomainModule { } 5 | -------------------------------------------------------------------------------- /src/app/domain/entities/bookmark.entity.ts: -------------------------------------------------------------------------------- 1 | export class BookmarkEntity { 2 | 3 | id?: string; 4 | short!: string; 5 | url!: string; 6 | date?: string; 7 | tags?: string[]; 8 | 9 | constructor(id?: string) { 10 | this.id = id; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/domain/entities/index.ts: -------------------------------------------------------------------------------- 1 | export { BookmarkEntity } from "../entities/bookmark.entity"; 2 | export { PostEntity } from "../entities/post.entity"; 3 | export { ProjectEntity } from "../entities/project.entity"; 4 | -------------------------------------------------------------------------------- /src/app/domain/entities/post.entity.ts: -------------------------------------------------------------------------------- 1 | export class PostEntity { 2 | id?: string; 3 | tags?: string[]; 4 | content!: string; 5 | date!: string; 6 | title!: string; 7 | created_at?: Date; 8 | author?: string; 9 | excerpt?: string; 10 | read?: number; 11 | views?: number; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/domain/entities/project.entity.ts: -------------------------------------------------------------------------------- 1 | export type ProjectAction = { 2 | title: string; 3 | link: string; 4 | } 5 | 6 | export class ProjectEntity { 7 | id?: string; 8 | title!: string; 9 | description!: string; 10 | features?: string[]; 11 | modules?: string[]; 12 | industries?: string[]; 13 | tools?: string[]; 14 | imageUrl?: string; 15 | action?: ProjectAction; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/domain/repositories/ibookmark.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable } from "rxjs"; 4 | 5 | import { Result } from 'src/app/core/types/types'; 6 | 7 | import { BookmarkEntity } from "../entities"; 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export abstract class IBookmarkRepository { 11 | 12 | abstract all(): Observable; 13 | 14 | abstract createBookmark(bookmark: BookmarkEntity): Observable; 15 | 16 | abstract getBookmark(id: string): Observable; 17 | 18 | abstract updateBookmark(id: string, bookmark: BookmarkEntity): Observable; 19 | 20 | abstract removeBookmark(id: string): Observable; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/domain/repositories/ipost.repository.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'src/app/core/types/types'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { PostEntity } from './../entities/'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export abstract class IPostRepository { 8 | 9 | abstract all(): Observable; 10 | 11 | abstract createPost(post: PostEntity): Observable; 12 | 13 | abstract getPost(id: string): Observable; 14 | 15 | abstract updatePost(id: string, post: PostEntity): Observable; 16 | 17 | abstract removePost(id: string): Observable; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/domain/repositories/iproject.repository.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'src/app/core/types/types'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { ProjectEntity } from '../entities'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export abstract class IProjectRepository { 8 | 9 | abstract all(): Observable; 10 | 11 | abstract createProject(project: ProjectEntity): Observable; 12 | 13 | abstract getProject(id: string): Observable; 14 | 15 | abstract updateProject(id: string, project: ProjectEntity): Observable; 16 | 17 | abstract removeProject(id: string): Observable; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/create-usecase/create-bookmark.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { MOCK_BOOKMARKS } from './../../../../data/datasources/remote/repo-implementations/bookmark/bookmark.repository.spec'; 3 | import { BookmarkEntity } from './../../../entities/bookmark.entity'; 4 | import { Param } from "src/app/core/params/param.payload"; 5 | import { IBookmarkRepository } from "src/app/domain/repositories/ibookmark.repository"; 6 | import { CreateBookmarkUsecase } from "./create-bookmark.usecase"; 7 | 8 | 9 | describe('CreateBookmarkUsecase', () => { 10 | let usecase: CreateBookmarkUsecase; 11 | let ibookmarkRepository: jasmine.SpyObj; 12 | let bookmark: BookmarkEntity; 13 | 14 | beforeEach(() => { 15 | ibookmarkRepository = jasmine.createSpyObj(IBookmarkRepository, ['createBookmark']); 16 | usecase = new CreateBookmarkUsecase(ibookmarkRepository); 17 | bookmark = MOCK_BOOKMARKS[0]; 18 | }); 19 | 20 | describe('constructor', () => { 21 | it('should be created', () => { 22 | expect(usecase).toBeTruthy(); 23 | }) 24 | it('should be created with dependency', () => { 25 | expect(usecase['ibookmarkRepository']).toBeTruthy(); 26 | }) 27 | }) 28 | 29 | describe('execute', () => { 30 | it('should accept Param as input', () => { 31 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(CreateBookmarkUsecase, ['execute']); 32 | 33 | usecase.execute(new Param(bookmark)); 34 | const args = usecase.execute.calls.argsFor(0); 35 | 36 | expect(args[0]).toBeInstanceOf(Param); 37 | expect(args[0].payload).toBeDefined; 38 | expect(args[0].payload).toEqual(jasmine.objectContaining(bookmark)); 39 | }) 40 | 41 | it('should return an Observable ', () => { 42 | spyOn(usecase, 'execute').and.returnValue(of()) 43 | 44 | const result = usecase.execute(new Param(bookmark)); 45 | 46 | expect(result).toBeInstanceOf(Observable); 47 | 48 | }); 49 | 50 | it('should call ibookmarkRepository.createBookmark with param', () => { 51 | usecase.execute(new Param(bookmark)); 52 | 53 | expect(usecase['ibookmarkRepository'].createBookmark).toHaveBeenCalled(); 54 | expect(usecase['ibookmarkRepository'].createBookmark).toHaveBeenCalledWith(bookmark); 55 | }); 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/create-usecase/create-bookmark.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { BookmarkEntity } from '../../../entities/bookmark.entity'; 4 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 5 | import { Param } from 'src/app/core/params/param.payload'; 6 | import { IBookmarkRepository } from '../../../repositories/ibookmark.repository'; 7 | import { Result } from 'src/app/core/types/types'; 8 | 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class CreateBookmarkUsecase implements Usecase, Observable> { 12 | 13 | constructor(private ibookmarkRepository: IBookmarkRepository) { } 14 | 15 | execute(param: Param): Observable { 16 | return this.ibookmarkRepository.createBookmark(param.payload); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/get-many-usecase/get-many-bookmarks.uscecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | import { NoParam } from "src/app/core/params/no-param.paylod"; 3 | import { Param } from "src/app/core/params/param.payload"; 4 | import { MOCK_BOOKMARKS } from "src/app/data/datasources/remote/repo-implementations/bookmark/bookmark.repository.spec"; 5 | import { BookmarkEntity } from "src/app/domain/entities"; 6 | import { IBookmarkRepository } from "src/app/domain/repositories/ibookmark.repository"; 7 | import { GetManyBookmarksUsecase } from "./get-many-bookmarks.usecase"; 8 | 9 | 10 | describe('GetManyBookmarksUsecase', () => { 11 | let usecase: GetManyBookmarksUsecase; 12 | let ibookmarkRepository: jasmine.SpyObj; 13 | let bookmark: BookmarkEntity; 14 | 15 | 16 | beforeEach(() => { 17 | ibookmarkRepository = jasmine.createSpyObj(IBookmarkRepository, ['all']); 18 | usecase = new GetManyBookmarksUsecase(ibookmarkRepository); 19 | bookmark = MOCK_BOOKMARKS[0]; 20 | }); 21 | 22 | describe('constructor', () => { 23 | it('should be created', () => { 24 | expect(usecase).toBeTruthy(); 25 | }) 26 | it('should be created with dependency', () => { 27 | expect(usecase['iBookmarkRepository']).toBeTruthy(); 28 | }) 29 | }) 30 | 31 | describe('execute', () => { 32 | it('should accept NoParam as input', () => { 33 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(GetManyBookmarksUsecase, ['execute']); 34 | 35 | usecase.execute(new NoParam()); 36 | const args = usecase.execute.calls.argsFor(0); 37 | 38 | expect(args[0]).toBeInstanceOf(NoParam); 39 | expect(args[0]).toBeDefined; 40 | 41 | }) 42 | it('should return an Observable ', () => { 43 | spyOn(usecase, 'execute').and.returnValue(of()) 44 | 45 | const result = usecase.execute(new NoParam()); 46 | 47 | expect(result).toBeInstanceOf(Observable); 48 | }); 49 | it('should call ibookmarkRepository.all', () => { 50 | usecase.execute(new NoParam()); 51 | 52 | expect(usecase['iBookmarkRepository'].all).toHaveBeenCalled(); 53 | expect(usecase['iBookmarkRepository'].all).toHaveBeenCalledWith(); 54 | 55 | }); 56 | 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/get-many-usecase/get-many-bookmarks.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { NoParam } from '../../../../core/params/no-param.paylod'; 4 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 5 | import { IBookmarkRepository } from '../../../repositories/ibookmark.repository'; 6 | import { BookmarkEntity } from '../../../entities'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class GetManyBookmarksUsecase implements Usecase> { 10 | 11 | constructor(private iBookmarkRepository: IBookmarkRepository) { } 12 | 13 | execute(payload: NoParam): Observable { 14 | return this.iBookmarkRepository.all(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/get-one-usecase/get-one-bookmark.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { MOCK_BOOKMARKS } from "src/app/data/datasources/remote/repo-implementations/bookmark/bookmark.repository.spec"; 2 | import { BookmarkEntity } from "src/app/domain/entities"; 3 | import { IBookmarkRepository } from "src/app/domain/repositories/ibookmark.repository"; 4 | import { GetOneBookmarkUsecase } from "./get-one-bookmark.usecase"; 5 | import { Param } from 'src/app/core/params/param.payload'; 6 | import { Observable, of } from 'rxjs'; 7 | 8 | describe('GetOneBookmarkUsecase', () => { 9 | 10 | let usecase: GetOneBookmarkUsecase 11 | let ibookmarkRepository: jasmine.SpyObj; 12 | let bookmark: BookmarkEntity; 13 | 14 | beforeEach(() => { 15 | ibookmarkRepository = jasmine.createSpyObj(IBookmarkRepository, ['getBookmark']); 16 | usecase = new GetOneBookmarkUsecase(ibookmarkRepository); 17 | bookmark = MOCK_BOOKMARKS[0]; 18 | }); 19 | 20 | 21 | describe('constructor', () => { 22 | it('should be created', () => { 23 | expect(usecase).toBeTruthy(); 24 | }); 25 | it('should accept IBookmarkRepository as dependency', () => { 26 | expect(usecase['ibookmarkRepository']).toBeTruthy(); 27 | }); 28 | 29 | }) 30 | 31 | describe('execute', () => { 32 | it('should accept Param as input', () => { 33 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(GetOneBookmarkUsecase, ['execute']); 34 | 35 | usecase.execute(new Param(bookmark.id!)); 36 | const args = usecase.execute.calls.argsFor(0); 37 | 38 | expect(args[0]).toBeInstanceOf(Param); 39 | expect(args[0].payload).toBeDefined; 40 | expect(args[0].payload).toEqual(jasmine.stringContaining(bookmark.id!)); 41 | 42 | }); 43 | it('should return an Observable', () => { 44 | spyOn(usecase, 'execute').and.returnValue(of()) 45 | 46 | const result = usecase.execute(new Param(bookmark.id!)); 47 | 48 | expect(result).toBeInstanceOf(Observable); 49 | 50 | }); 51 | it('should call ibookmarkRepository.getBookmark with Param', () => { 52 | usecase.execute(new Param(bookmark.id!)); 53 | 54 | expect(usecase['ibookmarkRepository'].getBookmark).toHaveBeenCalled(); 55 | expect(usecase['ibookmarkRepository'].getBookmark).toHaveBeenCalledWith(bookmark.id!); 56 | 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/get-one-usecase/get-one-bookmark.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Param } from '../../../../core/params/param.payload'; 4 | import { BookmarkEntity } from '../../../entities/bookmark.entity'; 5 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 6 | import { IBookmarkRepository } from '../../../repositories/ibookmark.repository'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class GetOneBookmarkUsecase implements Usecase, Observable> { 10 | 11 | constructor(private ibookmarkRepository: IBookmarkRepository) { } 12 | 13 | execute(param: Param): Observable { 14 | return this.ibookmarkRepository.getBookmark(param.payload); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/remove-usecase/remove-bookmark.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | import { Param } from "src/app/core/params/param.payload"; 3 | import { MOCK_BOOKMARKS } from "src/app/data/datasources/remote/repo-implementations/bookmark/bookmark.repository.spec"; 4 | import { BookmarkEntity } from "src/app/domain/entities"; 5 | import { IBookmarkRepository } from "src/app/domain/repositories/ibookmark.repository"; 6 | import { RemoveBookmarkUsecase } from "./remove-bookmark.usecase"; 7 | 8 | describe('RemoveBookmarkUsecase', () => { 9 | 10 | let usecase: RemoveBookmarkUsecase; 11 | let ibookmarkRepository: jasmine.SpyObj; 12 | let bookmark: BookmarkEntity; 13 | 14 | beforeEach(() => { 15 | ibookmarkRepository = jasmine.createSpyObj(IBookmarkRepository, ['removeBookmark']); 16 | usecase = new RemoveBookmarkUsecase(ibookmarkRepository); 17 | bookmark = MOCK_BOOKMARKS[0]; 18 | }); 19 | 20 | 21 | describe('constructor', () => { 22 | it('should be created', () => { 23 | expect(usecase).toBeTruthy(); 24 | }); 25 | it('should accept IBookmarkRepository as dependency', () => { 26 | expect(usecase['ibookmarkRepository']).toBeTruthy(); 27 | 28 | }); 29 | }) 30 | 31 | describe('execute', () => { 32 | it('should accept Param as input', () => { 33 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(RemoveBookmarkUsecase, ['execute']); 34 | 35 | usecase.execute(new Param(bookmark.id!)); 36 | const args = usecase.execute.calls.argsFor(0); 37 | 38 | expect(args[0]).toBeInstanceOf(Param); 39 | expect(args[0].payload).toBeDefined; 40 | expect(args[0].payload).toEqual(jasmine.stringContaining(bookmark.id!)); 41 | 42 | }); 43 | it('should return an Observable', () => { 44 | spyOn(usecase, 'execute').and.returnValue(of()) 45 | 46 | const result = usecase.execute(new Param(bookmark.id!)); 47 | 48 | expect(result).toBeInstanceOf(Observable); 49 | }); 50 | it('should call ibookmarkRepository.removeBookmark', () => { 51 | usecase.execute(new Param(bookmark.id!)); 52 | 53 | expect(usecase['ibookmarkRepository'].removeBookmark).toHaveBeenCalled(); 54 | 55 | }) 56 | it('should call ibookmarkRepository.removeBookmark with id and bookmark as input', () => { 57 | usecase.execute(new Param(bookmark.id!)); 58 | 59 | expect(usecase['ibookmarkRepository'].removeBookmark).toHaveBeenCalledWith(bookmark.id!,); 60 | 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/remove-usecase/remove-bookmark.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | import { Result } from 'src/app/core/types/types'; 5 | import { IBookmarkRepository } from '../../../repositories/ibookmark.repository'; 6 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class RemoveBookmarkUsecase implements Usecase, Observable> { 10 | 11 | constructor(private ibookmarkRepository: IBookmarkRepository) { } 12 | 13 | execute(param: Param): Observable { 14 | return this.ibookmarkRepository.removeBookmark(param.payload); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/usecases/bookmark-usecases/update-usecase/update-bookmark.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { BookmarkEntity } from '../../../entities'; 5 | import { Result } from '../../../../core/types/types'; 6 | import { Param } from '../../../../core/params/param.payload'; 7 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 8 | import { IBookmarkRepository } from '../../../repositories/ibookmark.repository'; 9 | 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class UpdateBookmarkUsecase implements Usecase, Observable>{ 13 | 14 | constructor(private ibookmarkRepository: IBookmarkRepository) { } 15 | 16 | execute(param: Param): Observable { 17 | const { id, ...bookmark } = param.payload; 18 | return this.ibookmarkRepository.updateBookmark(id!, bookmark); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/create-usecase/create-post.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | import { MOCK_POSTS } from './../../../../data/datasources/remote/repo-implementations/post/post.repository.spec'; 8 | 9 | import { CreatePostUsecase } from './create-post.usecase'; 10 | 11 | 12 | describe('CreatePostUsecase', () => { 13 | let usecase: CreatePostUsecase; 14 | let ipostRepository: jasmine.SpyObj; 15 | let post: PostEntity; 16 | 17 | beforeEach(() => { 18 | ipostRepository = jasmine.createSpyObj(IPostRepository, ['createPost']); 19 | usecase = new CreatePostUsecase(ipostRepository); 20 | post = MOCK_POSTS[0]; 21 | }); 22 | 23 | describe('constructor', () => { 24 | it('should be created', () => { 25 | expect(usecase).toBeTruthy(); 26 | }) 27 | it('should be created with dependency', () => { 28 | expect(usecase['ipostRepository']).toBeTruthy(); 29 | }) 30 | }) 31 | 32 | describe('execute', () => { 33 | it('should accept Param as input', () => { 34 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(CreatePostUsecase, ['execute']); 35 | 36 | usecase.execute(new Param(post)); 37 | const args = usecase.execute.calls.argsFor(0); 38 | 39 | expect(args[0]).toBeInstanceOf(Param); 40 | expect(args[0].payload).toBeDefined; 41 | expect(args[0].payload).toEqual(jasmine.objectContaining(post)); 42 | }) 43 | 44 | it('should return an Observable ', () => { 45 | spyOn(usecase, 'execute').and.returnValue(of()) 46 | 47 | const result = usecase.execute(new Param(post)); 48 | 49 | expect(result).toBeInstanceOf(Observable); 50 | 51 | }); 52 | 53 | it('should call ipostRepository.createPost with param', () => { 54 | usecase.execute(new Param(post)); 55 | 56 | expect(usecase['ipostRepository'].createPost).toHaveBeenCalled(); 57 | expect(usecase['ipostRepository'].createPost).toHaveBeenCalledWith(post); 58 | }); 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/create-usecase/create-post.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Result } from 'src/app/core/types/types'; 6 | import { Param } from 'src/app/core/params/param.payload'; 7 | 8 | import { PostEntity } from '../../../entities'; 9 | import { IPostRepository } from '../../../repositories/ipost.repository'; 10 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 11 | 12 | 13 | @Injectable({ providedIn: 'root' }) 14 | export class CreatePostUsecase implements Usecase, Observable> { 15 | 16 | constructor(private ipostRepository: IPostRepository) { } 17 | 18 | execute(param: Param): Observable { 19 | return this.ipostRepository.createPost(param.payload); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/get-many-usecase/get-many-posts.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | import { NoParam } from './../../../../core/params/no-param.paylod'; 8 | import { MOCK_POSTS } from '../../../../data/datasources/remote/repo-implementations/post/post.repository.spec'; 9 | 10 | import { GetManyPostUsecase } from './get-many-posts.usecase'; 11 | 12 | describe('GetManyPostUsecase', () => { 13 | let usecase: GetManyPostUsecase; 14 | let iPostRepository: jasmine.SpyObj; 15 | let post: PostEntity; 16 | 17 | beforeEach(() => { 18 | iPostRepository = jasmine.createSpyObj(IPostRepository, ['all']); 19 | usecase = new GetManyPostUsecase(iPostRepository); 20 | post = MOCK_POSTS[0]; 21 | }); 22 | 23 | describe('constructor', () => { 24 | it('should be created', () => { 25 | expect(usecase).toBeTruthy(); 26 | }) 27 | it('should be created with dependency', () => { 28 | expect(usecase['iPostRepository']).toBeTruthy(); 29 | }) 30 | }) 31 | 32 | describe('execute', () => { 33 | it('should accept NoParam as input', () => { 34 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(GetManyPostUsecase, ['execute']); 35 | 36 | usecase.execute(new NoParam()); 37 | const args = usecase.execute.calls.argsFor(0); 38 | 39 | expect(args[0]).toBeInstanceOf(NoParam); 40 | expect(args[0]).toBeDefined; 41 | expect(args[0]).toEqual(jasmine.objectContaining(new NoParam())); 42 | }) 43 | 44 | it('should return an Observable ', () => { 45 | spyOn(usecase, 'execute').and.returnValue(of()) 46 | 47 | const result = usecase.execute(new Param(post)); 48 | 49 | expect(result).toBeInstanceOf(Observable); 50 | 51 | }); 52 | 53 | it('should call iPostRepository.all with no param', () => { 54 | usecase.execute(new NoParam()); 55 | 56 | expect(usecase['iPostRepository'].all).toHaveBeenCalled(); 57 | expect(usecase['iPostRepository'].all).toHaveBeenCalledWith(); 58 | }); 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/get-many-usecase/get-many-posts.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { NoParam } from '../../../../core/params/no-param.paylod'; 4 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class GetManyPostUsecase implements Usecase> { 10 | 11 | constructor(private iPostRepository: IPostRepository) { } 12 | 13 | execute(payload: NoParam): Observable { 14 | return this.iPostRepository.all(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/get-one-usecase/get-one-post.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | import { MOCK_POSTS } from '../../../../data/datasources/remote/repo-implementations/post/post.repository.spec'; 8 | 9 | import { GetOnePostUsecase } from './get-one-post.usecase'; 10 | 11 | describe('GetOnePostUsecase', () => { 12 | let usecase: GetOnePostUsecase; 13 | let iPostRepository: jasmine.SpyObj; 14 | let post: PostEntity; 15 | 16 | beforeEach(() => { 17 | iPostRepository = jasmine.createSpyObj(IPostRepository, ['getPost']); 18 | usecase = new GetOnePostUsecase(iPostRepository); 19 | post = MOCK_POSTS[0]; 20 | }); 21 | 22 | describe('constructor', () => { 23 | it('should be created', () => { 24 | expect(usecase).toBeTruthy(); 25 | }) 26 | it('should be created with dependency', () => { 27 | expect(usecase['iPostRepository']).toBeTruthy(); 28 | }) 29 | }) 30 | 31 | describe('execute', () => { 32 | it('should accept a Param as input', () => { 33 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(GetOnePostUsecase, ['execute']); 34 | 35 | usecase.execute(new Param(post.id!)); 36 | const args = usecase.execute.calls.argsFor(0); 37 | 38 | expect(args[0]).toBeInstanceOf(Param); 39 | expect(args[0]).toBeDefined; 40 | expect(args[0]).toEqual(jasmine.objectContaining(new Param(post.id!))); 41 | }) 42 | 43 | it('should return an Observable ', () => { 44 | spyOn(usecase, 'execute').and.returnValue(of()) 45 | 46 | const result = usecase.execute(new Param(post.id!)); 47 | 48 | expect(result).toBeInstanceOf(Observable); 49 | }); 50 | 51 | it('should call iPostRepository.getPost with param', () => { 52 | usecase.execute(new Param(post.id!)); 53 | 54 | expect(usecase['iPostRepository'].getPost).toHaveBeenCalled(); 55 | expect(usecase['iPostRepository'].getPost).toHaveBeenCalledWith(post.id!); 56 | }); 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/get-one-usecase/get-one-post.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { Param } from '../../../../core/params/param.payload'; 5 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 6 | import { PostEntity } from '../../../entities'; 7 | import { IPostRepository } from '../../../repositories/ipost.repository'; 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export class GetOnePostUsecase implements Usecase, Observable> { 11 | 12 | constructor(private iPostRepository: IPostRepository) { } 13 | 14 | execute(param: Param): Observable { 15 | return this.iPostRepository.getPost(param.payload); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/remove-usecase/remove-post.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | import { MOCK_POSTS } from '../../../../data/datasources/remote/repo-implementations/post/post.repository.spec'; 8 | 9 | import { RemovePostUsecase } from './remove-post.usecase'; 10 | 11 | describe('GetOnePostUsecase', () => { 12 | let usecase: RemovePostUsecase; 13 | let iPostRepository: jasmine.SpyObj; 14 | let post: PostEntity; 15 | 16 | beforeEach(() => { 17 | iPostRepository = jasmine.createSpyObj(IPostRepository, ['removePost']); 18 | usecase = new RemovePostUsecase(iPostRepository); 19 | post = MOCK_POSTS[0]; 20 | }); 21 | 22 | describe('constructor', () => { 23 | it('should be created', () => { 24 | expect(usecase).toBeTruthy(); 25 | }) 26 | it('should be created with dependency', () => { 27 | expect(usecase['iPostRepository']).toBeTruthy(); 28 | }) 29 | }) 30 | 31 | describe('execute', () => { 32 | it('should accept a Param as input', () => { 33 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(RemovePostUsecase, ['execute']); 34 | 35 | usecase.execute(new Param(post.id!)); 36 | const args = usecase.execute.calls.argsFor(0); 37 | 38 | expect(args[0]).toBeInstanceOf(Param); 39 | expect(args[0]).toBeDefined; 40 | expect(args[0]).toEqual(jasmine.objectContaining(new Param(post.id!))); 41 | }) 42 | 43 | it('should return an Observable ', () => { 44 | spyOn(usecase, 'execute').and.returnValue(of()) 45 | 46 | const result = usecase.execute(new Param(post.id!)); 47 | 48 | expect(result).toBeInstanceOf(Observable); 49 | }); 50 | 51 | it('should call iPostRepository.getPost with param', () => { 52 | usecase.execute(new Param(post.id!)); 53 | 54 | expect(usecase['iPostRepository'].removePost).toHaveBeenCalled(); 55 | expect(usecase['iPostRepository'].removePost).toHaveBeenCalledWith(post.id!); 56 | }); 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/remove-usecase/remove-post.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Param } from 'src/app/core/params/param.payload'; 6 | import { Result } from 'src/app/core/types/types'; 7 | 8 | import { IPostRepository } from '../../../repositories/ipost.repository'; 9 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class RemovePostUsecase implements Usecase, Observable> { 13 | 14 | constructor(private iPostRepository: IPostRepository) { } 15 | 16 | execute(param: Param): Observable { 17 | return this.iPostRepository.removePost(param.payload); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/update-usecase/update-post.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | import { MOCK_POSTS } from '../../../../data/datasources/remote/repo-implementations/post/post.repository.spec'; 8 | 9 | import { UpdatePostUsecase } from './update-post.usecase'; 10 | 11 | 12 | describe('UpdatePostUsecase', () => { 13 | let usecase: UpdatePostUsecase; 14 | let iPostRepository: jasmine.SpyObj; 15 | let post: PostEntity; 16 | 17 | beforeEach(() => { 18 | iPostRepository = jasmine.createSpyObj(IPostRepository, ['updatePost']); 19 | usecase = new UpdatePostUsecase(iPostRepository); 20 | post = MOCK_POSTS[0]; 21 | }); 22 | 23 | describe('constructor', () => { 24 | it('should be created', () => { 25 | expect(usecase).toBeTruthy(); 26 | }) 27 | it('should be created with dependency', () => { 28 | expect(usecase['iPostRepository']).toBeTruthy(); 29 | }) 30 | }) 31 | 32 | describe('execute', () => { 33 | it('should accept a Param as input', () => { 34 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(UpdatePostUsecase, ['execute']); 35 | 36 | usecase.execute(new Param(post)); 37 | const args = usecase.execute.calls.argsFor(0); 38 | 39 | expect(args[0]).toBeInstanceOf(Param); 40 | expect(args[0]).toBeDefined; 41 | expect(args[0]).toEqual(jasmine.objectContaining(new Param(post))); 42 | }) 43 | 44 | it('should return an Observable ', () => { 45 | spyOn(usecase, 'execute').and.returnValue(of()) 46 | 47 | const result = usecase.execute(new Param(post)); 48 | 49 | expect(result).toBeInstanceOf(Observable); 50 | }); 51 | 52 | it('should call iPostRepository.updatePost with param', () => { 53 | const { id, ...rest } = post; 54 | 55 | usecase.execute(new Param(post)); 56 | 57 | expect(usecase['iPostRepository'].updatePost).toHaveBeenCalled(); 58 | expect(usecase['iPostRepository'].updatePost).toHaveBeenCalledWith(id!, rest); 59 | }); 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/app/domain/usecases/posts-usecases/update-usecase/update-post.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { PostEntity } from '../../../entities'; 6 | import { IPostRepository } from '../../../repositories/ipost.repository'; 7 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 8 | import { Param } from '../../../../core/params/param.payload'; 9 | import { Result } from '../../../../core/types/types'; 10 | 11 | 12 | @Injectable({ providedIn: 'root' }) 13 | export class UpdatePostUsecase implements Usecase, Observable>{ 14 | 15 | constructor(private iPostRepository: IPostRepository) { } 16 | 17 | execute(param: Param): Observable { 18 | const { id, ...Post } = param.payload; 19 | return this.iPostRepository.updatePost(id!, Post); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/create-usecase/create-project.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | 3 | import { Param } from "src/app/core/params/param.payload"; 4 | import { ProjectEntity } from "src/app/domain/entities"; 5 | import { IProjectRepository } from "src/app/domain/repositories/iproject.repository"; 6 | 7 | import { MOCK_PROJECT } from './../../../../data/datasources/remote/repo-implementations/project/project.repository.spec'; 8 | 9 | import { CreateProjectUsecase } from "./create-project.usecase"; 10 | 11 | describe('CreateProjectUsecase', () => { 12 | let usecase: CreateProjectUsecase; 13 | let iProjectRepository: jasmine.SpyObj; 14 | let project: ProjectEntity; 15 | 16 | beforeEach(() => { 17 | iProjectRepository = jasmine.createSpyObj(IProjectRepository, ['createProject']); 18 | usecase = new CreateProjectUsecase(iProjectRepository); 19 | project = MOCK_PROJECT[0]; 20 | }); 21 | 22 | describe('constructor', () => { 23 | it('should be created', () => { 24 | expect(usecase).toBeTruthy(); 25 | }) 26 | it('should be created with dependency', () => { 27 | expect(usecase['iProjectRepository']).toBeTruthy(); 28 | }) 29 | }) 30 | 31 | describe('execute', () => { 32 | it('should accept Param as input', () => { 33 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(CreateProjectUsecase, ['execute']); 34 | 35 | usecase.execute(new Param(project)); 36 | const args = usecase.execute.calls.argsFor(0); 37 | 38 | expect(args[0]).toBeInstanceOf(Param); 39 | expect(args[0].payload).toBeDefined; 40 | expect(args[0].payload).toEqual(jasmine.objectContaining(project)); 41 | }) 42 | 43 | it('should return an Observable ', () => { 44 | spyOn(usecase, 'execute').and.returnValue(of()) 45 | 46 | const result = usecase.execute(new Param(project)); 47 | 48 | expect(result).toBeInstanceOf(Observable); 49 | 50 | }); 51 | 52 | it('should call iProjectRepository.createProject with param', () => { 53 | usecase.execute(new Param(project)); 54 | 55 | expect(usecase['iProjectRepository'].createProject).toHaveBeenCalled(); 56 | expect(usecase['iProjectRepository'].createProject).toHaveBeenCalledWith(project); 57 | }); 58 | }) 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/create-usecase/create-project.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ProjectEntity } from '../../../entities'; 5 | import { Result } from 'src/app/core/types/types'; 6 | import { Param } from 'src/app/core/params/param.payload'; 7 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 8 | import { IProjectRepository } from '../../../repositories/iproject.repository'; 9 | 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class CreateProjectUsecase implements Usecase, Observable> { 13 | 14 | constructor(private iProjectRepository: IProjectRepository) { } 15 | 16 | execute(param: Param): Observable { 17 | return this.iProjectRepository.createProject(param.payload); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/get-many-usecase/get-many-project.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | 3 | import { NoParam } from "src/app/core/params/no-param.paylod"; 4 | import { MOCK_PROJECT } from "src/app/data/datasources/remote/repo-implementations/project/project.repository.spec"; 5 | import { ProjectEntity } from "src/app/domain/entities"; 6 | import { IProjectRepository } from "src/app/domain/repositories/iproject.repository"; 7 | 8 | import { GetManyProjectUsecase } from "./get-many-project.usecase"; 9 | 10 | describe('GetManyProjectUsecase', () => { 11 | let usecase: GetManyProjectUsecase; 12 | let iProjectRepository: jasmine.SpyObj; 13 | let project: ProjectEntity; 14 | 15 | beforeEach(() => { 16 | iProjectRepository = jasmine.createSpyObj(IProjectRepository, ['all']); 17 | usecase = new GetManyProjectUsecase(iProjectRepository); 18 | project = MOCK_PROJECT[0]; 19 | }); 20 | 21 | describe('constructor', () => { 22 | it('should be created', () => { 23 | expect(usecase).toBeTruthy(); 24 | }) 25 | it('should be created with dependency', () => { 26 | expect(usecase['iProjectRepository']).toBeTruthy(); 27 | }) 28 | }) 29 | 30 | describe('execute', () => { 31 | it('should accept NoParam as input', () => { 32 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(GetManyProjectUsecase, ['execute']); 33 | 34 | usecase.execute(new NoParam()); 35 | const args = usecase.execute.calls.argsFor(0); 36 | 37 | expect(args[0]).toBeInstanceOf(NoParam); 38 | expect(args[0]).toBeDefined; 39 | 40 | }) 41 | 42 | it('should return an Observable ', () => { 43 | spyOn(usecase, 'execute').and.returnValue(of()) 44 | 45 | const result = usecase.execute(new NoParam()); 46 | 47 | expect(result).toBeInstanceOf(Observable); 48 | 49 | }); 50 | 51 | it('should call iProjectRepository.all with no param', () => { 52 | usecase.execute(new NoParam()); 53 | 54 | expect(usecase['iProjectRepository'].all).toHaveBeenCalled(); 55 | expect(usecase['iProjectRepository'].all).toHaveBeenCalledWith(); 56 | }); 57 | }) 58 | }); 59 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/get-many-usecase/get-many-project.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { NoParam } from '../../../../core/params/no-param.paylod'; 4 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 5 | import { ProjectEntity } from '../../../entities'; 6 | import { IProjectRepository } from '../../../repositories/iproject.repository'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class GetManyProjectUsecase implements Usecase> { 10 | 11 | constructor(private iProjectRepository: IProjectRepository) { } 12 | 13 | execute(payload: NoParam): Observable { 14 | return this.iProjectRepository.all(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/get-one-usecase/get-one-project.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | 3 | import { Param } from "src/app/core/params/param.payload"; 4 | import { MOCK_PROJECT } from "src/app/data/datasources/remote/repo-implementations/project/project.repository.spec"; 5 | import { ProjectEntity } from "src/app/domain/entities"; 6 | import { IProjectRepository } from "src/app/domain/repositories/iproject.repository"; 7 | 8 | import { GetOneProjectUsecase } from "./get-one-project.usecase"; 9 | 10 | describe('GetOneProjectUsecase', () => { 11 | let usecase: GetOneProjectUsecase; 12 | let iProjectRepository: jasmine.SpyObj; 13 | let project: ProjectEntity; 14 | 15 | beforeEach(() => { 16 | iProjectRepository = jasmine.createSpyObj(IProjectRepository, ['getProject']); 17 | usecase = new GetOneProjectUsecase(iProjectRepository); 18 | project = MOCK_PROJECT[0]; 19 | }); 20 | 21 | describe('constructor', () => { 22 | it('should be created', () => { 23 | expect(usecase).toBeTruthy(); 24 | }) 25 | it('should be created with dependency', () => { 26 | expect(usecase['iProjectRepository']).toBeTruthy(); 27 | }) 28 | }) 29 | 30 | describe('execute', () => { 31 | it('should accept Param as input', () => { 32 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(GetOneProjectUsecase, ['execute']); 33 | 34 | usecase.execute(new Param(project.id!)); 35 | const args = usecase.execute.calls.argsFor(0); 36 | 37 | expect(args[0]).toBeInstanceOf(Param); 38 | expect(args[0]).toBeDefined; 39 | expect(args[0].payload).toEqual(jasmine.stringContaining(project.id!)); 40 | }) 41 | 42 | it('should return an Observable ', () => { 43 | spyOn(usecase, 'execute').and.returnValue(of()) 44 | 45 | const result = usecase.execute(new Param(project.id!)); 46 | 47 | expect(result).toBeInstanceOf(Observable); 48 | }); 49 | 50 | it('should call iProjectRepository.getProject with param', () => { 51 | usecase.execute(new Param(project.id!)); 52 | 53 | expect(usecase['iProjectRepository'].getProject).toHaveBeenCalled(); 54 | expect(usecase['iProjectRepository'].getProject).toHaveBeenCalledWith(project.id!); 55 | }); 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/get-one-usecase/get-one-project.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { Param } from '../../../../core/params/param.payload'; 5 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 6 | import { ProjectEntity } from '../../../entities'; 7 | import { IProjectRepository } from '../../../repositories/iproject.repository'; 8 | 9 | @Injectable({ providedIn: 'root' }) 10 | export class GetOneProjectUsecase implements Usecase, Observable> { 11 | 12 | constructor(private iProjectRepository: IProjectRepository) { } 13 | 14 | execute(param: Param): Observable { 15 | return this.iProjectRepository.getProject(param.payload); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/remove-usecase/remove-project.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | import { Param } from "src/app/core/params/param.payload"; 3 | import { MOCK_PROJECT } from "src/app/data/datasources/remote/repo-implementations/project/project.repository.spec"; 4 | import { ProjectEntity } from "src/app/domain/entities"; 5 | import { IProjectRepository } from "src/app/domain/repositories/iproject.repository"; 6 | import { RemoveProjectUsecase } from "./remove-project.usecase"; 7 | 8 | describe('RemoveProjectUsecase', () => { 9 | let usecase: RemoveProjectUsecase; 10 | let iProjectRepository: jasmine.SpyObj; 11 | let project: ProjectEntity; 12 | 13 | beforeEach(() => { 14 | iProjectRepository = jasmine.createSpyObj(IProjectRepository, ['removeProject']); 15 | usecase = new RemoveProjectUsecase(iProjectRepository); 16 | project = MOCK_PROJECT[0]; 17 | }); 18 | 19 | describe('constructor', () => { 20 | it('should be created', () => { 21 | expect(usecase).toBeTruthy(); 22 | }) 23 | it('should be created with dependency', () => { 24 | expect(usecase['iProjectRepository']).toBeTruthy(); 25 | }) 26 | }) 27 | 28 | describe('execute', () => { 29 | it('should accept Param as input', () => { 30 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(RemoveProjectUsecase, ['execute']); 31 | 32 | usecase.execute(new Param(project.id!)); 33 | const args = usecase.execute.calls.argsFor(0); 34 | 35 | expect(args[0]).toBeInstanceOf(Param); 36 | expect(args[0]).toBeDefined; 37 | expect(args[0].payload).toEqual(jasmine.stringContaining(project.id!)); 38 | }) 39 | 40 | it('should return an Observable ', () => { 41 | spyOn(usecase, 'execute').and.returnValue(of()) 42 | 43 | const result = usecase.execute(new Param(project.id!)); 44 | 45 | expect(result).toBeInstanceOf(Observable); 46 | }); 47 | 48 | it('should call iProjectRepository.removeProject with param', () => { 49 | usecase.execute(new Param(project.id!)); 50 | 51 | expect(usecase['iProjectRepository'].removeProject).toHaveBeenCalled(); 52 | expect(usecase['iProjectRepository'].removeProject).toHaveBeenCalledWith(project.id!); 53 | }); 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/remove-usecase/remove-project.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Param } from 'src/app/core/params/param.payload'; 4 | import { Result } from 'src/app/core/types/types'; 5 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 6 | import { IProjectRepository } from '../../../repositories/iproject.repository'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class RemoveProjectUsecase implements Usecase, Observable> { 10 | 11 | constructor(private iProjectRepository: IProjectRepository) { } 12 | 13 | execute(param: Param): Observable { 14 | return this.iProjectRepository.removeProject(param.payload); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/update-usecase/update-project.usecase.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from "rxjs"; 2 | import { Param } from "src/app/core/params/param.payload"; 3 | import { MOCK_PROJECT } from "src/app/data/datasources/remote/repo-implementations/project/project.repository.spec"; 4 | import { ProjectEntity } from "src/app/domain/entities"; 5 | import { IProjectRepository } from "src/app/domain/repositories/iproject.repository"; 6 | import { UpdateProjectUsecase } from "./update-project.usecase"; 7 | 8 | describe('UpdateProjectUsecase', () => { 9 | let usecase: UpdateProjectUsecase; 10 | let iProjectRepository: jasmine.SpyObj; 11 | let project: ProjectEntity; 12 | 13 | beforeEach(() => { 14 | iProjectRepository = jasmine.createSpyObj(IProjectRepository, ['updateProject']); 15 | usecase = new UpdateProjectUsecase(iProjectRepository); 16 | project = MOCK_PROJECT[0]; 17 | }); 18 | 19 | describe('constructor', () => { 20 | it('should be created', () => { 21 | expect(usecase).toBeTruthy(); 22 | }) 23 | it('should be created with dependency', () => { 24 | expect(usecase['iProjectRepository']).toBeTruthy(); 25 | }) 26 | }) 27 | 28 | describe('execute', () => { 29 | it('should accept Param as input', () => { 30 | let usecase: jasmine.SpyObj = jasmine.createSpyObj(UpdateProjectUsecase, ['execute']); 31 | 32 | usecase.execute(new Param(project)); 33 | const args = usecase.execute.calls.argsFor(0); 34 | 35 | expect(args[0]).toBeInstanceOf(Param); 36 | expect(args[0]).toBeDefined; 37 | expect(args[0].payload).toEqual(jasmine.objectContaining(project)); 38 | }) 39 | 40 | it('should return an Observable ', () => { 41 | spyOn(usecase, 'execute').and.returnValue(of()) 42 | 43 | const result = usecase.execute(new Param(project)); 44 | 45 | expect(result).toBeInstanceOf(Observable); 46 | }); 47 | 48 | it('should call iProjectRepository.updateProject with param', () => { 49 | const { id, ...rest } = project; 50 | usecase.execute(new Param(project)); 51 | 52 | expect(usecase['iProjectRepository'].updateProject).toHaveBeenCalled(); 53 | expect(usecase['iProjectRepository'].updateProject).toHaveBeenCalledWith(id!, rest); 54 | }); 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/app/domain/usecases/projects-usecases/update-usecase/update-project.usecase.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { ProjectEntity } from '../../../entities'; 5 | import { Result } from '../../../../core/types/types'; 6 | import { Param } from '../../../../core/params/param.payload'; 7 | import { Usecase } from '../../../../core/contracts/usecase.contract'; 8 | import { IProjectRepository } from '../../../repositories/iproject.repository'; 9 | 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class UpdateProjectUsecase implements Usecase, Observable>{ 13 | 14 | constructor(private iProjectRepository: IProjectRepository) { } 15 | 16 | execute(param: Param): Observable { 17 | const { id, ...project } = param.payload; 18 | return this.iProjectRepository.updateProject(id!, project); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/presenter/components/components.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ComponentModule } from './components.module'; 4 | describe('ComponentModule', () => { 5 | beforeEach(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [ComponentModule] 8 | }) 9 | }) 10 | 11 | it('should initialize', () => { 12 | const module = TestBed.inject(ComponentModule); 13 | 14 | expect(module).toBeTruthy(); 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/editor-header/editor-header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 12 |
    13 | 14 | 15 | 16 |
17 |
-------------------------------------------------------------------------------- /src/app/presenter/components/layouts/editor-header/editor-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { LinkComponent } from './../../shared/link/link.component'; 6 | 7 | import { EditorHeaderComponent } from './editor-header.component'; 8 | describe('EditorHeaderComponent', () => { 9 | let fixture: ComponentFixture; 10 | let component: EditorHeaderComponent; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [EditorHeaderComponent, LinkComponent], 15 | providers: [], 16 | imports: [ 17 | RouterModule.forRoot([]) 18 | ] 19 | }).compileComponents(); 20 | 21 | fixture = TestBed.createComponent(EditorHeaderComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }) 29 | 30 | it('should have 3 LinkComponents', () => { 31 | const { debugElement } = fixture; 32 | 33 | const linkComponents = debugElement.queryAll(By.directive(LinkComponent)); 34 | 35 | expect(linkComponents.length).toBe(3); 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/editor-header/editor-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-editor-header', 5 | templateUrl: './editor-header.component.html', 6 | styles: [] 7 | }) 8 | export class EditorHeaderComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/editor-layout/editor-layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/editor-layout/editor-layout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { By } from '@angular/platform-browser'; 2 | import { RouterModule } from '@angular/router'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | 5 | import { EditorHeaderComponent } from './../editor-header/editor-header.component'; 6 | import { LinkComponent } from './../../shared/link/link.component'; 7 | 8 | import { EditorLayoutComponent } from './editor-layout.component'; 9 | 10 | describe('EditorLayoutComponent', () => { 11 | let component: EditorLayoutComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async () => { 15 | await TestBed.configureTestingModule({ 16 | declarations: [EditorLayoutComponent, EditorHeaderComponent, LinkComponent], 17 | imports: [RouterModule.forRoot([])] 18 | }) 19 | .compileComponents(); 20 | 21 | fixture = TestBed.createComponent(EditorLayoutComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | 30 | it('should create headerComponent', () => { 31 | 32 | const { debugElement } = fixture; 33 | const header = debugElement.query(By.directive(EditorHeaderComponent)).nativeElement; 34 | 35 | expect(header).toBeTruthy(); 36 | }) 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/editor-layout/editor-layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-editor-layout', 5 | templateUrl: './editor-layout.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class EditorLayoutComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [FooterComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(FooterComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class FooterComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/header/header.component.html: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { By } from '@angular/platform-browser'; 2 | import { RouterModule } from '@angular/router'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | 5 | import { ThemeSwitcherComponent } from './../../shared/theme-switcher/theme-switcher.component'; 6 | import { DarkThemeSwitchComponent } from './../../shared/theme-switcher/dark-theme-switch/dark-theme-switch.component'; 7 | 8 | import { HeaderComponent } from './header.component'; 9 | 10 | describe('HeaderComponent', () => { 11 | let component: HeaderComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async () => { 15 | await TestBed.configureTestingModule({ 16 | declarations: [HeaderComponent, ThemeSwitcherComponent, DarkThemeSwitchComponent], 17 | providers: [], 18 | imports: [RouterModule.forRoot([])] 19 | }) 20 | .compileComponents(); 21 | 22 | fixture = TestBed.createComponent(HeaderComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | 31 | it('should load theme switcher component', () => { 32 | const { debugElement } = fixture; 33 | 34 | const switcherElement = debugElement.query(By.directive(ThemeSwitcherComponent)); 35 | 36 | expect(switcherElement).toBeTruthy(); 37 | }) 38 | }); 39 | -------------------------------------------------------------------------------- /src/app/presenter/components/layouts/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header', 5 | templateUrl: './header.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class HeaderComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/button/button.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/button/button.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ButtonComponent } from './button.component'; 4 | 5 | describe('ButtonComponent', () => { 6 | let component: ButtonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ButtonComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ButtonComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | 24 | describe('props', () => { 25 | let props: { 26 | text?: string, 27 | type?: string, 28 | loading?: boolean, 29 | } 30 | beforeEach(() => { 31 | props = { 32 | text: 'Fake Button', 33 | type: 'button', 34 | loading: false, 35 | }; 36 | }) 37 | 38 | it('should accept ButtonProps as input', () => { 39 | 40 | component.props = props; 41 | 42 | expect(component.props).toBeTruthy(); 43 | 44 | expect(component.props).toEqual(props); 45 | 46 | }) 47 | }) 48 | 49 | describe('action', () => { 50 | 51 | let action: () => void; 52 | 53 | beforeEach(() => { 54 | action = () => { } 55 | component.action = action; 56 | }) 57 | 58 | it('should accept a ButtonAction as input', () => { 59 | expect(component.action).toEqual(action); 60 | }) 61 | }) 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | export type ButtonProps = { 4 | text?: string, 5 | type?: string, 6 | loading?: boolean, 7 | } 8 | 9 | 10 | @Component({ 11 | selector: 'app-button', 12 | templateUrl: './button.component.html', 13 | styles: [ 14 | ] 15 | }) 16 | export class ButtonComponent { 17 | 18 | @Input() 19 | props: ButtonProps = { type: 'button', loading: false }; 20 | 21 | @Input() 22 | action: () => void = () => { } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/form-field/form-field.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 17 | 18 | 19 |
-------------------------------------------------------------------------------- /src/app/presenter/components/shared/form-field/form-field.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { By } from '@angular/platform-browser'; 2 | import { FormGroup, ReactiveFormsModule, FormControl } from '@angular/forms'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | 5 | import { FormFieldComponent } from './form-field.component'; 6 | 7 | describe('FormFieldComponent', () => { 8 | let component: FormFieldComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | declarations: [FormFieldComponent], 14 | imports: [ReactiveFormsModule] 15 | }).compileComponents(); 16 | 17 | fixture = TestBed.createComponent(FormFieldComponent); 18 | component = fixture.componentInstance; 19 | 20 | component.props = { 21 | formGroup: new FormGroup({ 22 | FakeInput: new FormControl(), 23 | }), 24 | controlName: "FakeInput", 25 | isValid: (arg: string) => { }, 26 | }; 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('should create', () => { 31 | expect(component).toBeTruthy(); 32 | }); 33 | 34 | describe('Input props', () => { 35 | 36 | it('should receive props as Input', () => { 37 | const props = component.props; 38 | 39 | expect(props).toBeDefined(); 40 | expect(props?.controlName).toEqual('FakeInput'); 41 | }); 42 | 43 | it('should correctly render the passed @Input value', () => { 44 | const { debugElement } = fixture; 45 | 46 | const label = debugElement.nativeElement.querySelector('label'); 47 | 48 | expect(label.innerText).toContain(component.props?.controlName); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/form-field/form-field.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | export type FormFieldProps = { 5 | controlName: string, 6 | isValid: (arg: string) => void, 7 | formGroup: FormGroup, 8 | type?: string 9 | } 10 | 11 | @Component({ 12 | selector: 'app-form-field', 13 | templateUrl: './form-field.component.html', 14 | styles: [ 15 | ] 16 | }) 17 | export class FormFieldComponent { 18 | 19 | @Input() 20 | props?: FormFieldProps; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/link/link.component.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 5 | {{props?.title}} 6 | 7 |
  • -------------------------------------------------------------------------------- /src/app/presenter/components/shared/link/link.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@angular/router'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { LinkComponent } from './link.component'; 5 | 6 | describe('LinkComponent', () => { 7 | let component: LinkComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [LinkComponent], 13 | imports: [RouterModule.forRoot([])], 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(LinkComponent); 17 | component = fixture.componentInstance; 18 | component.props = { link: 'fakelink.cm', title: 'FakeTitle' }; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | 26 | describe('@Input Props', () => { 27 | it('should receive props as Input', () => { 28 | expect(component.props).toBeDefined(); 29 | expect(component.props?.title).toContain('Fake'); 30 | }); 31 | 32 | it('should render the Input props value correctly', () => { 33 | const { debugElement } = fixture; 34 | 35 | const anchor = debugElement.nativeElement.querySelector('a'); 36 | 37 | expect(anchor.innerText).toEqual(component.props?.title); 38 | expect(anchor.href).toContain(component.props?.link); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/link/link.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-link', 5 | templateUrl: './link.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class LinkComponent { 10 | 11 | @Input() 12 | props?: { link: string, title: string }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/modal/modal.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    4 | 29 |
    30 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/modal/modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ModalService } from 'src/app/presenter/components/shared/modal/modal.service'; 4 | 5 | import { ModalComponent } from './modal.component'; 6 | 7 | describe('ModalComponent', () => { 8 | let component: ModalComponent; 9 | let fixture: ComponentFixture>; 10 | let modalService: jasmine.SpyObj>; 11 | 12 | let serviceProps = { 13 | title: 'FakeModalTitle', 14 | }; 15 | 16 | beforeEach(() => { 17 | modalService = jasmine.createSpyObj(ModalService, [ 18 | 'close', 19 | 'open', 20 | 'resolveComponent', 21 | 'appendComponentToHtmlBody', 22 | ]); 23 | 24 | modalService['title'] = serviceProps.title; 25 | 26 | TestBed.configureTestingModule({ 27 | declarations: [ModalComponent], 28 | providers: [{ provide: ModalService, useValue: modalService }], 29 | }).compileComponents(); 30 | 31 | fixture = TestBed.createComponent(ModalComponent); 32 | component = fixture.componentInstance; 33 | 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | 41 | describe('constructor', () => { 42 | it('should set title property', () => { 43 | expect(component.title).toEqual(serviceProps.title); 44 | }); 45 | }); 46 | 47 | describe('close', () => { 48 | it('should set display property to false', () => { 49 | component.close(); 50 | 51 | expect(component.display).toBeFalse(); 52 | }); 53 | 54 | it('should call service.close method', async () => { 55 | component.close(); 56 | 57 | expect(modalService.close).toHaveBeenCalled(); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/modal/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ModalService } from './modal.service'; 4 | 5 | @Component({ 6 | selector: 'app-modal', 7 | templateUrl: './modal.component.html', 8 | styles: [ 9 | ` 10 | section { 11 | visibility: hidden; 12 | opacity: 0; 13 | transition: opacity 250ms ease-in; 14 | } 15 | section.open { 16 | visibility: inherit; 17 | opacity: 1; 18 | } 19 | `, 20 | ], 21 | }) 22 | export class ModalComponent { 23 | public display = true; 24 | public title = ''; 25 | 26 | public constructor(private service: ModalService) { 27 | this.title = service.title!; 28 | } 29 | 30 | public close = () => { 31 | this.display = false; 32 | this.service.close(); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/modal/modal.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationRef, 3 | ComponentFactoryResolver, 4 | ComponentRef, 5 | EmbeddedViewRef, 6 | Injectable, 7 | Injector, 8 | Type, 9 | } from '@angular/core'; 10 | 11 | export type ComponentInput = { 12 | inputTitle: string; 13 | inputValue: U; 14 | }; 15 | 16 | export type ComponentParam = { 17 | component: Type; 18 | modalTitle?: string; 19 | }; 20 | 21 | @Injectable({ 22 | providedIn: 'root', 23 | }) 24 | export class ModalService { 25 | public title?: string; 26 | private componentRef?: ComponentRef; 27 | 28 | constructor( 29 | private componentFactoryResolver: ComponentFactoryResolver, 30 | private appRef: ApplicationRef, 31 | private injector: Injector 32 | ) {} 33 | 34 | public async close(): Promise { 35 | if (!this.componentRef) return; 36 | 37 | window.setTimeout(() => { 38 | if (this.componentRef) { 39 | this.appRef.detachView(this.componentRef.hostView); 40 | this.componentRef.destroy(); 41 | this.componentRef = undefined; 42 | } 43 | }, 1000); 44 | } 45 | 46 | public async open( 47 | param: ComponentParam, 48 | input?: ComponentInput 49 | ): Promise { 50 | if (this.componentRef) return; 51 | 52 | this.title = param.modalTitle; 53 | this.resolveComponent(param.component, input); 54 | this.appendComponentToHtmlBody(); 55 | } 56 | 57 | private resolveComponent( 58 | component: Type, 59 | input?: ComponentInput 60 | ): void { 61 | // use componentFactoryResolver to create component 62 | this.componentRef = this.componentFactoryResolver 63 | .resolveComponentFactory(component) 64 | .create(this.injector); 65 | this.appRef.attachView(this.componentRef.hostView); 66 | 67 | // set a default input value in the component before modal opens 68 | if (input) 69 | this.componentRef.setInput(input.inputTitle, input?.inputValue); 70 | } 71 | 72 | private appendComponentToHtmlBody(): void { 73 | const domElement = (this.componentRef?.hostView as EmbeddedViewRef) 74 | .rootNodes[0] as HTMLElement; 75 | document.body.appendChild(domElement); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    5 | Error404 6 |

    7 |

    Sorry, we couldn't find this page.

    8 |

    But don't worry, you can find plenty of other things on our homepage. 9 |

    10 | Back to homepage 12 |
    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotFoundComponent } from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [NotFoundComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(NotFoundComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-not-found', 5 | templateUrl: './not-found.component.html', 6 | }) 7 | export class NotFoundComponent {} 8 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/blog-skeleton/blog-skeleton.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 | 19 |
    20 |
    21 |
    22 |
    23 |
    24 | 25 | Loading... 26 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/blog-skeleton/blog-skeleton.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BlogSkeletonComponent } from './blog-skeleton.component'; 4 | 5 | describe('BlogSkeletonComponent', () => { 6 | let component: BlogSkeletonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BlogSkeletonComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(BlogSkeletonComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/blog-skeleton/blog-skeleton.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-blog-skeleton', 5 | templateUrl: './blog-skeleton.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class BlogSkeletonComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/bookmarks-skeleton/bookmarks-skeleton.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |
    6 |
    7 |
    8 |
    9 | 10 |
    11 |
    12 |
    13 |
    14 | 15 | Loading... 16 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/bookmarks-skeleton/bookmarks-skeleton.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BookmarksSkeletonComponent } from './bookmarks-skeleton.component'; 4 | 5 | describe('BookmarksSkeletonComponent', () => { 6 | let component: BookmarksSkeletonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BookmarksSkeletonComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(BookmarksSkeletonComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/bookmarks-skeleton/bookmarks-skeleton.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-bookmarks-skeleton', 5 | templateUrl: './bookmarks-skeleton.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class BookmarksSkeletonComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/projects-skeleton/projects-skeleton.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |
    6 |
    7 |
    8 |
    9 | 10 |
    11 |
    12 |
    13 |
    14 | 15 | Loading... 16 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/projects-skeleton/projects-skeleton.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectsSkeletonComponent } from './projects-skeleton.component'; 4 | 5 | describe('ProjectsSkeletonComponent', () => { 6 | let component: ProjectsSkeletonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProjectsSkeletonComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ProjectsSkeletonComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/projects-skeleton/projects-skeleton.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-projects-skeleton', 5 | templateUrl: './projects-skeleton.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class ProjectsSkeletonComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/skeleton-loader.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 |
    6 | 7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |
    14 | 15 |
    16 |
    17 |
    18 |
    19 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/skeleton-loader.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SkeletonLoaderComponent } from './skeleton-loader.component'; 4 | 5 | describe('SkeletonLoaderComponent', () => { 6 | let component: SkeletonLoaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SkeletonLoaderComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SkeletonLoaderComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/skeleton-loader/skeleton-loader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | export enum PageEnum { 4 | blog, 5 | project, 6 | bookmark, 7 | } 8 | 9 | @Component({ 10 | selector: 'app-skeleton-loader', 11 | templateUrl: './skeleton-loader.component.html', 12 | }) 13 | export class SkeletonLoaderComponent { 14 | 15 | @Input() 16 | public pageEnum!: PageEnum; 17 | 18 | public blog = PageEnum.blog; 19 | public project = PageEnum.project; 20 | public bookmark = PageEnum.bookmark; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/table/table.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | 51 |
    6 | {{col.title}} 7 |
    19 | {{item[alias] }} 20 | 27 | {{title }} 28 | 34 | {{date}} 35 | 41 | Edit 43 | Remove 45 |
    52 |
    53 | 54 |

    No data yet

    55 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/table/table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TableComponent } from './table.component'; 4 | 5 | describe('TableComponent', () => { 6 | let component: TableComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TableComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TableComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/table/table.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Component, Input, OnInit } from '@angular/core'; 3 | 4 | export type TableData = { 5 | _id: string, 6 | title: string, 7 | date?: string, 8 | } 9 | 10 | type TableAction = { 11 | onRemove: (id: string, data?: T[]) => void; 12 | onEdit: (id: string, data?: T) => void; 13 | }; 14 | 15 | export type Table = { 16 | 17 | cols?: { title: string }[], 18 | data$?: Observable, 19 | action?: TableAction, 20 | alias?: string 21 | } 22 | 23 | 24 | @Component({ 25 | selector: 'app-table', 26 | templateUrl: './table.component.html', 27 | styles: [ 28 | ] 29 | }) 30 | export class TableComponent { 31 | 32 | @Input() 33 | payload?: Table; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/tag/tag.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TagComponent } from './tag.component'; 4 | 5 | describe('TagComponent', () => { 6 | let component: TagComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TagComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TagComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/tag/tag.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tag', 5 | template: ` 6 | 9 | {{text}} 10 | 11 | ` 12 | }) 13 | export class TagComponent { 14 | @Input() 15 | public text: string = ''; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/theme-switcher/dark-theme-switch/dark-theme-switch.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dark-switch', 5 | template: ` 6 | ` 10 | }) 11 | export class DarkThemeSwitchComponent { 12 | } 13 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/theme-switcher/light-theme-switch/light-theme-switch.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-light-switch', 5 | template: ` 6 | 20 | ` 21 | }) 22 | export class LightThemeSwitchComponent { 23 | } 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/theme-switcher/theme-switcher.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    -------------------------------------------------------------------------------- /src/app/presenter/components/shared/theme-switcher/theme-switcher.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ThemeSwitcherComponent } from './theme-switcher.component'; 4 | 5 | import { DarkThemeSwitchComponent } from './dark-theme-switch/dark-theme-switch.component'; 6 | import { LightThemeSwitchComponent } from './light-theme-switch/light-theme-switch.component'; 7 | 8 | describe('ThemeSwitcherComponent', () => { 9 | let component: ThemeSwitcherComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [ 15 | ThemeSwitcherComponent, 16 | DarkThemeSwitchComponent, 17 | LightThemeSwitchComponent, 18 | ], 19 | }).compileComponents(); 20 | 21 | fixture = TestBed.createComponent(ThemeSwitcherComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/theme-switcher/theme-switcher.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ThemeSwitcherService } from './theme-switcher.service'; 4 | 5 | @Component({ 6 | selector: 'app-theme-switcher', 7 | templateUrl: './theme-switcher.component.html', 8 | styles: [] 9 | }) 10 | export class ThemeSwitcherComponent implements OnInit { 11 | 12 | public isDarkTheme?: boolean; 13 | 14 | public constructor(private themeService: ThemeSwitcherService) { } 15 | 16 | ngOnInit(): void { 17 | this.themeService 18 | .pref$.subscribe(res => this.isDarkTheme = res === 'dark'); 19 | } 20 | 21 | public toggleTheme = () => { 22 | this.isDarkTheme = !this.isDarkTheme; 23 | this.themeService.updateThemePref(this.isDarkTheme ? 'dark' : 'light'); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/theme-switcher/theme-switcher.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | import { BehaviorSubject, Observable } from "rxjs"; 4 | 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ThemeSwitcherService { 10 | public pref$: Observable; 11 | private pref: BehaviorSubject; 12 | private prefKey = 'THEME_PREF'; 13 | 14 | constructor() { 15 | this.pref = new BehaviorSubject(this.getThemePref()); 16 | this.pref$ = this.pref.asObservable(); 17 | } 18 | 19 | public updateThemePref(value: 'dark' | 'light'): void { 20 | if (value) { 21 | localStorage.setItem(this.prefKey, value); 22 | this.pref.next(value); 23 | } 24 | } 25 | 26 | private getThemePref(): string { 27 | const localPref = localStorage.getItem(this.prefKey); 28 | return localPref || this.pref?.value; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/toast/toast.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 20 |
    21 |
    22 |
    23 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/toast/toast.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToastComponent } from './toast.component'; 4 | 5 | describe('ToastComponent', () => { 6 | let component: ToastComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ToastComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ToastComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/toast/toast.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, style, transition, trigger } from '@angular/animations'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { Toast, ToastService, ToastType } from './toast.service'; 5 | 6 | @Component({ 7 | selector: 'app-toast', 8 | templateUrl: './toast.component.html', 9 | animations: [ 10 | trigger( 11 | 'inOutAnimation', 12 | [ 13 | transition( 14 | ':enter', 15 | [ 16 | style({ left: 0, opacity: 0 }), 17 | animate('1s ease-out', 18 | style({ left: 20, opacity: 1 })) 19 | ] 20 | ), 21 | transition( 22 | ':leave', 23 | [ 24 | style({ left: 20, opacity: 1 }), 25 | animate('1s ease-in', 26 | style({ left: 0, opacity: 0 })) 27 | ] 28 | ) 29 | ] 30 | ) 31 | ] 32 | }) 33 | export class ToastComponent implements OnInit { 34 | 35 | public toast$?: Observable 36 | 37 | public constructor(private toastService: ToastService) { } 38 | 39 | ngOnInit(): void { 40 | this.toast$ = this.toastService.listen$; 41 | } 42 | 43 | public color(toastType: ToastType): string { 44 | switch (toastType) { 45 | case ToastType.info: 46 | return `bg-blue-500`; 47 | case ToastType.warning: 48 | return `bg-yellow-500`; 49 | case ToastType.success: 50 | return `bg-green-500`; 51 | case ToastType.error: 52 | return `bg-red-500`; 53 | default: 54 | return `bg-green-500`; 55 | } 56 | } 57 | 58 | public close() { 59 | this.toastService.close(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/app/presenter/components/shared/toast/toast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { BehaviorSubject, Observable } from "rxjs"; 3 | 4 | export enum ToastType { 5 | warning, 6 | success, 7 | info, 8 | error 9 | } 10 | 11 | export type Toast = { 12 | isOpen: boolean, 13 | type?: ToastType, 14 | title?: string, 15 | message?: string, 16 | } 17 | 18 | @Injectable({ 19 | providedIn: 'root' 20 | }) 21 | export class ToastService { 22 | 23 | public listen$: Observable; 24 | private listen: BehaviorSubject 25 | private options = { autoClose: true, location: 'top-right' }; 26 | 27 | constructor() { 28 | this.listen = new BehaviorSubject({ type: ToastType.info, isOpen: false }); 29 | this.listen$ = this.listen.asObservable(); 30 | } 31 | 32 | public show(args: { title: string, message?: string, type?: ToastType }): void { 33 | this.listen.next({ 34 | isOpen: true, 35 | title: args.title, 36 | type: args.type ?? ToastType.info, 37 | message: args.message ?? '' 38 | }); 39 | setTimeout(() => this.close(), 5000) 40 | } 41 | 42 | public close(): void { 43 | this.listen.next({ isOpen: false }) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/app/presenter/pages/blog/blog.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Blog

    4 |

    5 | Welcome to my blog section. 6 |

    7 |

    8 |
    9 |
    10 | 11 |
    12 |
    13 | 14 | 15 | 16 |
    17 |
    18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/presenter/pages/blog/blog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { IPostInteractor } from 'src/app/data/interactors/contracts/ipost.interactor'; 4 | 5 | import { PageSeoService } from '../page-seo.service'; 6 | import { SkeletonLoaderComponent } from './../../components/shared/skeleton-loader/skeleton-loader.component'; 7 | import { BlogSkeletonComponent } from './../../components/shared/skeleton-loader/blog-skeleton/blog-skeleton.component'; 8 | 9 | import { BlogComponent } from './blog.component'; 10 | 11 | describe('BlogComponent', () => { 12 | let component: BlogComponent; 13 | let fixture: ComponentFixture; 14 | let interactor: jasmine.SpyObj; 15 | let seoService: jasmine.SpyObj; 16 | 17 | beforeEach(async () => { 18 | interactor = jasmine.createSpyObj(IPostInteractor, ['getMany']); 19 | seoService = jasmine.createSpyObj(PageSeoService, ['setSEO']); 20 | 21 | await TestBed.configureTestingModule({ 22 | declarations: [ 23 | BlogComponent, 24 | SkeletonLoaderComponent, 25 | BlogSkeletonComponent, 26 | ], 27 | providers: [ 28 | { provide: IPostInteractor, useValue: interactor }, 29 | { provide: PageSeoService, useValue: seoService }, 30 | ], 31 | }).compileComponents(); 32 | 33 | fixture = TestBed.createComponent(BlogComponent); 34 | component = fixture.componentInstance; 35 | fixture.detectChanges(); 36 | }); 37 | 38 | it('should create', () => { 39 | expect(component).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/presenter/pages/blog/blog.component.ts: -------------------------------------------------------------------------------- 1 | import { PageSeoService } from './../page-seo.service'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { PageEnum } from '../../components/shared/skeleton-loader/skeleton-loader.component'; 5 | import { PostRequest } from 'src/app/data/requests/posts.request'; 6 | import { IPostInteractor } from 'src/app/data/interactors/contracts/ipost.interactor'; 7 | 8 | @Component({ 9 | selector: 'app-blog', 10 | templateUrl: './blog.component.html', 11 | styles: [] 12 | }) 13 | export class BlogComponent implements OnInit { 14 | 15 | public blog = PageEnum.blog; 16 | public posts$?: Observable; 17 | 18 | public constructor( 19 | private interactor: IPostInteractor, 20 | private seoService: PageSeoService 21 | ) { } 22 | 23 | ngOnInit(): void { 24 | this.seoService.setSEO({ pageTitle: 'Blog Posts' }); 25 | this.posts$ = this.interactor.getMany(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/presenter/pages/blog/post/post.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    5 | 7 | 8 |

    9 |

    10 |
    12 |
    13 |

    {{post?.author}} • {{post?.date | date}}

    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 | 21 |
    22 |
    23 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/blog/post/post.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { By } from '@angular/platform-browser'; 2 | import { RouterModule } from '@angular/router'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | 5 | import { MOCK_POSTS } from 'src/app/data/datasources/remote/repo-implementations/post/post.repository.spec'; 6 | 7 | import { TagComponent } from './../../../components/shared/tag/tag.component'; 8 | 9 | import { PostComponent } from './post.component'; 10 | 11 | describe('PostComponent', () => { 12 | let component: PostComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | declarations: [PostComponent, TagComponent], 18 | imports: [RouterModule.forRoot([])] 19 | }) 20 | .compileComponents(); 21 | 22 | fixture = TestBed.createComponent(PostComponent); 23 | component = fixture.componentInstance; 24 | component.post = MOCK_POSTS[0]; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | 32 | 33 | it('should set post input', () => { 34 | expect(component.post).toBeTruthy(); 35 | }); 36 | 37 | it('should add tag to page', () => { 38 | 39 | const { debugElement } = fixture; 40 | 41 | const tag = debugElement.query(By.directive(TagComponent)); 42 | 43 | expect(tag).toBeTruthy(); 44 | }) 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/presenter/pages/blog/post/post.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { PostRequest } from './../../../../data/requests/posts.request'; 4 | 5 | @Component({ 6 | selector: 'app-post', 7 | templateUrl: './post.component.html', 8 | }) 9 | export class PostComponent { 10 | 11 | @Input() 12 | post?: PostRequest; 13 | 14 | ngOnInit(): void { 15 | if (this.post && this.post.tags && typeof this.post.tags === 'string') { 16 | const strTag: string = this.post.tags; 17 | this.post.tags = strTag.split(','); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/presenter/pages/bookmarks/bookmark/bookmark.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |

    5 |
    6 | 7 | 10 | 11 |
    12 |
    13 | 14 |
    15 | 16 | 17 | Saved on {{bookmark?.date | date}} 18 | 19 |
    20 |
    21 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/bookmarks/bookmark/bookmark.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BookmarkComponent } from './bookmark.component'; 4 | 5 | describe('BookmarkComponent', () => { 6 | let component: BookmarkComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [BookmarkComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(BookmarkComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/presenter/pages/bookmarks/bookmark/bookmark.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { BookmarkRequest } from './../../../../data/requests/bookmark.request'; 3 | 4 | @Component({ 5 | selector: 'app-bookmark', 6 | templateUrl: './bookmark.component.html', 7 | }) 8 | export class BookmarkComponent { 9 | 10 | @Input() 11 | public bookmark?: BookmarkRequest; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/presenter/pages/bookmarks/bookmarks.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Bookmarks

    4 | 5 |

    6 | My favorite place for keeping some useful, interesting, and fun web hyperlinks. 7 |

    8 | 9 |
    10 |

    11 | Popular Tags 12 |

    13 | 14 |
    15 | 16 | 17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 | 31 |
    32 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/bookmarks/bookmarks.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { of } from 'rxjs'; 4 | 5 | import { BookmarkInteractor } from 'src/app/data/interactors/implementations/bookmark/bookmark.interactor'; 6 | 7 | import { PageSeoService } from '../page-seo.service'; 8 | import { SkeletonLoaderComponent } from './../../components/shared/skeleton-loader/skeleton-loader.component'; 9 | import { TagComponent } from './../../components/shared/tag/tag.component'; 10 | import { BookmarksSkeletonComponent } from './../../components/shared/skeleton-loader/bookmarks-skeleton/bookmarks-skeleton.component'; 11 | 12 | import { BookmarksComponent } from './bookmarks.component'; 13 | 14 | describe('BookmarksComponent', () => { 15 | let component: BookmarksComponent; 16 | let fixture: ComponentFixture; 17 | let seo: jasmine.SpyObj; 18 | let bookmarkInteractor: jasmine.SpyObj; 19 | 20 | beforeEach(async () => { 21 | bookmarkInteractor = jasmine.createSpyObj(BookmarkInteractor, [ 22 | 'getMany', 23 | ]); 24 | bookmarkInteractor.getMany = jasmine.createSpy().and.returnValue(of()); 25 | 26 | seo = jasmine.createSpyObj(PageSeoService, ['setSEO']); 27 | 28 | await TestBed.configureTestingModule({ 29 | declarations: [ 30 | BookmarksComponent, 31 | TagComponent, 32 | SkeletonLoaderComponent, 33 | BookmarksSkeletonComponent, 34 | ], 35 | providers: [ 36 | { provide: PageSeoService, useValue: seo }, 37 | { provide: BookmarkInteractor, useValue: bookmarkInteractor }, 38 | ], 39 | }).compileComponents(); 40 | 41 | fixture = TestBed.createComponent(BookmarksComponent); 42 | component = fixture.componentInstance; 43 | fixture.detectChanges(); 44 | }); 45 | 46 | it('should create', () => { 47 | expect(component).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/presenter/pages/bookmarks/bookmarks.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { BookmarkInteractor } from 'src/app/data/interactors/implementations/bookmark/bookmark.interactor'; 6 | import { BookmarkRequest } from 'src/app/data/requests/bookmark.request'; 7 | 8 | import { PageSeoService } from './../page-seo.service'; 9 | import { PageEnum } from '../../components/shared/skeleton-loader/skeleton-loader.component'; 10 | 11 | @Component({ 12 | selector: 'app-bookmarks', 13 | templateUrl: './bookmarks.component.html', 14 | styles: [] 15 | }) 16 | export class BookmarksComponent implements OnInit { 17 | 18 | public bookmarks$?: Observable; 19 | public bookmarkPage = PageEnum.bookmark; 20 | 21 | constructor( 22 | private seo: PageSeoService, 23 | private bookmarkInteractor: BookmarkInteractor 24 | ) { } 25 | 26 | ngOnInit(): void { 27 | this.seo.setSEO({ pageTitle: 'My Bookmarks & Useful Links' }); 28 | this.bookmarks$ = this.bookmarkInteractor.getMany(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/bookmark-form/bookmark-form.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 6 | 8 | 10 | 12 | 13 |
    14 | 15 |
    16 |
    17 |
    18 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/bookmark-form/bookmark-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormGroup, ReactiveFormsModule, FormControl } from '@angular/forms'; 3 | 4 | import { MOCK_BOOKMARKS } from 'src/app/data/datasources/remote/repo-implementations/bookmark/bookmark.repository.spec'; 5 | import { ComponentModule } from 'src/app/presenter/components/components.module'; 6 | import { ButtonComponent } from 'src/app/presenter/components/shared/button/button.component'; 7 | 8 | import { FormFieldComponent } from './../../../../components/shared/form-field/form-field.component'; 9 | import { ModalComponent } from './../../../../components/shared/modal/modal.component'; 10 | 11 | import { 12 | BookmarFormProps, 13 | BookmarkFormComponent, 14 | } from './bookmark-form.component'; 15 | 16 | describe('BookmarkFormComponent', () => { 17 | let component: BookmarkFormComponent; 18 | let fixture: ComponentFixture; 19 | 20 | let props: BookmarFormProps = { 21 | form: new FormGroup({ 22 | url: new FormControl(), 23 | short: new FormControl(), 24 | tags: new FormControl(), 25 | date: new FormControl(), 26 | }), 27 | action: () => {}, 28 | data: MOCK_BOOKMARKS[0], 29 | isLoading: false, 30 | }; 31 | 32 | beforeEach(async () => { 33 | await TestBed.configureTestingModule({ 34 | declarations: [ 35 | BookmarkFormComponent, 36 | ModalComponent, 37 | FormFieldComponent, 38 | ButtonComponent, 39 | ], 40 | imports: [ReactiveFormsModule, ComponentModule], 41 | }).compileComponents(); 42 | 43 | fixture = TestBed.createComponent(BookmarkFormComponent); 44 | component = fixture.componentInstance; 45 | component.defaultValue = props; 46 | fixture.detectChanges(); 47 | }); 48 | 49 | it('should create', () => { 50 | expect(component).toBeTruthy(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/bookmark-form/bookmark-form.component.ts: -------------------------------------------------------------------------------- 1 | import { BookmarkRequest } from 'src/app/data/requests/bookmark.request'; 2 | import { Component, Input } from '@angular/core'; 3 | import { FormGroup } from '@angular/forms'; 4 | import { isInvalidControl } from '../../editor.functions'; 5 | 6 | export type BookmarFormProps = { 7 | form: FormGroup, 8 | action: () => void, 9 | data: BookmarkRequest, 10 | isLoading: boolean, 11 | } 12 | 13 | @Component({ 14 | selector: 'app-bookmark-form', 15 | templateUrl: './bookmark-form.component.html', 16 | styles: [ 17 | ] 18 | }) 19 | export class BookmarkFormComponent { 20 | @Input() 21 | public defaultValue?: BookmarFormProps; 22 | 23 | public constructor() { } 24 | 25 | public isControlInValid = (controlName: string): boolean => { 26 | return isInvalidControl(controlName, this.defaultValue?.form!); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/post-form/post-form.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 17 | 18 |
    19 | 20 |
    21 |
    22 |
    23 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/post-form/post-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, ReactiveFormsModule, FormControl } from '@angular/forms'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { MOCK_POSTS } from 'src/app/data/datasources/remote/repo-implementations/post/post.repository.spec'; 5 | import { FormFieldComponent } from 'src/app/presenter/components/shared/form-field/form-field.component'; 6 | import { ComponentModule } from 'src/app/presenter/components/components.module'; 7 | 8 | import { ButtonComponent } from './../../../../components/shared/button/button.component'; 9 | import { ModalComponent } from './../../../../components/shared/modal/modal.component'; 10 | 11 | import { PostFormComponent, PostFormProps } from './post-form.component'; 12 | 13 | describe('PostFormComponent', () => { 14 | let component: PostFormComponent; 15 | let fixture: ComponentFixture; 16 | 17 | let props: PostFormProps = { 18 | form: new FormGroup({ 19 | title: new FormControl(''), 20 | excerpt: new FormControl(''), 21 | author: new FormControl(''), 22 | tags: new FormControl(''), 23 | date: new FormControl(''), 24 | content: new FormControl(''), 25 | }), 26 | action: () => {}, 27 | data: MOCK_POSTS[0], 28 | isLoading: false, 29 | }; 30 | 31 | beforeEach(async () => { 32 | await TestBed.configureTestingModule({ 33 | declarations: [ 34 | PostFormComponent, 35 | ModalComponent, 36 | FormFieldComponent, 37 | ButtonComponent, 38 | ], 39 | imports: [ReactiveFormsModule, ComponentModule], 40 | }).compileComponents(); 41 | 42 | fixture = TestBed.createComponent(PostFormComponent); 43 | component = fixture.componentInstance; 44 | component.defaultValue = props; 45 | fixture.detectChanges(); 46 | }); 47 | 48 | it('should create', () => { 49 | expect(component).toBeTruthy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/post-form/post-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { isInvalidControl } from '../../editor.functions'; 5 | import { PostRequest } from './../../../../../data/requests/posts.request'; 6 | 7 | export type PostFormProps = { 8 | form: FormGroup; 9 | action: () => void; 10 | data: PostRequest; 11 | isLoading: boolean; 12 | }; 13 | 14 | @Component({ 15 | selector: 'app-post-form', 16 | templateUrl: './post-form.component.html', 17 | styles: [], 18 | }) 19 | export class PostFormComponent { 20 | @Input() 21 | public defaultValue?: PostFormProps; 22 | 23 | public constructor() {} 24 | 25 | public isControlInValid = (controlName: string): boolean => { 26 | return isInvalidControl(controlName, this.defaultValue?.form!); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/project-form/project-form.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 23 |
    24 | 25 | 26 | 27 |
    28 |
    29 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/project-form/project-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; 3 | 4 | import { MOCK_PROJECT } from 'src/app/data/datasources/remote/repo-implementations/project/project.repository.spec'; 5 | import { ComponentModule } from 'src/app/presenter/components/components.module'; 6 | import { ButtonComponent } from 'src/app/presenter/components/shared/button/button.component'; 7 | import { FormFieldComponent } from 'src/app/presenter/components/shared/form-field/form-field.component'; 8 | 9 | import { ModalComponent } from './../../../../components/shared/modal/modal.component'; 10 | 11 | import { 12 | ProjectFormComponent, 13 | ProjectFormProps, 14 | } from './project-form.component'; 15 | 16 | describe('ProjectFormComponent', () => { 17 | let component: ProjectFormComponent; 18 | let fixture: ComponentFixture; 19 | let props: ProjectFormProps = { 20 | form: new FormGroup({ 21 | _id: new FormControl(''), 22 | title: new FormControl(''), 23 | description: new FormControl(''), 24 | features: new FormControl(''), 25 | modules: new FormControl(''), 26 | industries: new FormControl(''), 27 | tools: new FormControl(''), 28 | imageUrl: new FormControl(''), 29 | actionTitle: new FormControl(''), 30 | actionLink: new FormControl(''), 31 | }), 32 | action: () => {}, 33 | data: MOCK_PROJECT[0], 34 | isLoading: false, 35 | }; 36 | 37 | beforeEach(async () => { 38 | await TestBed.configureTestingModule({ 39 | declarations: [ 40 | ProjectFormComponent, 41 | ModalComponent, 42 | FormFieldComponent, 43 | ButtonComponent, 44 | ], 45 | imports: [ReactiveFormsModule, ComponentModule], 46 | }).compileComponents(); 47 | 48 | fixture = TestBed.createComponent(ProjectFormComponent); 49 | component = fixture.componentInstance; 50 | component.defaultValue = props; 51 | fixture.detectChanges(); 52 | }); 53 | 54 | it('should create', () => { 55 | expect(component).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/components/project-form/project-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { ProjectRequest } from 'src/app/data/requests/project.request'; 5 | import { isInvalidControl } from '../../editor.functions'; 6 | 7 | export type ProjectFormProps = { 8 | form: FormGroup, 9 | action: () => void, 10 | data: ProjectRequest, 11 | isLoading: boolean, 12 | } 13 | 14 | @Component({ 15 | selector: 'app-project-form', 16 | templateUrl: './project-form.component.html', 17 | styles: [ 18 | ] 19 | }) 20 | export class ProjectFormComponent { 21 | 22 | @Input() 23 | public defaultValue?: ProjectFormProps; 24 | 25 | public constructor() { } 26 | 27 | public isControlInValid = (controlName: string): boolean => { 28 | return isInvalidControl(controlName, this.defaultValue?.form!); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/editor-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundComponent } from '../../components/shared/not-found/not-found.component'; 2 | import { EditorBookmarksComponent } from './pages/editor-bookmarks/editor-bookmarks.component'; 3 | import { EditorProjectsComponent } from './pages/editor-projects/editor-projects.component'; 4 | import { EditorPostsComponent } from './pages/editor-posts/editor-posts.component'; 5 | import { EditorHomeComponent } from './pages/editor-home/editor-home.component'; 6 | import { NgModule } from "@angular/core"; 7 | import { RouterModule, Routes } from "@angular/router"; 8 | 9 | const routes: Routes = [ 10 | { path: '', component: EditorHomeComponent }, 11 | { path: 'posts', component: EditorPostsComponent }, 12 | { path: 'projects', component: EditorProjectsComponent }, 13 | { path: 'bookmarks', component: EditorBookmarksComponent }, 14 | { path: '**', component: NotFoundComponent }, 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class EditorRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/editor.component.html: -------------------------------------------------------------------------------- 1 |

    editor works!

    2 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@angular/router'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { EditorComponent } from './editor.component'; 5 | 6 | describe('EditorComponent', () => { 7 | let component: EditorComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [EditorComponent], 13 | imports: [RouterModule.forRoot([])], 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(EditorComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { PageSeoService } from '../page-seo.service'; 4 | 5 | @Component({ 6 | selector: 'app-editor', 7 | templateUrl: './editor.component.html', 8 | styles: [ 9 | ] 10 | }) 11 | export class EditorComponent implements OnInit { 12 | 13 | constructor( 14 | private seoService: PageSeoService, 15 | ) { } 16 | 17 | ngOnInit(): void { 18 | this.seoService.setSEO(); 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/editor.module.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | 6 | import { EditorRoutingModule } from './editor-routing.module'; 7 | import { EditorComponent } from './editor.component'; 8 | import { EditorHomeComponent } from './pages/editor-home/editor-home.component'; 9 | import { EditorPostsComponent } from './pages/editor-posts/editor-posts.component'; 10 | import { EditorProjectsComponent } from './pages/editor-projects/editor-projects.component'; 11 | import { EditorBookmarksComponent } from './pages/editor-bookmarks/editor-bookmarks.component'; 12 | import { ComponentModule } from "../../components/components.module"; 13 | import { PostFormComponent } from './components/post-form/post-form.component'; 14 | import { ProjectFormComponent } from './components/project-form/project-form.component'; 15 | import { BookmarkFormComponent } from './components/bookmark-form/bookmark-form.component'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | EditorComponent, 20 | EditorHomeComponent, 21 | EditorPostsComponent, 22 | EditorProjectsComponent, 23 | EditorBookmarksComponent, 24 | PostFormComponent, 25 | ProjectFormComponent, 26 | BookmarkFormComponent 27 | ], 28 | providers: [], 29 | exports: [ 30 | EditorComponent 31 | ], 32 | imports: [ 33 | RouterModule, 34 | ComponentModule, 35 | CommonModule, 36 | FormsModule, 37 | ReactiveFormsModule, 38 | EditorRoutingModule, 39 | ] 40 | }) 41 | export class EditorModule { } 42 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-bookmarks/editor-bookmarks.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-bookmarks/editor-bookmarks.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { Observable, of } from 'rxjs'; 4 | 5 | import { BookmarkInteractor } from 'src/app/data/interactors/implementations/bookmark/bookmark.interactor'; 6 | import { ModalService } from 'src/app/presenter/components/shared/modal/modal.service'; 7 | import { ToastService } from 'src/app/presenter/components/shared/toast/toast.service'; 8 | 9 | import { ButtonComponent } from './../../../../components/shared/button/button.component'; 10 | import { TableComponent } from './../../../../components/shared/table/table.component'; 11 | 12 | import { EditorBookmarksComponent } from './editor-bookmarks.component'; 13 | 14 | describe('EditorBookmarksComponent', () => { 15 | let component: EditorBookmarksComponent; 16 | let fixture: ComponentFixture; 17 | 18 | let bookmarkInteractor: jasmine.SpyObj; 19 | let modalService: jasmine.SpyObj>; 20 | let toastService: jasmine.SpyObj; 21 | 22 | beforeEach(async () => { 23 | bookmarkInteractor = jasmine.createSpyObj(BookmarkInteractor, [ 24 | 'getMany', 25 | ]); 26 | 27 | bookmarkInteractor.getMany = jasmine.createSpy().and.returnValue(of()); 28 | 29 | modalService = jasmine.createSpyObj(ModalService, ['open', 'close']); 30 | toastService = jasmine.createSpyObj(ToastService, ['show']); 31 | await TestBed.configureTestingModule({ 32 | declarations: [ 33 | EditorBookmarksComponent, 34 | ButtonComponent, 35 | TableComponent, 36 | ], 37 | providers: [ 38 | { provide: BookmarkInteractor, useValue: bookmarkInteractor }, 39 | { provide: ModalService, useValue: modalService }, 40 | { provide: ToastService, useValue: toastService }, 41 | ], 42 | }).compileComponents(); 43 | 44 | fixture = TestBed.createComponent(EditorBookmarksComponent); 45 | component = fixture.componentInstance; 46 | fixture.detectChanges(); 47 | }); 48 | 49 | it('should create', () => { 50 | expect(component).toBeTruthy(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-home/editor-home.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Welcome to the editor panel 4 |
    5 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-home/editor-home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditorHomeComponent } from './editor-home.component'; 4 | 5 | describe('EditorHomeComponent', () => { 6 | let component: EditorHomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ EditorHomeComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(EditorHomeComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-home/editor-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-editor-home', 5 | templateUrl: './editor-home.component.html', 6 | styles: [ 7 | ] 8 | }) 9 | export class EditorHomeComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-posts/editor-posts.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 | 6 | 7 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-posts/editor-posts.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ButtonComponent } from 'src/app/presenter/components/shared/button/button.component'; 4 | import { TableComponent } from 'src/app/presenter/components/shared/table/table.component'; 5 | import { ModalService } from 'src/app/presenter/components/shared/modal/modal.service'; 6 | import { ToastService } from 'src/app/presenter/components/shared/toast/toast.service'; 7 | 8 | import { of } from 'rxjs'; 9 | 10 | import { PostInteractor } from './../../../../../data/interactors/implementations/post/post.interactor'; 11 | 12 | import { EditorPostsComponent } from './editor-posts.component'; 13 | 14 | describe('EditorPostsComponent', () => { 15 | let component: EditorPostsComponent; 16 | let fixture: ComponentFixture; 17 | let postInteractor: jasmine.SpyObj; 18 | let modalService: jasmine.SpyObj>; 19 | let toastService: jasmine.SpyObj; 20 | 21 | beforeEach(async () => { 22 | postInteractor = jasmine.createSpyObj(PostInteractor, ['getMany']); 23 | postInteractor.getMany = jasmine.createSpy().and.returnValue(of()); 24 | 25 | await TestBed.configureTestingModule({ 26 | declarations: [ 27 | EditorPostsComponent, 28 | ButtonComponent, 29 | TableComponent, 30 | ], 31 | providers: [ 32 | { provide: PostInteractor, useValue: postInteractor }, 33 | { provide: ModalService, useValue: modalService }, 34 | { provide: ToastService, useValue: toastService }, 35 | ], 36 | }).compileComponents(); 37 | 38 | fixture = TestBed.createComponent(EditorPostsComponent); 39 | component = fixture.componentInstance; 40 | fixture.detectChanges(); 41 | }); 42 | 43 | it('should create', () => { 44 | expect(component).toBeTruthy(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-projects/editor-projects.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /src/app/presenter/pages/editor/pages/editor-projects/editor-projects.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { of } from 'rxjs'; 4 | 5 | import { ModalService } from 'src/app/presenter/components/shared/modal/modal.service'; 6 | import { ToastService } from 'src/app/presenter/components/shared/toast/toast.service'; 7 | 8 | import { ButtonComponent } from './../../../../components/shared/button/button.component'; 9 | import { TableComponent } from './../../../../components/shared/table/table.component'; 10 | import { ProjectInteractor } from './../../../../../data/interactors/implementations/project/project.interactor'; 11 | 12 | import { EditorProjectsComponent } from './editor-projects.component'; 13 | 14 | describe('EditorProjectsComponent', () => { 15 | let component: EditorProjectsComponent; 16 | let fixture: ComponentFixture; 17 | let projectInteractor: jasmine.SpyObj; 18 | let modalService: jasmine.SpyObj>; 19 | let toastService: jasmine.SpyObj; 20 | 21 | beforeEach(async () => { 22 | projectInteractor = jasmine.createSpyObj(ProjectInteractor, [ 23 | 'getMany', 24 | ]); 25 | projectInteractor.getMany = jasmine.createSpy().and.returnValue(of()); 26 | 27 | await TestBed.configureTestingModule({ 28 | declarations: [ 29 | EditorProjectsComponent, 30 | ButtonComponent, 31 | TableComponent, 32 | ], 33 | providers: [ 34 | { provide: ProjectInteractor, useValue: projectInteractor }, 35 | { provide: ModalService, useValue: modalService }, 36 | { provide: ToastService, useValue: toastService }, 37 | ], 38 | }).compileComponents(); 39 | 40 | fixture = TestBed.createComponent(EditorProjectsComponent); 41 | component = fixture.componentInstance; 42 | fixture.detectChanges(); 43 | }); 44 | 45 | it('should create', () => { 46 | expect(component).toBeTruthy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/presenter/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@angular/router'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { HomeComponent } from './home.component'; 5 | 6 | describe('HomeComponent', () => { 7 | let component: HomeComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [HomeComponent], 13 | imports: [RouterModule.forRoot([])], 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(HomeComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/presenter/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | }) 7 | export class HomeComponent { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/app/presenter/pages/page-seo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Meta, Title } from '@angular/platform-browser'; 3 | 4 | export interface SeoData { 5 | pageTitle: string; 6 | pageDescription?: string; 7 | pageKeywords?: string; 8 | pageUrl?: string; 9 | author?: string; 10 | pageImageUrl?: string; 11 | } 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class PageSeoService { 17 | 18 | constructor( 19 | private meta: Meta, 20 | private title: Title 21 | ) { } 22 | 23 | 24 | public setSEO(data?: SeoData): void { 25 | const defaultImage = `${window.location.origin}/assets/img/blog-thumbnail.jpg`; 26 | 27 | const author = `${data?.author ?? 'James Aworo'}`; 28 | const imagePath = `${data?.pageImageUrl ?? defaultImage}`; 29 | const pageTitle = `${data?.pageTitle ?? `Welcome to my Personal | Projects | Blog Website`}`; 30 | const description = `${data?.pageDescription ?? 31 | ` 32 | Hi, my name is ${author} welcome to my personal space on the web. 33 | I am a fullstack software engineer. 34 | I'm passionate about building functional and performant products, 35 | I like to learn new stuff and I get excited when work on big problems with amazing people.` 36 | } `; 37 | const keywords = `${data?.pageKeywords ?? 'programming, fullstack engineering, java, typescript, blogging'}`; 38 | const pageUrl = `${data?.pageUrl ?? 'https://jamesaworo.com/'}`; 39 | 40 | this.title.setTitle(`${author} | ${pageTitle}`); 41 | this.meta.updateTag({ name: 'author', content: author }) 42 | this.meta.updateTag({ name: 'description', content: description }) 43 | this.meta.updateTag({ name: 'keywords', content: keywords }) 44 | 45 | this.meta.updateTag({ name: 'og:title', content: pageTitle }) 46 | this.meta.updateTag({ name: 'og:description', content: description }) 47 | this.meta.updateTag({ name: 'og:image', content: imagePath }) 48 | this.meta.updateTag({ name: 'og:type', content: 'article' }) 49 | this.meta.updateTag({ name: 'og:url', content: pageUrl }) 50 | this.meta.updateTag({ name: 'og:site_name', content: author }) 51 | this.meta.updateTag({ name: 'article:author', content: author }) 52 | 53 | this.meta.updateTag({ name: 'twitter:title', content: pageTitle }) 54 | this.meta.updateTag({ name: 'twitter:description', content: description }) 55 | this.meta.updateTag({ name: 'twitter:image', content: imagePath }) 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/presenter/pages/pages.module.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownModule } from 'ngx-markdown'; 2 | import { RouterModule } from '@angular/router'; 3 | import { PostDetailComponent } from './post-detail/post-detail.component'; 4 | import { HomeComponent } from './home/home.component'; 5 | import { EditorModule } from './editor/editor.module'; 6 | import { BookmarksComponent } from './bookmarks/bookmarks.component'; 7 | import { BlogComponent } from './blog/blog.component'; 8 | import { NgModule } from '@angular/core'; 9 | import { ProjectsComponent } from './projects/projects.component'; 10 | import { ComponentModule } from "../components/components.module"; 11 | import { BookmarkComponent } from './bookmarks/bookmark/bookmark.component'; 12 | import { CommonModule } from '@angular/common'; 13 | import { PostComponent } from './blog/post/post.component'; 14 | import { ProjectComponent } from './projects/project/project.component'; 15 | 16 | 17 | @NgModule({ 18 | declarations: [ 19 | BlogComponent, 20 | BookmarksComponent, 21 | HomeComponent, 22 | PostComponent, 23 | ProjectsComponent, 24 | BookmarkComponent, 25 | PostDetailComponent, 26 | ProjectComponent, 27 | ], 28 | exports: [ 29 | CommonModule, 30 | ComponentModule, 31 | PostComponent, 32 | BlogComponent, 33 | ProjectComponent 34 | ], 35 | imports: [ 36 | RouterModule, 37 | CommonModule, 38 | ComponentModule, 39 | EditorModule, 40 | MarkdownModule.forChild() 41 | ] 42 | }) 43 | export class PagesModule { } 44 | -------------------------------------------------------------------------------- /src/app/presenter/pages/post-detail/post-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{setSeo(post)}} 3 |
    4 |
    5 |

    6 | {{post?.title}} 7 |

    8 | 9 |

    10 | {{ post?.date | date}} 11 |

    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/app/presenter/pages/post-detail/post-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | describe('PostComponent', () => { 4 | 5 | describe('ngOnInit', () => { 6 | it('should get route param from Router', () => { }); 7 | it('should call Router.navigateByUrl if route param is NOT present'); 8 | it('should call interactor.getOne with route param as input if present') 9 | }); 10 | 11 | describe('setSeo', () => { 12 | it('should call seoService.setSEO method', () => { }); 13 | it('should an object to setSEO method of seoService', () => { }); 14 | }) 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/presenter/pages/post-detail/post-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { PageSeoService } from './../page-seo.service'; 3 | import { IPostInteractor } from 'src/app/data/interactors/contracts/ipost.interactor'; 4 | import { PostRequest } from './../../../data/requests/posts.request'; 5 | import { Component, OnInit } from '@angular/core'; 6 | import { ActivatedRoute, Router } from '@angular/router'; 7 | 8 | @Component({ 9 | selector: 'app-post-detail', 10 | templateUrl: './post-detail.component.html', 11 | }) 12 | export class PostDetailComponent implements OnInit { 13 | 14 | public post$?: Observable 15 | public content?: any; 16 | 17 | public constructor( 18 | private route: ActivatedRoute, 19 | private interactor: IPostInteractor, 20 | private seoService: PageSeoService, 21 | private router: Router 22 | ) { } 23 | 24 | ngOnInit(): void { 25 | const routeParam = this.route.snapshot.paramMap; 26 | const postId = routeParam.get('postId')!; 27 | 28 | if (!postId) { 29 | this.router.navigateByUrl('blog'); 30 | return; 31 | } 32 | 33 | this.post$ = this.interactor.getOne(postId); 34 | } 35 | 36 | public setSeo(post?: PostRequest): void { 37 | this.seoService.setSEO({ 38 | pageTitle: `${post?.title}`, 39 | pageDescription: `${post?.excerpt}`, 40 | pageKeywords: `${post?.tags?.toString()}` 41 | }); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/app/presenter/pages/projects/project/project.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectComponent } from './project.component'; 4 | 5 | describe('ProjectComponent', () => { 6 | let component: ProjectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProjectComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ProjectComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/presenter/pages/projects/project/project.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ProjectRequest } from 'src/app/data/requests/project.request'; 3 | 4 | @Component({ 5 | selector: 'app-project', 6 | templateUrl: './project.component.html', 7 | 8 | }) 9 | export class ProjectComponent { 10 | @Input() 11 | public project?: ProjectRequest; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/presenter/pages/projects/projects.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Projects

    4 |

    5 | I enjoy creating beautiful and reliable applications for multiple platforms. 6 | My goal is to build scalable products and performant experiences constantly. 7 |

    8 | 9 |

    10 | I derive joy in working on projects that solve specific business requirements or help people solve 11 | everyday problems. 12 |

    13 | 14 |

    15 | My projects range from web, desktop, and mobile apps. 16 |

    17 |
    18 |
    19 | 20 | 21 | 22 | 23 | 24 | 25 |
    26 | 27 |
    28 |
    -------------------------------------------------------------------------------- /src/app/presenter/pages/projects/projects.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { of } from 'rxjs'; 4 | 5 | import { IProjectInteractor } from 'src/app/data/interactors/contracts/iproject.interactor'; 6 | import { ProjectInteractor } from 'src/app/data/interactors/implementations/project/project.interactor'; 7 | 8 | import { PageSeoService } from './../page-seo.service'; 9 | import { SkeletonLoaderComponent } from './../../components/shared/skeleton-loader/skeleton-loader.component'; 10 | import { ProjectsSkeletonComponent } from './../../components/shared/skeleton-loader/projects-skeleton/projects-skeleton.component'; 11 | 12 | import { ProjectsComponent } from './projects.component'; 13 | 14 | describe('ProjectsComponent', () => { 15 | let component: ProjectsComponent; 16 | let fixture: ComponentFixture; 17 | let seoService: jasmine.SpyObj; 18 | let interactor: jasmine.SpyObj; 19 | 20 | beforeEach(async () => { 21 | seoService = jasmine.createSpyObj(PageSeoService, ['setSEO']); 22 | interactor = jasmine.createSpyObj(IProjectInteractor, ['getMany']); 23 | interactor.getMany = jasmine.createSpy().and.returnValue(of()); 24 | 25 | await TestBed.configureTestingModule({ 26 | providers: [{ provide: IProjectInteractor, useValue: interactor }], 27 | declarations: [ 28 | ProjectsComponent, 29 | SkeletonLoaderComponent, 30 | ProjectsSkeletonComponent, 31 | ], 32 | }).compileComponents(); 33 | 34 | fixture = TestBed.createComponent(ProjectsComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/app/presenter/pages/projects/projects.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { ProjectRequest } from 'src/app/data/requests/project.request'; 6 | import { IProjectInteractor } from 'src/app/data/interactors/contracts/iproject.interactor'; 7 | 8 | import { PageSeoService } from './../page-seo.service'; 9 | import { PageEnum } from '../../components/shared/skeleton-loader/skeleton-loader.component'; 10 | 11 | @Component({ 12 | selector: 'app-projects', 13 | templateUrl: './projects.component.html', 14 | }) 15 | export class ProjectsComponent { 16 | public projects$?: Observable; 17 | public page = PageEnum.project; 18 | 19 | constructor( 20 | private seoService: PageSeoService, 21 | private interactor: IProjectInteractor 22 | ) {} 23 | 24 | ngOnInit(): void { 25 | this.seoService.setSEO({ pageTitle: 'My Projects / Portfolio' }); 26 | this.projects$ = this.interactor.getMany(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/presenter/presenter.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { PresenterModule } from './presenter.module'; 4 | 5 | describe('PresenterModule', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [PresenterModule] 9 | }) 10 | }) 11 | 12 | it('should initialize', () => { 13 | const module = TestBed.inject(PresenterModule); 14 | 15 | expect(module).toBeTruthy(); 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/app/presenter/presenter.module.ts: -------------------------------------------------------------------------------- 1 | import { DataModule } from './../data/data.module'; 2 | import { PagesModule } from './pages/pages.module'; 3 | import { ComponentModule } from './components/components.module'; 4 | import { NgModule } from '@angular/core'; 5 | import { CommonModule } from '@angular/common'; 6 | 7 | @NgModule({ 8 | declarations: [], 9 | exports: [ 10 | CommonModule, 11 | ComponentModule, 12 | PagesModule, 13 | ], 14 | imports: [ 15 | CommonModule, 16 | DataModule, 17 | ComponentModule, 18 | PagesModule 19 | ], 20 | }) 21 | export class PresenterModule { } 22 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/img/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/img/blog-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/blog-thumbnail.jpg -------------------------------------------------------------------------------- /src/assets/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/img/jamie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/assets/img/jamie.png -------------------------------------------------------------------------------- /src/assets/img/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesawo/angular-clean-architecture/4436611555ba841f8c1eee42919972f2fa32ee53/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Aworo James | Personal | Blog Website 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 10 | } 11 | 12 | body.dark .prose h1, 13 | body.dark .prose h2, 14 | body.dark .prose h3, 15 | body.dark .prose h4, 16 | body.dark .prose blockquote>p, 17 | body.dark .prose p>strong, 18 | body.dark .prose p>a, 19 | body.dark .prose { 20 | color: white; 21 | } 22 | 23 | body.dark li>strong { 24 | color: white; 25 | font-weight: bold; 26 | } 27 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | "./src/**/*.{html,ts}" 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require('@tailwindcss/typography'), 12 | 13 | ], 14 | corePlugins: { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine", 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------