├── frontend ├── cypress.json ├── src │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── index.css │ ├── reportWebVitals.ts │ ├── stories │ │ └── TwitterMenu │ │ │ ├── TwitterMenu.stories.tsx │ │ │ ├── TwitterMenu.test.tsx │ │ │ └── TwitterMenu.tsx │ ├── index.tsx │ ├── App.test.tsx │ ├── logo.svg │ ├── contexts │ │ └── TwitterContext.tsx │ ├── App.css │ └── App.tsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── cypress │ ├── fixtures │ │ └── example.json │ ├── support │ │ ├── index.js │ │ └── commands.js │ ├── plugins │ │ └── index.js │ └── integration │ │ └── tweet.spec.js ├── .storybook │ ├── preview.js │ └── main.js ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── backend-nest-mongodb ├── .prettierrc ├── tsconfig.build.json ├── nest-cli.json ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts ├── src │ ├── twitter │ │ ├── dto │ │ │ └── create-twitter.dto.ts │ │ ├── schemas │ │ │ └── twitter.schema.ts │ │ ├── twitter.module.ts │ │ ├── twitter.service.spec.ts │ │ ├── twitter.controller.spec.ts │ │ ├── twitter.controller.ts │ │ └── twitter.service.ts │ ├── main.ts │ └── app.module.ts ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── package.json └── README.md ├── img └── complete-react-developer.png ├── docker-twitter-mongodb ├── docker-compose.yml └── .gitignore ├── .gitignore └── README.md /frontend/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /backend-nest-mongodb/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbaisden/complete-react-developer/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbaisden/complete-react-developer/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbaisden/complete-react-developer/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /img/complete-react-developer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbaisden/complete-react-developer/HEAD/img/complete-react-developer.png -------------------------------------------------------------------------------- /backend-nest-mongodb/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /backend-nest-mongodb/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | } -------------------------------------------------------------------------------- /backend-nest-mongodb/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/dto/create-twitter.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateTwitterDto { 2 | id?: string; 3 | 4 | tweet: string; 5 | 6 | img: string; 7 | } 8 | 9 | export class TwitterDto { 10 | id?: string; 11 | 12 | tweet: string; 13 | 14 | img: string; 15 | } 16 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | app.enableCors(); 7 | await app.listen(8080); 8 | } 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /docker-twitter-mongodb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | # MongoDB services 5 | mongo_db: 6 | container_name: db_container 7 | image: mongo:latest 8 | restart: always 9 | ports: 10 | - 2717:27017 11 | volumes: 12 | - mongo_db:/data/db 13 | 14 | volumes: 15 | mongo_db: {} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/schemas/twitter.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | 3 | import { Document } from 'mongoose'; 4 | 5 | export type TwitterDocument = Twitter & Document; 6 | 7 | @Schema() 8 | export class Twitter { 9 | @Prop() 10 | tweet: string; 11 | 12 | @Prop() 13 | img: string; 14 | } 15 | 16 | export const TwitterSchema = SchemaFactory.createForClass(Twitter); 17 | -------------------------------------------------------------------------------- /docker-twitter-mongodb/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/twitter.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | 5 | import { Twitter, TwitterSchema } from './schemas/twitter.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ name: Twitter.name, schema: TwitterSchema }]), 10 | ], 11 | 12 | exports: [MongooseModule], 13 | }) 14 | export class TwitterModule {} 15 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions", 10 | "@storybook/preset-create-react-app" 11 | ], 12 | "framework": "@storybook/react", 13 | "core": { 14 | "builder": "@storybook/builder-webpack5" 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /backend-nest-mongodb/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/twitter.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TwitterService } from './twitter.service'; 3 | 4 | describe('TwitterService', () => { 5 | let service: TwitterService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [TwitterService], 10 | }).compile(); 11 | 12 | service = module.get(TwitterService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/twitter.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TwitterController } from './twitter.controller'; 3 | 4 | describe('TwitterController', () => { 5 | let controller: TwitterController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [TwitterController], 10 | }).compile(); 11 | 12 | controller = module.get(TwitterController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend-nest-mongodb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/stories/TwitterMenu/TwitterMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 2 | 3 | import { TwitterMenu } from './TwitterMenu'; 4 | 5 | export default { 6 | title: 'Twitter/Compose Tweet', 7 | component: TwitterMenu, 8 | } as ComponentMeta; 9 | 10 | const Template: ComponentStory = (args) => ; 11 | 12 | export const Primary = Template.bind({}); 13 | 14 | Primary.args = { 15 | home: 'Home', 16 | explore: 'Explore', 17 | notifications: 'Notifications', 18 | messages: 'Messages', 19 | bookmarks: 'Bookmarks', 20 | lists: 'Lists', 21 | profile: 'Profile', 22 | more: 'More', 23 | }; 24 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | 5 | import { TwitterController } from './twitter/twitter.controller'; 6 | 7 | import { TwitterService } from './twitter/twitter.service'; 8 | 9 | import { TwitterModule } from './twitter/twitter.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | TwitterModule, 14 | 15 | // Local MongoDb database 16 | 17 | // Change the port to 127.0.0.1:2717 to connect to Docker 18 | 19 | MongooseModule.forRoot('mongodb://127.0.0.1:2717/twitter'), 20 | ], 21 | 22 | controllers: [TwitterController], 23 | 24 | providers: [TwitterService], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /backend-nest-mongodb/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import TwitterContextProvider from './contexts/TwitterContext'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /frontend/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /backend-nest-mongodb/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | describe('', () => { 5 | it('has a following text label', () => { 6 | render(); 7 | const el = screen.getByText(/Following/i); 8 | 9 | expect(el).toBeTruthy(); 10 | }); 11 | it('has a followers text label', () => { 12 | render(); 13 | const el = screen.getByText(/Followers/i); 14 | 15 | expect(el).toBeTruthy(); 16 | }); 17 | it('has a you might like heading', () => { 18 | render(); 19 | const el = screen.getByText(/You might like/i); 20 | 21 | expect(el.innerHTML).toBe('You might like'); 22 | }); 23 | it('has a whats happening heading', () => { 24 | render(); 25 | const el = screen.getByText(/Whats happening/i); 26 | 27 | expect(el.innerHTML).toBe('Whats happening'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /frontend/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /frontend/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /frontend/cypress/integration/tweet.spec.js: -------------------------------------------------------------------------------- 1 | describe('user form flow', () => { 2 | beforeEach(() => { 3 | cy.viewport(1600, 900); 4 | cy.visit('http://localhost:3000/'); 5 | }); 6 | 7 | it('user posts a tweet', () => { 8 | // Post a tweet 9 | cy.get('.compose-tweet-btn').click(); 10 | cy.get('textarea[name="tweet"]').type( 11 | 'What happened to all that fun you were having?! Come on, lets try to enjoy this!' 12 | ); 13 | cy.wait(3000); 14 | cy.get('.post-tweet-btn').click(); 15 | }); 16 | 17 | it('user posts a second tweet', () => { 18 | // Post a tweet 19 | cy.get('.compose-tweet-btn').click(); 20 | cy.get('textarea[name="tweet"]').type('That was an Attack on Titan easter egg 🥚 😄'); 21 | cy.wait(3000); 22 | cy.get('.post-tweet-btn').click(); 23 | }); 24 | 25 | it('user posts a third tweet', () => { 26 | // Post a tweet 27 | cy.get('.compose-tweet-btn').click(); 28 | cy.get('textarea[name="tweet"]').type( 29 | 'The Rumbling arrives on Marley 😱 https://www.youtube.com/watch?v=wT2H68kEmi8' 30 | ); 31 | cy.wait(3000); 32 | cy.get('.post-tweet-btn').click(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/twitter.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Param, 7 | Put, 8 | Delete, 9 | } from '@nestjs/common'; 10 | 11 | import { CreateTwitterDto, TwitterDto } from './dto/create-twitter.dto'; 12 | 13 | import { TwitterService } from './twitter.service'; 14 | 15 | @Controller('tweets') 16 | export class TwitterController { 17 | constructor(private twitterService: TwitterService) {} 18 | 19 | @Post() 20 | async create(@Body() createTwitterDto: CreateTwitterDto) { 21 | this.twitterService.create(createTwitterDto); 22 | } 23 | 24 | @Get() 25 | async findAll(): Promise { 26 | return this.twitterService.findAll(); 27 | } 28 | 29 | @Get(':id') 30 | async findOne(@Param('id') id): Promise { 31 | return this.twitterService.findOne(id); 32 | } 33 | 34 | @Put(':id') 35 | update( 36 | @Body() updateTwitterDto: CreateTwitterDto, 37 | 38 | @Param('id') id, 39 | ): Promise { 40 | return this.twitterService.update(id, updateTwitterDto); 41 | } 42 | 43 | @Delete(':id') 44 | delete(@Param('id') id): Promise { 45 | return this.twitterService.delete(id); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend-nest-mongodb/src/twitter/twitter.service.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'mongoose'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | import { InjectModel } from '@nestjs/mongoose'; 6 | 7 | import { Twitter, TwitterDocument } from './schemas/twitter.schema'; 8 | 9 | import { CreateTwitterDto } from './dto/create-twitter.dto'; 10 | 11 | @Injectable() 12 | export class TwitterService { 13 | constructor( 14 | @InjectModel(Twitter.name) private twitterModel: Model, 15 | ) {} 16 | 17 | async create(createTwitterDto: CreateTwitterDto): Promise { 18 | const createdTwitter = new this.twitterModel(createTwitterDto); 19 | 20 | return createdTwitter.save(); 21 | } 22 | 23 | async findAll(): Promise { 24 | return this.twitterModel.find().exec(); 25 | } 26 | 27 | async findOne(id: string): Promise { 28 | return this.twitterModel.findOne({ _id: id }); 29 | } 30 | 31 | async update(id: string, twitter: Twitter): Promise { 32 | return this.twitterModel.findByIdAndUpdate(id, twitter, { new: true }); 33 | } 34 | 35 | async delete(id: string): Promise { 36 | return this.twitterModel.findByIdAndRemove({ _id: id }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.1.1", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.0", 10 | "@types/node": "^16.11.33", 11 | "@types/react": "^18.0.8", 12 | "@types/react-dom": "^18.0.3", 13 | "react": "^18.1.0", 14 | "react-dom": "^18.1.0", 15 | "react-scripts": "5.0.1", 16 | "typescript": "^4.6.4", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject", 24 | "storybook": "start-storybook -p 6006 -s public", 25 | "build-storybook": "build-storybook -s public" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ], 32 | "overrides": [ 33 | { 34 | "files": [ 35 | "**/*.stories.*" 36 | ], 37 | "rules": { 38 | "import/no-anonymous-default-export": "off" 39 | } 40 | } 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@storybook/addon-actions": "^6.4.22", 57 | "@storybook/addon-essentials": "^6.4.22", 58 | "@storybook/addon-interactions": "^6.4.22", 59 | "@storybook/addon-links": "^6.4.22", 60 | "@storybook/builder-webpack5": "^6.4.22", 61 | "@storybook/manager-webpack5": "^6.4.22", 62 | "@storybook/node-logger": "^6.4.22", 63 | "@storybook/preset-create-react-app": "^4.1.0", 64 | "@storybook/react": "^6.4.22", 65 | "@storybook/testing-library": "^0.0.11", 66 | "webpack": "^5.72.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /backend-nest-mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-nest-mongodb", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.0.0", 25 | "@nestjs/core": "^8.0.0", 26 | "@nestjs/mongoose": "^9.0.3", 27 | "@nestjs/platform-express": "^8.0.0", 28 | "mongoose": "^6.3.2", 29 | "reflect-metadata": "^0.1.13", 30 | "rimraf": "^3.0.2", 31 | "rxjs": "^7.2.0" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^8.0.0", 35 | "@nestjs/schematics": "^8.0.0", 36 | "@nestjs/testing": "^8.0.0", 37 | "@types/express": "^4.17.13", 38 | "@types/jest": "27.4.1", 39 | "@types/node": "^16.0.0", 40 | "@types/supertest": "^2.0.11", 41 | "@typescript-eslint/eslint-plugin": "^5.0.0", 42 | "@typescript-eslint/parser": "^5.0.0", 43 | "eslint": "^8.0.1", 44 | "eslint-config-prettier": "^8.3.0", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "jest": "^27.2.5", 47 | "prettier": "^2.3.2", 48 | "source-map-support": "^0.5.20", 49 | "supertest": "^6.1.3", 50 | "ts-jest": "^27.0.3", 51 | "ts-loader": "^9.2.3", 52 | "ts-node": "^10.0.0", 53 | "tsconfig-paths": "^3.10.1", 54 | "typescript": "^4.3.5" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".*\\.spec\\.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/contexts/TwitterContext.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, createContext, useContext } from 'react'; 2 | 3 | interface ContextProps { 4 | data: any; 5 | loading: boolean; 6 | handleToggleComposetweet: any; 7 | toggleComposeTweet: boolean; 8 | tweet: string; 9 | setTweet: any; 10 | postTweet: any; 11 | deleteTweet: any; 12 | } 13 | 14 | const TwitterContext = createContext({} as ContextProps); 15 | 16 | export const useTwitter = () => useContext(TwitterContext); 17 | 18 | const TwitterContextProvider = (props: any): any => { 19 | useEffect(() => { 20 | const getTweets = () => { 21 | const API = 'http://localhost:8080/tweets'; 22 | 23 | fetch(API) 24 | .then((response) => { 25 | console.log(response); 26 | return response.json(); 27 | }) 28 | .then((data) => { 29 | console.log(data); 30 | setLoading(false); 31 | setData(data); 32 | }) 33 | .catch((err) => { 34 | console.log(err); 35 | }); 36 | }; 37 | getTweets(); 38 | }, []); 39 | const [data, setData] = useState([]); 40 | const [loading, setLoading] = useState(true); 41 | const [toggleComposeTweet, setToggleComposeTweet] = useState(false); 42 | const [tweet, setTweet] = useState(''); 43 | 44 | const handleToggleComposetweet = () => { 45 | toggleComposeTweet === true ? setToggleComposeTweet(false) : setToggleComposeTweet(true); 46 | }; 47 | 48 | const postTweet = () => { 49 | if (tweet === '') { 50 | let myHeaders = new Headers(); 51 | myHeaders.append('Content-Type', 'application/json'); 52 | 53 | let raw = JSON.stringify({ 54 | tweet: 'Congratulations this is what happens when you post an empty tweet 🤪 Create some validation 🙃', 55 | img: '', 56 | }); 57 | 58 | fetch('http://localhost:8080/tweets', { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow' }) 59 | .then((response) => response.text()) 60 | .then((result) => console.log(result)) 61 | .catch((error) => console.log('error', error)); 62 | } else { 63 | let myHeaders = new Headers(); 64 | myHeaders.append('Content-Type', 'application/json'); 65 | 66 | let raw = JSON.stringify({ 67 | tweet: tweet, 68 | img: '', 69 | }); 70 | 71 | fetch('http://localhost:8080/tweets', { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow' }) 72 | .then((response) => response.text()) 73 | .then((result) => console.log(result)) 74 | .catch((error) => console.log('error', error)); 75 | } 76 | }; 77 | 78 | const deleteTweet = (tweetId: string) => { 79 | console.log('Deleted', tweetId); 80 | let urlencoded = new URLSearchParams(); 81 | 82 | fetch(`http://localhost:8080/tweets/${tweetId}`, { 83 | method: 'DELETE', 84 | body: urlencoded, 85 | redirect: 'follow', 86 | }) 87 | .then((response) => response.text()) 88 | .then((result) => console.log(result)) 89 | .catch((error) => console.log('error', error)); 90 | 91 | window.location.reload(); 92 | }; 93 | 94 | const value = { 95 | data, 96 | loading, 97 | toggleComposeTweet, 98 | handleToggleComposetweet, 99 | postTweet, 100 | tweet, 101 | setTweet, 102 | deleteTweet, 103 | }; 104 | 105 | return {props.children}; 106 | }; 107 | 108 | export default TwitterContextProvider; 109 | -------------------------------------------------------------------------------- /backend-nest-mongodb/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /frontend/src/stories/TwitterMenu/TwitterMenu.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | 3 | import TwitterMenu from './TwitterMenu'; 4 | 5 | describe('', () => { 6 | it('has a name called home', () => { 7 | render( 8 | 18 | ); 19 | const el = screen.getByTestId('home'); 20 | 21 | expect(el.innerHTML).toBe('Home'); 22 | }); 23 | it('has a name called explore', () => { 24 | render( 25 | 35 | ); 36 | const el = screen.getByTestId('explore'); 37 | 38 | expect(el.innerHTML).toBe('Explore'); 39 | }); 40 | it('has a name called notifications', () => { 41 | render( 42 | 52 | ); 53 | const el = screen.getByTestId('notifications'); 54 | 55 | expect(el.innerHTML).toBe('Notifications'); 56 | }); 57 | it('has a name name called messages', () => { 58 | render( 59 | 69 | ); 70 | const el = screen.getByTestId('messages'); 71 | 72 | expect(el.innerHTML).toBe('Messages'); 73 | }); 74 | it('has a name called bookmarks', () => { 75 | render( 76 | 86 | ); 87 | const el = screen.getByTestId('bookmarks'); 88 | 89 | expect(el.innerHTML).toBe('Bookmarks'); 90 | }); 91 | it('has a name name called lists', () => { 92 | render( 93 | 103 | ); 104 | const el = screen.getByTestId('lists'); 105 | 106 | expect(el.innerHTML).toBe('Lists'); 107 | }); 108 | it('has a name called profile', () => { 109 | render( 110 | 120 | ); 121 | const el = screen.getByTestId('profile'); 122 | 123 | expect(el.innerHTML).toBe('Profile'); 124 | }); 125 | it('has a name called more', () => { 126 | render( 127 | 137 | ); 138 | const el = screen.getByTestId('more'); 139 | 140 | expect(el.innerHTML).toBe('More'); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Complete React Developer 2 | 3 | ![The Complete React Developer](/img/complete-react-developer.png) 4 | 5 | ## Reference material 6 | 7 | These articles have the tutorials and documentation for this codebase. 8 | 9 | [https://dev.to/livecycle/how-to-build-and-deploy-a-modern-day-nextjs-application-mgn](https://dev.to/livecycle/how-to-build-and-deploy-a-modern-day-nextjs-application-mgn) 10 | 11 | [https://andrewbaisden.hashnode.dev/the-complete-modern-react-developer-2022](https://andrewbaisden.hashnode.dev/the-complete-modern-react-developer-2022) 12 | 13 | ## Setup 14 | 15 | 1. Start the Docker Desktop Application on your computer 16 | 17 | 2. `cd` into the root folder for **backend-nest-mongodb** and **frontend** and then run the commands below to install the dependencies. You will probably need to force the installation when trying to install the dependencies for the frontend React application in this case otherwise it could give you an error. 18 | 19 | ```shell 20 | # Run this command inside of the backend-nest-mongodb folder 21 | npm install 22 | 23 | # Run this command inside of the frontend folder 24 | npm install --force 25 | ``` 26 | 27 | 3. `cd` into the root folder for **docker-twitter-mongodb** and run the command below to start the MongoDB database inside of a Docker Container. 28 | 29 | ```shell 30 | docker compose up 31 | ``` 32 | 33 | 4. `cd` into the root folder for **backend-nest-mongodb** and run the command below to start the backend NestJS server. 34 | 35 | ```shell 36 | npm run start:dev 37 | ``` 38 | 39 | 5. `cd` into the root folder for **frontend** and run the command below to start the frontend React server. 40 | 41 | ```shell 42 | npm run start 43 | ``` 44 | 45 | ## The Twitter Clone App 46 | 47 | You should see your database running inside of a Docker Container and your Twitter Clone React application open in the browser. 48 | 49 | Run these commands inside of the root folder for frontend which is where React is. The command below starts Storybook. 50 | 51 | ```shell 52 | # Starts Storybook 53 | npm run storybook 54 | ``` 55 | 56 | You should see a Storybook component library open in the browser with a component for composing tweets. You can play around and change the names in the control to see how it looks in the demo. The command below runs the unit and integration tests. 57 | 58 | ```shell 59 | # Runs the React testing library unit and integration tests 60 | npm run jest 61 | ``` 62 | 63 | All of the tests should be passing in your console. The command below starts Cypress. 64 | 65 | ```shell 66 | # Runs the Cypress End-To-End tests 67 | npx cypress open 68 | ``` 69 | 70 | A new Cypress window should open. Run the integration test and get ready to be amazed as it automatically posts 3 tweets for you! 71 | 72 | ## REST API Testing 73 | 74 | Use these example routes for testing in Postman or whatever REST API tool you are using: 75 | 76 | ### GET all tweets 77 | 78 | Request: GET 79 | Route: [http://localhost:8080/tweets](http://localhost:8080/tweets) 80 | 81 | ### GET tweet by ID 82 | 83 | Request: GET 84 | Route: [http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1](http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1) 85 | 86 | ### CREATE tweet 87 | 88 | Request: POST 89 | Route: [http://localhost:8080/tweets](http://localhost:8080/tweets) 90 | 91 | Body raw: {"tweet": 'Hello World', img: ""} 92 | 93 | ### UPDATE tweet by ID 94 | 95 | Request: POST 96 | Route: [http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1](http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1) 97 | 98 | Body raw: {"tweet": 'Hello Moon', img: ""} 99 | 100 | ### DELETE tweet by ID 101 | 102 | Request: DELETE 103 | Route: [http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1](http://localhost:8080/tweets/d5d29839-788f-4d23-99ee-82b49ff1bbf1) 104 | -------------------------------------------------------------------------------- /frontend/src/stories/TwitterMenu/TwitterMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useTwitter } from '../../contexts/TwitterContext'; 2 | import '../../App.css'; 3 | 4 | interface TwitterMenuProps { 5 | home: string; 6 | explore: string; 7 | notifications: string; 8 | messages: string; 9 | bookmarks: string; 10 | lists: string; 11 | profile: string; 12 | more: string; 13 | } 14 | 15 | export const TwitterMenu = ({ 16 | home, 17 | explore, 18 | notifications, 19 | messages, 20 | bookmarks, 21 | lists, 22 | profile, 23 | more, 24 | }: TwitterMenuProps) => { 25 | const { handleToggleComposetweet } = useTwitter(); 26 | return ( 27 | <> 28 | 137 | 138 | ); 139 | }; 140 | 141 | export default TwitterMenu; 142 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'); 2 | 3 | *, 4 | **::before, 5 | *::after { 6 | padding: 0; 7 | margin: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | font-size: 16px; 13 | } 14 | 15 | body { 16 | font-size: 1rem; 17 | font-family: 'Roboto', sans-serif; 18 | color: #ffffff; 19 | background-color: #15202b; 20 | } 21 | 22 | .container { 23 | display: flex; 24 | flex-flow: row nowrap; 25 | justify-content: space-around; 26 | margin: 0 auto; 27 | max-width: 90rem; 28 | width: 100%; 29 | height: 100vh; 30 | } 31 | 32 | /* Sidebar Menu */ 33 | .twitter-logo { 34 | margin: 2rem 0 0 1rem; 35 | } 36 | 37 | .twitter-logo svg { 38 | width: 2rem; 39 | } 40 | 41 | aside { 42 | width: 10rem; 43 | display: flex; 44 | flex-flow: column nowrap; 45 | position: fixed; 46 | left: 12rem; 47 | } 48 | 49 | aside nav { 50 | width: 10rem; 51 | } 52 | 53 | aside nav a { 54 | text-decoration: none; 55 | color: #ffffff; 56 | font-size: 1.2rem; 57 | font-weight: 600; 58 | display: block; 59 | padding: 0.2rem 0.8rem 0.2rem 0.8rem; 60 | width: 10rem; 61 | } 62 | 63 | aside nav a:hover { 64 | background-color: rgb(38, 53, 68); 65 | border-radius: 10rem; 66 | } 67 | 68 | aside nav div { 69 | display: flex; 70 | flex-flow: row nowrap; 71 | } 72 | 73 | aside nav svg { 74 | width: 1.8rem; 75 | margin-right: 1rem; 76 | } 77 | 78 | .tweet-button { 79 | margin-top: 1rem; 80 | } 81 | 82 | .tweet-button button { 83 | background: #1a8cd8; 84 | border-radius: 5rem; 85 | color: #ffffff; 86 | padding: 0.8rem; 87 | width: 14rem; 88 | font-weight: 600; 89 | font-size: 1.2rem; 90 | border: none; 91 | text-align: center; 92 | cursor: pointer; 93 | } 94 | 95 | .tweet-button button:hover { 96 | background: #209aeb; 97 | } 98 | 99 | .twitter-profile-accounts { 100 | display: flex; 101 | flex-flow: row nowrap; 102 | align-items: center; 103 | margin-top: 30rem; 104 | width: 12rem; 105 | } 106 | 107 | .profile-picture { 108 | border-radius: 100%; 109 | background-color: #8480de; 110 | height: 2.5rem; 111 | width: 2.5rem; 112 | margin-right: 0.5rem; 113 | } 114 | 115 | .twitter-profile-accounts-user h1 { 116 | font-size: 1rem; 117 | margin: 0rem; 118 | } 119 | 120 | .twitter-profile-accounts-user p { 121 | color: rgb(139, 152, 165); 122 | margin: 0rem; 123 | } 124 | 125 | .twitter-profile-accounts-menu { 126 | margin-left: 1rem; 127 | font-weight: 600; 128 | font-size: 1.4rem; 129 | } 130 | 131 | /* Sidebar Content */ 132 | article { 133 | width: 24rem; 134 | position: fixed; 135 | right: 3rem; 136 | } 137 | 138 | .twitter-gallery { 139 | display: flex; 140 | flex-flow: row wrap; 141 | width: 24rem; 142 | } 143 | 144 | .twitter-gallery-image { 145 | border: 0.1rem solid black; 146 | height: 5rem; 147 | width: 7.6rem; 148 | background-color: #b781b2; 149 | } 150 | 151 | .twitter-gallery-image-top-left-round { 152 | border-top-left-radius: 1rem; 153 | } 154 | 155 | .twitter-gallery-image-top-right-round { 156 | border-top-right-radius: 1rem; 157 | } 158 | 159 | .twitter-gallery-image-bottom-left-round { 160 | border-bottom-left-radius: 1rem; 161 | } 162 | 163 | .twitter-gallery-image-bottom-right-round { 164 | border-bottom-right-radius: 1rem; 165 | } 166 | 167 | form { 168 | color: rgb(255, 255, 255); 169 | display: flex; 170 | padding: 2px; 171 | border: 1px solid currentColor; 172 | border-radius: 5px; 173 | margin: 0 0 30px; 174 | } 175 | 176 | input[type='search'] { 177 | border: none; 178 | background: transparent; 179 | margin: 0; 180 | padding: 7px 8px; 181 | font-size: 14px; 182 | color: inherit; 183 | border: 1px solid transparent; 184 | border-radius: inherit; 185 | } 186 | 187 | input[type='search']::placeholder { 188 | color: #bbb; 189 | } 190 | 191 | button[type='submit'] { 192 | text-indent: -999px; 193 | overflow: hidden; 194 | width: 40px; 195 | padding: 0; 196 | margin: 0; 197 | border: 1px solid transparent; 198 | border-radius: inherit; 199 | background: transparent 200 | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'%3E%3C/path%3E%3C/svg%3E") 201 | no-repeat center; 202 | cursor: pointer; 203 | opacity: 0.7; 204 | } 205 | 206 | button[type='submit']:hover { 207 | opacity: 1; 208 | } 209 | 210 | button[type='submit']:focus, 211 | input[type='search']:focus { 212 | box-shadow: 0 0 3px 0 #1183d6; 213 | border-color: #1183d6; 214 | outline: none; 215 | } 216 | 217 | form.nosubmit { 218 | margin-top: 0.5rem; 219 | border: none; 220 | padding: 0; 221 | } 222 | 223 | input.nosubmit { 224 | border: none; 225 | border-radius: 4rem; 226 | width: 100%; 227 | padding: 1rem 3rem; 228 | background: #273340 229 | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'%3E%3C/path%3E%3C/svg%3E") 230 | no-repeat 13px center; 231 | } 232 | 233 | .you-might-like, 234 | .whats-happening { 235 | margin-top: 1rem; 236 | width: auto; 237 | height: auto; 238 | background: #1e2732; 239 | display: flex; 240 | flex-flow: column nowrap; 241 | justify-content: space-around; 242 | border-radius: 1rem; 243 | } 244 | 245 | .whats-happening > h1 { 246 | margin: 2rem 0 2rem 1rem; 247 | font-size: 1.5rem; 248 | } 249 | 250 | .you-might-like h1 { 251 | font-size: 1.5rem; 252 | margin: 1rem; 253 | } 254 | 255 | .you-might-like a, 256 | .whats-happening a { 257 | text-decoration: none; 258 | color: rgb(29, 155, 240); 259 | display: block; 260 | margin: 2rem 0 1rem 1rem; 261 | } 262 | 263 | .twitter-profile-accounts-users { 264 | display: flex; 265 | flex-flow: row nowrap; 266 | align-items: center; 267 | justify-content: space-between; 268 | padding: 1rem; 269 | } 270 | 271 | .twitter-profile-accounts-users h1 { 272 | font-size: 1rem; 273 | margin: 0rem; 274 | } 275 | 276 | .twitter-profile-accounts-users p { 277 | color: rgb(139, 152, 165); 278 | margin: 0rem; 279 | } 280 | 281 | .twitter-profile-accounts-users button { 282 | border: none; 283 | background: #ffffff; 284 | color: #000000; 285 | border-radius: 5rem; 286 | padding: 0.5rem 1rem 0.5rem 1rem; 287 | font-weight: 600; 288 | } 289 | 290 | .user-profile { 291 | display: flex; 292 | flex-flow: row nowrap; 293 | } 294 | 295 | .twitter-trending h1 { 296 | margin: 0 0 0 1rem; 297 | } 298 | 299 | .twitter-trending p { 300 | margin: 0.5rem 0 0.5rem 1rem; 301 | color: rgb(139, 152, 165); 302 | font-size: 0.9rem; 303 | } 304 | 305 | .twitter-trending { 306 | display: flex; 307 | flex-flow: row nowrap; 308 | justify-content: space-between; 309 | } 310 | 311 | .twitter-trending:hover, 312 | .twitter-profile-accounts-users:hover { 313 | background: #252e38; 314 | cursor: pointer; 315 | } 316 | 317 | .twitter-trending-content-img { 318 | background: #b982ae; 319 | height: 4rem; 320 | width: 4rem; 321 | border-radius: 1rem; 322 | margin-right: 1rem; 323 | } 324 | 325 | .twitter-footer-links { 326 | margin-top: 1rem; 327 | } 328 | 329 | .twitter-footer-links nav a { 330 | text-decoration: none; 331 | color: rgb(139, 152, 165); 332 | display: inline-block; 333 | padding: 0.3rem; 334 | font-size: 0.9rem; 335 | } 336 | 337 | .twitter-footer-links nav a:hover { 338 | text-decoration: underline; 339 | } 340 | .twitter-footer-links p { 341 | font-size: 0.9rem; 342 | color: rgb(139, 152, 165); 343 | margin: 0; 344 | } 345 | 346 | .twitter-messages { 347 | display: flex; 348 | flex-flow: row nowrap; 349 | align-items: center; 350 | justify-content: space-between; 351 | padding: 0.5rem 1rem 0.5rem 1rem; 352 | background: #15202b; 353 | border-top-left-radius: 0.5rem; 354 | border-top-right-radius: 0.5rem; 355 | box-shadow: 0px 0px 5px 0.1rem #2b3f51; 356 | cursor: pointer; 357 | position: fixed; 358 | bottom: 0; 359 | z-index: 1; 360 | width: 22rem; 361 | } 362 | 363 | .twitter-messages-icons { 364 | display: flex; 365 | flex-flow: row nowrap; 366 | align-items: center; 367 | justify-content: space-between; 368 | width: 4rem; 369 | } 370 | 371 | .twitter-messages svg { 372 | width: 1.5rem; 373 | height: 1.5rem; 374 | } 375 | 376 | /* Main Twitter Content */ 377 | main { 378 | display: flex; 379 | width: 40rem; 380 | flex-flow: column nowrap; 381 | height: auto; 382 | border-left: 0.1rem solid rgb(46, 55, 64); 383 | border-right: 0.1rem solid rgb(46, 55, 64); 384 | } 385 | 386 | .twitter-profile-main { 387 | border-left: 0.1rem solid rgb(46, 55, 64); 388 | border-right: 0.1rem solid rgb(46, 55, 64); 389 | } 390 | 391 | .twitter-profile-main-tweets { 392 | display: flex; 393 | flex-flow: row nowrap; 394 | align-items: center; 395 | border-left: 0.1rem solid rgb(46, 55, 64); 396 | border-right: 0.1rem solid rgb(46, 55, 64); 397 | border-bottom: 0.1rem solid rgb(46, 55, 64); 398 | } 399 | 400 | .twitter-profile-main-tweets svg { 401 | height: 3rem; 402 | width: 1.5rem; 403 | margin: 0 2rem 0 1rem; 404 | } 405 | 406 | .twitter-profile-main-tweets h1 { 407 | margin: 1rem 0 0 0; 408 | font-size: 1.4rem; 409 | } 410 | 411 | .twitter-profile-main-tweets p { 412 | color: rgb(139, 152, 165); 413 | margin: 0; 414 | } 415 | 416 | .twitter-profile-main-header { 417 | background-color: #edf2fb; 418 | height: 12.458rem; 419 | width: auto; 420 | margin-top: 0.5rem; 421 | } 422 | 423 | .profile-image-main { 424 | background-color: #8480de; 425 | height: 8rem; 426 | width: 8rem; 427 | border-radius: 100%; 428 | border: 0.2rem solid black; 429 | position: relative; 430 | top: 8rem; 431 | left: 1rem; 432 | } 433 | 434 | .twitter-profile-edit { 435 | position: relative; 436 | top: 5rem; 437 | left: 32.5rem; 438 | } 439 | 440 | .twitter-profile-edit button { 441 | background-color: #15202b; 442 | color: #ffffff; 443 | border: 0.01rem solid #919191; 444 | border-radius: 2rem; 445 | padding: 0.6rem; 446 | font-size: 1rem; 447 | font-weight: 600; 448 | } 449 | 450 | .twitter-profile-main-name { 451 | margin-top: 4rem; 452 | padding: 1rem; 453 | } 454 | 455 | .twitter-profile-main-name h1, 456 | .twitter-profile-main-name p { 457 | margin: 0; 458 | } 459 | 460 | .twitter-profile-main-name p { 461 | color: rgb(139, 152, 165); 462 | } 463 | 464 | .twitter-profile-main-description { 465 | padding: 0 1rem 0 1rem; 466 | } 467 | 468 | .twitter-profile-main-stats { 469 | display: flex; 470 | flex-flow: row nowrap; 471 | align-items: center; 472 | color: rgb(139, 152, 165); 473 | margin-left: 1rem; 474 | } 475 | 476 | .twitter-profile-main-stats div { 477 | display: flex; 478 | flex-flow: row nowrap; 479 | align-items: center; 480 | margin: 0 0.2rem 0 0.2rem; 481 | } 482 | 483 | .twitter-profile-main-stats svg { 484 | height: 1.2rem; 485 | width: 1.2rem; 486 | margin-right: 0.2rem; 487 | } 488 | 489 | .twitter-profile-main-stats a { 490 | color: rgb(29, 155, 240); 491 | text-decoration: none; 492 | } 493 | 494 | .twitter-profile-main-followers { 495 | display: flex; 496 | flex-flow: row nowrap; 497 | margin-left: 1rem; 498 | } 499 | 500 | .twitter-profile-main-followers p { 501 | font-weight: 600; 502 | margin: 0; 503 | } 504 | 505 | .twitter-profile-main-followers p span { 506 | font-weight: 100; 507 | color: rgb(139, 152, 165); 508 | } 509 | 510 | .twitter-following { 511 | padding-right: 2rem; 512 | } 513 | 514 | .twitter-profile-main-navigation { 515 | border-bottom: 0.1rem solid rgb(46, 55, 64); 516 | } 517 | 518 | .twitter-profile-main-navigation nav { 519 | display: flex; 520 | flex-flow: row nowrap; 521 | justify-content: space-around; 522 | margin-top: 1rem; 523 | } 524 | 525 | .twitter-profile-main-navigation nav a { 526 | text-decoration: none; 527 | font-weight: 600; 528 | padding: 1rem; 529 | display: inline-block; 530 | width: 100%; 531 | text-align: center; 532 | } 533 | 534 | .twitter-profile-main-navigation nav a:hover { 535 | background: #2c3640; 536 | } 537 | 538 | .selected { 539 | color: #ffffff; 540 | border-bottom: 0.3rem solid #1a8cd8; 541 | } 542 | 543 | .unselected { 544 | color: rgb(139, 152, 165); 545 | } 546 | 547 | .twitter-profile-picture-container { 548 | height: 9rem; 549 | padding: 0.5rem; 550 | } 551 | 552 | .twitter-profile-main-tweets-content { 553 | width: 100%; 554 | } 555 | 556 | .twitter-profile-main-tweets-header { 557 | display: flex; 558 | flex-flow: row nowrap; 559 | justify-content: space-between; 560 | margin-top: 1rem; 561 | } 562 | 563 | .twitter-profile-main-tweets-header p { 564 | color: #ffffff; 565 | font-weight: 600; 566 | margin-top: 1rem; 567 | } 568 | 569 | .twitter-profile-main-tweets-header p span { 570 | color: rgb(139, 152, 165); 571 | font-weight: normal; 572 | } 573 | 574 | .twitter-profile-main-tweets-post p { 575 | margin-top: 0.3rem; 576 | color: #ffffff; 577 | } 578 | 579 | .twitter-profile-main-tweets-header button { 580 | margin-right: 1rem; 581 | border: none; 582 | background: none; 583 | color: rgb(139, 152, 165); 584 | cursor: pointer; 585 | width: 4rem; 586 | } 587 | 588 | .twitter-profile-main-tweets-header button svg { 589 | position: relative; 590 | right: 0.2rem; 591 | top: 0.1rem; 592 | } 593 | 594 | .twitter-profile-main-tweets-header button:hover { 595 | background-color: #1c3345; 596 | border-radius: 100%; 597 | color: #1183d6; 598 | } 599 | 600 | .twitter-profile-main-tweets-social { 601 | display: flex; 602 | flex-flow: row nowrap; 603 | justify-content: space-between; 604 | } 605 | 606 | .twitter-profile-main-tweets-social div svg { 607 | cursor: pointer; 608 | margin-top: 0.5rem; 609 | margin-left: 0; 610 | height: 2rem; 611 | width: 1.2rem; 612 | } 613 | 614 | /* Compose Tweet */ 615 | .compose-tweet-container { 616 | width: 37.5rem; 617 | height: 17.375rem; 618 | border-radius: 1rem; 619 | background-color: #15202b; 620 | border: 0.02rem solid white; 621 | display: flex; 622 | flex-flow: row nowrap; 623 | padding: 0.5rem; 624 | position: absolute; 625 | z-index: 1; 626 | top: 2rem; 627 | left: 38%; 628 | } 629 | .compose-tweet-profile { 630 | display: flex; 631 | flex-flow: column nowrap; 632 | align-items: flex-start; 633 | } 634 | 635 | .compose-tweet-profile button { 636 | border: none; 637 | background: none; 638 | color: #ffffff; 639 | font-size: 1.2rem; 640 | margin-bottom: 1rem; 641 | cursor: pointer; 642 | } 643 | 644 | .compose-tweet-content { 645 | width: 100%; 646 | margin-top: 2.2rem; 647 | } 648 | 649 | .compose-tweet-content form { 650 | width: 100%; 651 | display: flex; 652 | flex-flow: column nowrap; 653 | justify-content: space-between; 654 | border: none; 655 | } 656 | 657 | .compose-tweet-content textarea { 658 | height: 10rem; 659 | resize: none; 660 | padding: 1rem; 661 | background-color: #15202b; 662 | border: none; 663 | overflow: auto; 664 | outline: none; 665 | box-shadow: none; 666 | color: #ffffff; 667 | font-size: 1.3rem; 668 | } 669 | 670 | .compose-tweet-post { 671 | display: flex; 672 | flex-flow: row nowrap; 673 | justify-content: flex-end; 674 | border-top: 0.01rem solid rgb(118, 118, 118); 675 | padding-top: 1rem; 676 | } 677 | 678 | .compose-tweet-post input { 679 | color: #ffffff; 680 | background: #1a8cd8; 681 | border: none; 682 | font-weight: 600; 683 | border-radius: 2rem; 684 | padding: 0.5rem 1.3rem 0.5rem 1.3rem; 685 | cursor: pointer; 686 | } 687 | 688 | .compose-tweet-post input:hover { 689 | background: #209aeb; 690 | } 691 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import TwitterMenu from './stories/TwitterMenu/TwitterMenu'; 2 | import { useTwitter } from './contexts/TwitterContext'; 3 | import './App.css'; 4 | 5 | const App = () => { 6 | const { data, loading, handleToggleComposetweet, deleteTweet, toggleComposeTweet, postTweet, tweet, setTweet } = 7 | useTwitter(); 8 | return ( 9 | <> 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | 40 |
41 |
42 |
43 |
44 | 49 |
50 |
51 |

Levi Ackerman

52 |

13.5k Tweets

53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |

Levi Ackerman

64 |

@leviackerman

65 |
66 |
67 | ⚛️ Software Developer | 🌅 Technical Writer | 🎨 Content Creator | 📝 1 Million+ Subscribers 68 |
69 |
70 |
71 | 77 |

Paradis Island

78 |
79 |
80 | 86 |

87 | limey.io/leviackerman 88 |

89 |
90 |
91 | 104 |

Joined January 2012

105 |
106 |
107 |
108 |

109 | 100 Following 110 |

111 |

112 | 300K Followers 113 |

114 |
115 | 131 |
132 | {/* Posted Tweets */} 133 | {loading ? ( 134 |
135 |

Loading...

136 |
137 | ) : ( 138 |
139 | {data?.map((tweet: any) => ( 140 |
141 |
142 |
143 |
144 |
145 |
146 |

147 | Levi Ackerman @leviackerman May 2 148 |

149 | 150 | 158 |
159 |
160 |

{tweet.tweet}

161 | {tweet.img === '' ?
: Tweet Media} 162 |
163 |
164 |
165 | 170 |
171 |
172 | 177 |
178 |
179 | 184 |
185 |
186 | 192 |
193 |
194 | 199 |
200 |
201 |
202 |
203 | ))} 204 |
205 | )} 206 |
207 |
208 |
209 |
210 |
211 | 212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |

You might like

224 | 225 |
226 |
227 |
228 |
229 |

Eren Yeager

230 |

@erenyeager

231 |
232 |
233 |
234 | 235 |
236 |
237 |
238 |
239 |
240 |
241 |

Mikasa Ackerman

242 |

@mikasaackerman

243 |
244 |
245 |
246 | 247 |
248 |
249 | Show more 250 |
251 |
252 |

Whats happening

253 |
254 |
255 |

Holidays - LIVE

256 |

It's May the Fourth

257 |

64k Tweets

258 |
259 |
260 |
261 |
262 |
263 |

Trending in United Kingdom

264 |

Pfizer

265 |

305K Tweets

266 |
267 |
268 |
269 |
270 |
271 |

Entertainment - Trending

272 |

Elizabeth Olsen

273 |

38.9K Tweets

274 |
275 |
276 |
277 |
278 |
279 |

UK national news - LIVE

280 |

Summer heatwave predicted

281 |

2,145 Tweets

282 |
283 |
284 |
285 |
286 |
287 |

ET Canada

288 |

New 'Obi-Wan Kenobi' trailer

289 |

5,905 Tweets

290 |
291 |
292 |
293 | Show more 294 |
295 |
296 |
297 | 305 |

© 2022 Twitter, Inc.

306 |
307 |
308 |
309 |

Messages

310 |
311 |
312 | 317 |
318 |
319 | 325 |
326 |
327 |
328 |
329 |
330 |
331 | 332 | ); 333 | }; 334 | 335 | export default App; 336 | --------------------------------------------------------------------------------