├── front ├── pngTransform.js ├── prod.env ├── config │ ├── test │ │ ├── setup.ts │ │ └── jest.js │ └── webpack │ │ ├── helpers.js │ │ ├── analyze.js │ │ ├── dev.js │ │ ├── prod.js │ │ └── base.js ├── src │ ├── common │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── use-with-ref.tsx │ │ │ └── auto-scroll.hook.tsx │ │ ├── index.ts │ │ └── global-window.ts │ ├── pods │ │ ├── student │ │ │ ├── index.ts │ │ │ ├── student.styles.ts │ │ │ ├── student.container.tsx │ │ │ ├── student.component.tsx │ │ │ └── student.component.spec.tsx │ │ ├── trainer │ │ │ ├── index.ts │ │ │ ├── trainer.styles.ts │ │ │ ├── trainer.component.spec.tsx │ │ │ ├── components │ │ │ │ ├── new-text.styles.ts │ │ │ │ ├── header.component.spec.tsx │ │ │ │ ├── new-text.component.tsx │ │ │ │ ├── new-text.component.spec.tsx │ │ │ │ ├── header.styles.ts │ │ │ │ ├── session.component.spec.tsx │ │ │ │ ├── header.component.tsx │ │ │ │ ├── session.component.tsx │ │ │ │ └── session.styles.ts │ │ │ ├── trainer.component.tsx │ │ │ └── trainer.container.tsx │ │ └── create-session │ │ │ ├── index.ts │ │ │ ├── api │ │ │ └── create-session.api.ts │ │ │ ├── create-session.container.tsx │ │ │ ├── create-session.component.tsx │ │ │ ├── create-session.component.spec.tsx │ │ │ └── create-session.styles.ts │ ├── common-app │ │ ├── components │ │ │ ├── appbar │ │ │ │ ├── index.ts │ │ │ │ ├── appbar.component.tsx │ │ │ │ └── appbar.styles.ts │ │ │ └── footer │ │ │ │ ├── index.ts │ │ │ │ ├── footer.component.tsx │ │ │ │ └── footer.styles.ts │ │ └── index.ts │ ├── layout │ │ ├── index.ts │ │ ├── app-layout.tsx │ │ └── session-layout.tsx │ ├── core │ │ ├── theme │ │ │ ├── index.ts │ │ │ ├── theme-provider.component.tsx │ │ │ ├── theme.vm.ts │ │ │ └── theme.ts │ │ ├── router │ │ │ ├── index.ts │ │ │ ├── router.component.tsx │ │ │ ├── routes.ts │ │ │ └── routes.spec.ts │ │ ├── index.ts │ │ ├── use-log.hook.ts │ │ ├── api.ts │ │ ├── const.ts │ │ └── use-log.hook.spec.ts │ ├── assets │ │ ├── bg-create-session.png │ │ ├── favicon │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-384x384.png │ │ │ ├── browserconfig.xml │ │ │ ├── site.webmanifest │ │ │ └── safari-pinned-tab.svg │ │ ├── github-logo.svg │ │ ├── lemoncode-logo.svg │ │ └── logo.svg │ ├── scenes │ │ ├── index.ts │ │ ├── trainer.scene.tsx │ │ ├── create-session.scene.tsx │ │ └── student.scene.tsx │ ├── app.tsx │ ├── index.tsx │ └── app.html ├── .vscode │ └── settings.json ├── global.types.d.ts ├── .prettierrc ├── dev.env ├── static │ ├── assets │ │ ├── alicia-ga.png │ │ ├── codepaster.mp4 │ │ ├── juan-pablo.jpg │ │ ├── braulio-diez.png │ │ ├── daniel-sanchez.png │ │ ├── alberto-caparros.jpg │ │ ├── miguel-angel-romero.jpg │ │ ├── sergio-conde-ortega.png │ │ ├── miguel-angel-vazquez.jpg │ │ └── sprite.svg │ └── styles.css ├── .gitignore ├── .babelrc ├── .editorconfig ├── tsconfig.json ├── LICENSE └── package.json ├── back ├── src │ ├── core │ │ ├── db │ │ │ ├── index.ts │ │ │ └── database.connection.ts │ │ ├── constants │ │ │ ├── index.ts │ │ │ ├── header.constants.ts │ │ │ └── env.constants.ts │ │ ├── index.ts │ │ └── servers │ │ │ ├── index.ts │ │ │ ├── express.server.ts │ │ │ ├── sockets.server.ts │ │ │ └── cors.ts │ ├── pods │ │ ├── room │ │ │ ├── index.ts │ │ │ ├── room.api.ts │ │ │ ├── room-name-generator.business.ts │ │ │ ├── room-name-generator.business.spec.ts │ │ │ ├── baseTrainerTokens.ts │ │ │ └── baseNames.ts │ │ └── messages │ │ │ ├── index.ts │ │ │ ├── processors │ │ │ ├── index.ts │ │ │ ├── response.ts │ │ │ ├── output-processor.ts │ │ │ └── input-processor.ts │ │ │ ├── messages.model.ts │ │ │ ├── messages.consts.ts │ │ │ └── messages.socket-events.ts │ ├── dals │ │ ├── index.ts │ │ ├── room │ │ │ ├── room.model.ts │ │ │ ├── index.ts │ │ │ ├── repository │ │ │ │ ├── room.contract.ts │ │ │ │ ├── index.ts │ │ │ │ ├── room.repository.ts │ │ │ │ └── room.mock.ts │ │ │ └── room.context.ts │ │ └── session │ │ │ ├── index.ts │ │ │ ├── session.model.ts │ │ │ ├── repository │ │ │ ├── index.ts │ │ │ ├── session.contract.ts │ │ │ ├── session.repository.ts │ │ │ └── session.mock.ts │ │ │ └── session.context.ts │ ├── index.ts │ └── app.ts ├── .prettierrc ├── config │ └── test │ │ ├── setup.js │ │ └── jest.js ├── .gitignore ├── .editorconfig ├── .vscode │ ├── tasks.json │ └── launch.json ├── .env ├── docker-compose.yml ├── README.md ├── tsconfig.json ├── .babelrc └── package.json ├── images └── logo.png ├── .gitignore ├── .dockerignore ├── nginx.conf ├── .github └── workflows │ ├── ci.yml │ ├── prod-cd.yml │ └── dev-cd.yml ├── Dockerfile └── README.md /front/pngTransform.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/prod.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | -------------------------------------------------------------------------------- /back/src/core/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database.connection'; -------------------------------------------------------------------------------- /back/src/pods/room/index.ts: -------------------------------------------------------------------------------- 1 | export * from './room.api'; 2 | -------------------------------------------------------------------------------- /front/config/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /front/src/common/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-with-ref'; 2 | -------------------------------------------------------------------------------- /front/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /front/src/pods/student/index.ts: -------------------------------------------------------------------------------- 1 | export * from './student.container'; 2 | -------------------------------------------------------------------------------- /front/src/pods/trainer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trainer.container'; 2 | -------------------------------------------------------------------------------- /back/src/dals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './room'; 2 | export * from './session'; -------------------------------------------------------------------------------- /front/global.types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.svg'; 3 | -------------------------------------------------------------------------------- /front/src/common-app/components/appbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appbar.component'; 2 | -------------------------------------------------------------------------------- /front/src/common-app/components/footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer.component'; 2 | -------------------------------------------------------------------------------- /front/src/pods/create-session/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-session.container'; 2 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/images/logo.png -------------------------------------------------------------------------------- /front/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './global-window'; 3 | -------------------------------------------------------------------------------- /front/src/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app-layout'; 2 | export * from './session-layout'; 3 | -------------------------------------------------------------------------------- /back/src/core/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env.constants'; 2 | export * from './header.constants'; -------------------------------------------------------------------------------- /back/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | export * from './db' 3 | export * from './servers' -------------------------------------------------------------------------------- /back/src/core/servers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './express.server'; 2 | export * from './sockets.server' 3 | -------------------------------------------------------------------------------- /front/src/core/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme-provider.component'; 2 | export * from './theme'; 3 | -------------------------------------------------------------------------------- /back/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "endOfLine": "lf" 5 | } 6 | -------------------------------------------------------------------------------- /back/config/test/setup.js: -------------------------------------------------------------------------------- 1 | const { config } = require('dotenv'); 2 | config({ 3 | path: './.env', 4 | }); 5 | -------------------------------------------------------------------------------- /back/src/dals/room/room.model.ts: -------------------------------------------------------------------------------- 1 | export interface RoomInfo { 2 | room: string; 3 | content: string; 4 | } -------------------------------------------------------------------------------- /front/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "endOfLine": "lf" 5 | } 6 | -------------------------------------------------------------------------------- /front/src/common-app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/appbar'; 2 | export * from './components/footer'; 3 | -------------------------------------------------------------------------------- /front/src/core/router/index.ts: -------------------------------------------------------------------------------- 1 | export * from './router.component'; 2 | export { routes } from './routes'; 3 | -------------------------------------------------------------------------------- /back/src/pods/messages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './messages.socket-events'; 2 | export * from './messages.consts'; 3 | -------------------------------------------------------------------------------- /front/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './const'; 3 | export * from './use-log.hook'; 4 | -------------------------------------------------------------------------------- /back/src/pods/messages/processors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input-processor'; 2 | export * from './output-processor'; 3 | -------------------------------------------------------------------------------- /front/dev.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | BASE_API_URL=http://localhost:8081 3 | BASE_SOCKET_URL=http://localhost:8081 4 | -------------------------------------------------------------------------------- /front/static/assets/alicia-ga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/alicia-ga.png -------------------------------------------------------------------------------- /back/src/dals/room/index.ts: -------------------------------------------------------------------------------- 1 | export * from './room.model'; 2 | export * from './room.context'; 3 | export * from './repository'; -------------------------------------------------------------------------------- /front/static/assets/codepaster.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/codepaster.mp4 -------------------------------------------------------------------------------- /front/static/assets/juan-pablo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/juan-pablo.jpg -------------------------------------------------------------------------------- /front/src/assets/bg-create-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/bg-create-session.png -------------------------------------------------------------------------------- /front/src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /front/static/assets/braulio-diez.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/braulio-diez.png -------------------------------------------------------------------------------- /front/static/assets/daniel-sanchez.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/daniel-sanchez.png -------------------------------------------------------------------------------- /back/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import { config } from 'dotenv'; 3 | 4 | config(); 5 | require('./app'); 6 | -------------------------------------------------------------------------------- /front/src/common/global-window.ts: -------------------------------------------------------------------------------- 1 | export const getHostBaseUrl = () => 2 | `${window.location.origin}${window.location.pathname}`; 3 | -------------------------------------------------------------------------------- /front/static/assets/alberto-caparros.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/alberto-caparros.jpg -------------------------------------------------------------------------------- /back/src/dals/session/index.ts: -------------------------------------------------------------------------------- 1 | export * from './repository'; 2 | export * from './session.model'; 3 | export * from './session.context'; 4 | -------------------------------------------------------------------------------- /front/src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /front/src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /front/src/assets/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /front/src/scenes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-session.scene'; 2 | export * from './trainer.scene'; 3 | export * from './student.scene'; 4 | -------------------------------------------------------------------------------- /front/static/assets/miguel-angel-romero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/miguel-angel-romero.jpg -------------------------------------------------------------------------------- /front/static/assets/sergio-conde-ortega.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/sergio-conde-ortega.png -------------------------------------------------------------------------------- /front/src/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /front/static/assets/miguel-angel-vazquez.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/static/assets/miguel-angel-vazquez.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .awcache 5 | test-report.* 6 | junit.xml 7 | *.log 8 | *.orig 9 | .awcache 10 | public 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist 3 | node_modules 4 | .editorconfig 5 | .env 6 | .env.example 7 | .gitignore 8 | package-lock.json 9 | README.md 10 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .awcache 5 | test-report.* 6 | junit.xml 7 | *.log 8 | *.orig 9 | .awcache 10 | public 11 | -------------------------------------------------------------------------------- /back/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .awcache 5 | test-report.* 6 | junit.xml 7 | *.log 8 | *.orig 9 | .awcache 10 | public 11 | 12 | -------------------------------------------------------------------------------- /front/src/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /front/src/assets/favicon/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/code-paster/HEAD/front/src/assets/favicon/android-chrome-384x384.png -------------------------------------------------------------------------------- /back/src/core/constants/header.constants.ts: -------------------------------------------------------------------------------- 1 | export const headerConstants = { 2 | protocol: 'x-forwarded-proto', 3 | httpsProtocol: 'https', 4 | host: 'host', 5 | }; -------------------------------------------------------------------------------- /front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | "@babel/preset-react" 6 | ], 7 | "plugins": ["@emotion"] 8 | } 9 | -------------------------------------------------------------------------------- /front/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouterComponent } from 'core/router'; 3 | 4 | export const App: React.FunctionComponent = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /front/config/webpack/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.resolve(__dirname, '../../'); 4 | 5 | exports.resolveFromRootPath = (...args) => path.join(rootPath, ...args); 6 | -------------------------------------------------------------------------------- /back/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /front/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /back/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "npm", 4 | "type": "shell", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "args": ["run", "build:dev"], 9 | "group": "build" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /back/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | INTERNAL_PORT=8081 3 | MOCK_REPOSITORY=false 4 | API_URL=/api 5 | CORS_ORIGIN=http://localhost:8080 6 | MONGODB_URI=mongodb://localhost:27017/code-paster 7 | STATIC_FILES_PATH="../../front/dist" 8 | MONGO_EXPIRATION_TIME='2d' 9 | -------------------------------------------------------------------------------- /back/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | mongodb-codepaster: 4 | container_name: mongodb-codepaster 5 | image: mongo:6 6 | ports: 7 | - '27017:27017' 8 | networks: 9 | - codepaster 10 | 11 | networks: 12 | codepaster: 13 | -------------------------------------------------------------------------------- /front/config/webpack/analyze.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 3 | const config = require('./prod'); 4 | 5 | module.exports = merge(config, { 6 | plugins: [new BundleAnalyzerPlugin()], 7 | }); 8 | -------------------------------------------------------------------------------- /back/src/dals/session/session.model.ts: -------------------------------------------------------------------------------- 1 | export interface ConnectSessionInfo { 2 | room: string; 3 | trainerToken: string; 4 | isTrainer: boolean; 5 | } 6 | 7 | export interface UserSession extends ConnectSessionInfo { 8 | connectionId: string; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /back/src/core/servers/express.server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { corsOptions } from './cors'; 3 | 4 | export const createApp = () => { 5 | const app = express(); 6 | 7 | app.use(express.json()); 8 | app.use(corsOptions); 9 | 10 | return app; 11 | }; 12 | -------------------------------------------------------------------------------- /back/src/core/db/database.connection.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { connect } from 'mongoose'; 2 | mongoose.set('strictQuery', false); 3 | 4 | export const connectToDB = async (connectionString: string) => { 5 | const db = await connect(connectionString); 6 | 7 | console.log('Connected to DB'); 8 | 9 | return db; 10 | }; 11 | -------------------------------------------------------------------------------- /front/src/assets/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #fbf867 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /back/src/dals/room/repository/room.contract.ts: -------------------------------------------------------------------------------- 1 | import { RoomInfo } from 'dals'; 2 | 3 | export interface RoomRepositoryContract { 4 | saveRoomInfo: (newRoomInfo: RoomInfo, setFullText: boolean) => Promise; 5 | isRoomAvailable: (room: string) => Promise; 6 | getRoomContent:(room: string) => Promise; 7 | } 8 | -------------------------------------------------------------------------------- /back/config/test/jest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '../../', 3 | preset: 'ts-jest', 4 | restoreMocks: true, 5 | setupFiles: ['/config/test/setup.js'], 6 | modulePathIgnorePatterns: ['node_modules', '/dist/'], 7 | testEnvironment: 'node', 8 | moduleDirectories: ['/src', 'node_modules'], 9 | }; 10 | -------------------------------------------------------------------------------- /back/src/dals/room/repository/index.ts: -------------------------------------------------------------------------------- 1 | import { envConstants } from 'core'; 2 | import * as mockRepository from './room.mock'; 3 | import * as repository from './room.repository'; 4 | import { RoomRepositoryContract } from './room.contract'; 5 | 6 | export const roomRepository: RoomRepositoryContract = envConstants.isMockRepository 7 | ? mockRepository 8 | : repository; 9 | -------------------------------------------------------------------------------- /back/src/dals/session/repository/index.ts: -------------------------------------------------------------------------------- 1 | import { envConstants } from 'core'; 2 | import * as mockRepository from './session.mock'; 3 | import * as repository from './session.repository'; 4 | import { SessionRepositoryContract } from './session.contract'; 5 | 6 | export const sessionRepository: SessionRepositoryContract = envConstants.isMockRepository 7 | ? mockRepository 8 | : repository; 9 | -------------------------------------------------------------------------------- /front/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { ThemeProviderComponent } from 'core/theme'; 4 | import { App } from './app'; 5 | 6 | const root = createRoot(document.getElementById('root')); 7 | root.render( 8 | <> 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /back/src/core/servers/sockets.server.ts: -------------------------------------------------------------------------------- 1 | import SocketIOClient, { Socket } from 'socket.io'; 2 | import { messageSocketEvents } from 'pods/messages'; 3 | 4 | export const createSocketServer = (app) => { 5 | const io: SocketIOClient.Socket = require('socket.io')(app); 6 | 7 | io.on('connection', async (socket: Socket) => { 8 | await messageSocketEvents(socket, io); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /back/README.md: -------------------------------------------------------------------------------- 1 | # Scaffolding Express Typescript 2 | 3 | ## Steps 4 | 5 | - Run install 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | - Create `.env` file with same `.env.example` value. 12 | 13 | - Run development start: 14 | 15 | ```bash 16 | npm start 17 | ``` 18 | 19 | - Run debug start: 20 | 21 | ```bash 22 | npm run start:debug 23 | ``` 24 | 25 | - And run .vscode/launch.json 26 | -------------------------------------------------------------------------------- /front/src/layout/app-layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppbarComponent, FooterComponent } from 'common-app'; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | export const AppLayout: React.FC = (props) => { 9 | return ( 10 | <> 11 | 12 | {props.children} 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /back/src/dals/session/repository/session.contract.ts: -------------------------------------------------------------------------------- 1 | import { UserSession } from 'dals'; 2 | 3 | export interface SessionRepositoryContract { 4 | isTrainerUser: (connectionId: string) => Promise; 5 | isExistingConnection: (connectionId: string) => Promise; 6 | addNewUser: (userSession: UserSession) => Promise; 7 | getRoomFromConnectionId: (connectionId: string) => Promise; 8 | } -------------------------------------------------------------------------------- /front/config/test/jest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '../../', 3 | preset: 'ts-jest', 4 | restoreMocks: true, 5 | testEnvironment: 'jsdom', 6 | moduleDirectories: ['/src', 'node_modules'], 7 | setupFilesAfterEnv: ['/config/test/setup.ts'], 8 | modulePathIgnorePatterns: ['cypress'], 9 | moduleNameMapper: { 10 | '^.+\\.png$': '/pngTransform.js', 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /front/src/layout/session-layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppbarComponent, FooterComponent } from 'common-app'; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | export const SessionLayout: React.FC = (props) => { 9 | return ( 10 | <> 11 | 12 | {props.children} 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /back/src/pods/messages/processors/response.ts: -------------------------------------------------------------------------------- 1 | export const responseType = { 2 | CONNECTION_ACK: 'CONNECTION_ACK', 3 | APPEND_TEXT: 'APPEND_TEXT', 4 | STUDENT_GET_FULL_CONTENT: 'STUDENT_GET_FULL_CONTENT', 5 | TRAINER_GET_FULL_CONTENT: 'TRAINER_GET_FULL_CONTENT', 6 | UPDATE_FULL_CONTENT: 'UPDATE_FULL_CONTENT' 7 | }; 8 | 9 | export interface ResponseBase { 10 | type: string; 11 | payload?: any; 12 | } 13 | -------------------------------------------------------------------------------- /front/src/scenes/trainer.scene.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SessionLayout } from 'layout'; 3 | import { TrainerContainer } from 'pods/trainer'; 4 | 5 | export const TrainerScene: React.FC = () => { 6 | React.useEffect(() => { 7 | document.title = `Trainer - Code Paster`; 8 | }, []); 9 | 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /front/src/common-app/components/appbar/appbar.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CodePasterLogo from 'assets/logo.svg'; 3 | import * as classes from './appbar.styles'; 4 | 5 | export const AppbarComponent: React.FC = () => { 6 | const { appbarContainer, logo } = classes; 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /front/src/common/hooks/use-with-ref.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useWithRef = function(initialValue: T) { 4 | const [state, setInternalState] = React.useState(initialValue); 5 | const ref = React.useRef(initialValue); 6 | 7 | const setState = (value: T) => { 8 | ref.current = value; 9 | setInternalState(value); 10 | }; 11 | 12 | return [state, setState, ref] as const; 13 | }; 14 | -------------------------------------------------------------------------------- /front/src/pods/create-session/api/create-session.api.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import { baseApiUrl } from 'core'; 3 | 4 | const getRoomUrl = `${baseApiUrl}/api/create-room`; 5 | 6 | interface SessionInfo { 7 | room: string; 8 | trainerToken: string; 9 | } 10 | 11 | export const createRoom = async (): Promise => { 12 | // TODO Error handling 13 | const result = await Axios.get(getRoomUrl); 14 | 15 | return result.data; 16 | }; 17 | -------------------------------------------------------------------------------- /front/src/scenes/create-session.scene.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppLayout } from 'layout'; 3 | import { CreateSessionContainer } from 'pods/create-session'; 4 | 5 | export const CreateSessionScene: React.FC = () => { 6 | React.useEffect(() => { 7 | document.title = `Create New Session - Code Paster`; 8 | }, []); 9 | 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /back/src/pods/messages/messages.model.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io'; 2 | 3 | export type MessageType = 'message' | 'error'; 4 | 5 | export interface SocketInfo { 6 | socket: Socket; 7 | io: Socket; 8 | connectionId: string; 9 | } 10 | 11 | export interface Action { 12 | type: string; 13 | payload?: any; 14 | messageType?: MessageType; 15 | } 16 | 17 | export interface InputEstablishConnectionTrainer { 18 | room: string; 19 | trainertoken: string; 20 | } 21 | -------------------------------------------------------------------------------- /back/src/core/constants/env.constants.ts: -------------------------------------------------------------------------------- 1 | export const envConstants = { 2 | NODE_ENV: process.env.NODE_ENV, 3 | PORT: process.env.INTERNAL_PORT, 4 | isMockRepository: process.env.MOCK_REPOSITORY === 'true', 5 | MONGODB_URI: process.env.MONGODB_URI, 6 | API_URL: process.env.API_URL, 7 | CORS_ORIGIN: process.env.CORS_ORIGIN, 8 | isProduction: process.env.NODE_ENV === 'production', 9 | STATIC_FILES_PATH: process.env.STATIC_FILES_PATH, 10 | MONGO_EXPIRATION_TIME: process.env.MONGO_EXPIRATION_TIME, 11 | }; 12 | -------------------------------------------------------------------------------- /front/src/scenes/student.scene.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { SessionLayout } from 'layout'; 3 | import { StudentContainer } from 'pods/student'; 4 | import { useParams } from 'react-router-dom'; 5 | 6 | interface Params { 7 | room: string; 8 | } 9 | 10 | export const StudentScene = () => { 11 | React.useEffect(() => { 12 | document.title = `Student - Code Paster`; 13 | }, []); 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /front/src/common/hooks/auto-scroll.hook.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useAutoScroll = () => { 4 | const [isAutoScrollEnabled, setIsAutoScrollEnabled] = React.useState(true); 5 | const textAreaRef = React.useRef(null); 6 | 7 | const doAutoScroll = () => { 8 | if(isAutoScrollEnabled && textAreaRef.current) { 9 | textAreaRef.current.scrollTop = textAreaRef.current.scrollHeight; 10 | } 11 | } 12 | 13 | return {isAutoScrollEnabled, setIsAutoScrollEnabled, textAreaRef, doAutoScroll} 14 | } 15 | -------------------------------------------------------------------------------- /back/src/dals/room/room.context.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from 'mongoose'; 2 | import { RoomInfo } from 'dals'; 3 | import { envConstants } from 'core'; 4 | 5 | const roomSchema = new Schema({ 6 | room: Schema.Types.String, 7 | content: Schema.Types.String, 8 | expireAt: { 9 | type: Schema.Types.Date, 10 | default: Date.now, 11 | index: { 12 | expires: envConstants.MONGO_EXPIRATION_TIME, 13 | }, 14 | }, 15 | }); 16 | 17 | export const RoomContext = model('RoomInfo', roomSchema); 18 | -------------------------------------------------------------------------------- /front/src/assets/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#fbf867", 17 | "background_color": "#fbf867", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /front/src/core/theme/theme-provider.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | StyledEngineProvider, 4 | ThemeProvider, 5 | CssBaseline, 6 | } from '@mui/material'; 7 | import { theme } from './theme'; 8 | 9 | export const ThemeProviderComponent = (props) => { 10 | const { children } = props; 11 | 12 | return ( 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | upstream app { 2 | server 127.0.0.1:INTERNAL_PORT; 3 | } 4 | 5 | server { 6 | listen PORT default_server; 7 | 8 | server_name _; 9 | 10 | if ($http_x_forwarded_proto != "https") { 11 | return 301 https://$host$request_uri; 12 | } 13 | 14 | proxy_http_version 1.1; 15 | proxy_set_header Upgrade $http_upgrade; 16 | proxy_set_header Connection 'upgrade'; 17 | proxy_set_header Host $host; 18 | proxy_cache_bypass $http_upgrade; 19 | 20 | location / { 21 | client_max_body_size 10m; 22 | proxy_pass http://app; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "noLib": false, 10 | "allowJs": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "baseUrl": "./src", 15 | "paths": { 16 | "core": ["core"], 17 | "dals": ["dals"], 18 | "pods": ["pods"] 19 | } 20 | }, 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /back/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Program", 8 | "program": "${workspaceFolder}/dist/index.js", 9 | "preLaunchTask": "build", 10 | "runtimeExecutable": null, 11 | "runtimeArgs": ["--nolazy"], 12 | "env": { 13 | "NODE_ENV": "development", 14 | "MOCK_REPOSITORY": "true" 15 | }, 16 | "console": "internalConsole", 17 | "sourceMaps": true, 18 | "outFiles": ["${workspaceRoot}/**/*.js"] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /back/src/pods/room/room.api.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { generateNewRoomName, generateNewTrainerToken } from './room-name-generator.business'; 3 | 4 | export const api = Router(); 5 | 6 | api.get('/create-room', async (req, res) => { 7 | const roomName = generateNewRoomName(); 8 | const trainerToken = generateNewTrainerToken(); 9 | res.send({ room: roomName, trainerToken: trainerToken }); 10 | }); 11 | 12 | api.post('/enroll-room', async (req, res) => { 13 | // TODO: ensure parameter is informed 14 | const roomName = req.body.name; 15 | 16 | res.send({ error: roomName }); 17 | }); 18 | -------------------------------------------------------------------------------- /front/src/core/theme/theme.vm.ts: -------------------------------------------------------------------------------- 1 | import { Theme as DefaultTheme } from '@mui/material/styles'; 2 | import { Palette as DefaultPalette } from '@mui/material/styles/createPalette'; 3 | 4 | interface Palette extends DefaultPalette { 5 | customPalette: { 6 | background: string; 7 | primary: string; 8 | secondary: string; 9 | successLight: string; 10 | successDark: string; 11 | alertLight: string; 12 | alertDark: string; 13 | greyLight: string; 14 | greyMedium: string; 15 | }; 16 | } 17 | 18 | export interface Theme extends Omit { 19 | palette: Palette; 20 | } 21 | -------------------------------------------------------------------------------- /front/src/core/use-log.hook.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useLog = () => { 4 | const [log, internalSetLog] = React.useState(''); 5 | const logRef = React.useRef(''); 6 | 7 | const appendToLog = (value: string) => { 8 | const newText = logRef.current 9 | ? `${logRef.current}\n${value} ` 10 | : `${value} `; 11 | setLog(newText); 12 | }; 13 | 14 | const setLog = (content: string) => { 15 | if (content !== undefined && content !== null) { 16 | internalSetLog(content); 17 | logRef.current = content; 18 | } 19 | }; 20 | 21 | return { log, appendToLog, logRef, setLog }; 22 | }; 23 | -------------------------------------------------------------------------------- /front/src/core/api.ts: -------------------------------------------------------------------------------- 1 | import * as ioClient from 'socket.io-client'; 2 | import { Socket } from 'socket.io'; 3 | import { baseSocketUrl } from './const'; 4 | 5 | export interface ConnectionSetup { 6 | room: string; 7 | trainertoken: string; 8 | } 9 | 10 | export const createSocket = (connectionSetup: ConnectionSetup): Socket => { 11 | const { room, trainertoken } = connectionSetup; 12 | const socketParams = { 13 | url: baseSocketUrl, 14 | options: { 15 | query: { room, trainertoken }, 16 | timeout: 60000, 17 | }, 18 | }; 19 | 20 | // TODO Add channel (room) 21 | return ioClient(socketParams.url, socketParams.options); 22 | }; 23 | -------------------------------------------------------------------------------- /front/src/core/router/router.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { switchRoutes } from './routes'; 4 | import { CreateSessionScene, TrainerScene, StudentScene } from 'scenes'; 5 | 6 | export const RouterComponent: React.FunctionComponent = () => { 7 | return ( 8 | 9 | 10 | } /> 11 | } /> 12 | } /> 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /back/src/dals/session/session.context.ts: -------------------------------------------------------------------------------- 1 | import { Document, model, Schema } from 'mongoose'; 2 | import { UserSession } from 'dals/session'; 3 | import { envConstants } from 'core/constants'; 4 | 5 | const userSessionSchema = new Schema({ 6 | room: Schema.Types.String, 7 | trainerToken: Schema.Types.String, 8 | isTrainer: Schema.Types.Boolean, 9 | connectionId: Schema.Types.String, 10 | expireAt: { 11 | type: Schema.Types.Date, 12 | default: Date.now, 13 | index: { 14 | expires: envConstants.MONGO_EXPIRATION_TIME, 15 | }, 16 | }, 17 | }); 18 | 19 | export const UserSessionContext = model( 20 | 'UserSession', 21 | userSessionSchema 22 | ); 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI workflow 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v2 11 | - name: Install front 12 | run: | 13 | cd front 14 | npm install 15 | - name: Install back 16 | run: | 17 | cd back 18 | npm install 19 | - name: Build 20 | run: | 21 | cd front 22 | npm run build 23 | - name: Tests front 24 | run: | 25 | cd front 26 | npm test 27 | - name: Tests back 28 | run: | 29 | cd back 30 | npm test 31 | -------------------------------------------------------------------------------- /front/src/core/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { generatePath } from 'react-router-dom'; 2 | 3 | interface SwitchRoutes { 4 | root: string; 5 | student: string; 6 | trainer: string; 7 | } 8 | 9 | export const switchRoutes: SwitchRoutes = { 10 | root: '/', 11 | student: '/student/:room', 12 | trainer: '/trainer/:room/:token', 13 | }; 14 | 15 | interface Routes extends Omit { 16 | student: (room: string) => string; 17 | trainer: (room: string, token: string) => string; 18 | } 19 | 20 | export const routes: Routes = { 21 | ...switchRoutes, 22 | student: room => generatePath(switchRoutes.student, { room }), 23 | trainer: (room, token) => generatePath(switchRoutes.trainer, { room, token }), 24 | }; 25 | -------------------------------------------------------------------------------- /back/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | [ 6 | { 7 | "env": { 8 | "development": { 9 | "sourceMaps": true, 10 | "retainLines": true 11 | } 12 | } 13 | } 14 | ] 15 | ], 16 | "plugins": ["@babel/plugin-proposal-optional-chaining", 17 | [ 18 | "module-resolver", 19 | { 20 | "root": ["."], 21 | "alias": { 22 | "core": "./src/core", 23 | "dals": "./src/dals", 24 | "pods": "./src/pods", 25 | } 26 | } 27 | ] 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "jsx": "react", 10 | "noLib": false, 11 | "allowJs": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "baseUrl": "./src/", 16 | "paths": { 17 | "layout": ["layout"], 18 | "assets": ["assets"], 19 | "core": ["core"], 20 | "scenes": ["scenes"], 21 | "pods": ["pods"], 22 | "common": ["common"], 23 | "common-app": ["common-app"] 24 | } 25 | }, 26 | "include": ["./src/**/*", "./config/test/setup.ts", "./global.types.d.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /front/src/pods/create-session/create-session.container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { routes } from 'core/router'; 4 | import { CreateSessionComponent } from './create-session.component'; 5 | import { createRoom } from './api/create-session.api'; 6 | 7 | export const CreateSessionContainer: React.FunctionComponent = () => { 8 | const navigate = useNavigate(); 9 | 10 | const handleCreateSession = async (): Promise => { 11 | const response = await createRoom(); 12 | const trainerUrl = routes.trainer(response.room, response.trainerToken); 13 | navigate(trainerUrl); 14 | }; 15 | 16 | return ( 17 | <> 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /front/src/core/const.ts: -------------------------------------------------------------------------------- 1 | export const baseApiUrl = process.env.BASE_API_URL; 2 | export const baseSocketUrl = process.env.BASE_SOCKET_URL; 3 | 4 | export const SocketOuputMessageLiteral = { 5 | MESSAGE: 'message', 6 | }; 7 | 8 | export const SocketEmitMessageTypes = { 9 | TRAINER_APPEND_TEXT: 'TRAINER_APPEND_TEXT', // Trainer add new text 10 | TRAINER_SET_FULL_TEXT: 'TRAINER_SET_FULL_TEXT', // Trainer send full session text 11 | }; 12 | 13 | export const SocketReceiveMessageTypes = { 14 | CONNECTION_ACK: 'CONNECTION_ACK', 15 | APPEND_TEXT: 'APPEND_TEXT', 16 | TRAINER_GET_FULL_CONTENT: 'TRAINER_GET_FULL_CONTENT', 17 | STUDENT_GET_FULL_CONTENT: 'STUDENT_GET_FULL_CONTENT', 18 | UPDATE_FULL_CONTENT: 'UPDATE_FULL_CONTENT', 19 | }; 20 | 21 | export const lineSeparator = '\n\n*********************************\n'; 22 | -------------------------------------------------------------------------------- /back/src/pods/messages/messages.consts.ts: -------------------------------------------------------------------------------- 1 | export const InputMessageTypes = { 2 | ESTABLISH_CONNECTION_TRAINER: 'ESTABLISH_CONNECTION_TRAINER', 3 | ESTABLISH_CONNECTION_STUDENT: 'ESTABLISH_CONNECTION_STUDENT', 4 | TRAINER_APPEND_TEXT: 'TRAINER_APPEND_TEXT', 5 | TRAINER_SET_FULL_TEXT: 'TRAINER_SET_FULL_TEXT', 6 | TRAINER_SAVE_CONTENT: 'TRAINER_SAVE_CONTENT', 7 | }; 8 | 9 | export const OutputMessageTypes = { 10 | CONNECTION_ESTABLISHED_TRAINER: 'CONNECTION_ESTABLISHED_TRAINER', 11 | CONNECTION_ESTABLISHED_STUDENT: 'CONNECTION_ESTABLISHED_STUDENT', 12 | ERROR_MULTI_TRAINER_NOT_IMPLEMENTED_YET: 'ERROR_MULTI_TRAINER_NOT_IMPLEMENTED_YET', 13 | APPEND_TEXT: 'APPEND_TEXT', 14 | UPDATE_FULL_CONTENT: 'UPDATE_FULL_CONTENT', 15 | }; 16 | 17 | export const SocketMessageTypes = {}; 18 | 19 | export const ErrorCodes = {}; 20 | 21 | export const SocketOuputMessageLiteral = { 22 | MESSAGE: 'message', 23 | }; 24 | -------------------------------------------------------------------------------- /front/src/core/theme/theme.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import { createTheme } from '@mui/material/styles'; 3 | import { Theme } from './theme.vm'; 4 | 5 | const defaultTheme = createTheme(); 6 | 7 | export const theme: Theme = merge(defaultTheme, { 8 | palette: { 9 | background: { 10 | default: '#fff', 11 | }, 12 | customPalette: { 13 | background: '#fff', 14 | primary: '#d9d900', 15 | secondary: '#2E2800', 16 | successLight: '#11ae64', 17 | successDark: '#0f834c', 18 | alertLight: 'rgb(255, 87, 51)', 19 | alertDark: 'rgb(207, 70, 41)', 20 | greyLight: '#eee', 21 | greyMedium: '#ccc', 22 | }, 23 | }, 24 | breakpoints: { 25 | values: { 26 | xs: 380, 27 | sm: 578, 28 | md: 728, 29 | lg: 1100, 30 | }, 31 | }, 32 | spacing: (factor: number) => `${0.5 * factor}rem`, // 1 unit = 8px / 0.5rem 33 | }); 34 | -------------------------------------------------------------------------------- /back/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | 4 | import { createApp, createSocketServer } from 'core/servers'; 5 | import { connectToDB } from 'core/db'; 6 | import { envConstants } from 'core/constants'; 7 | import { api } from 'pods/room'; 8 | 9 | const app = createApp(); 10 | app.use(envConstants.API_URL, api); 11 | 12 | const staticFilesPath = path.resolve(__dirname, envConstants.STATIC_FILES_PATH); 13 | app.use('/', express.static(staticFilesPath)); 14 | 15 | const appServer = app.listen(envConstants.PORT, async () => { 16 | if(!envConstants.isMockRepository && envConstants.MONGODB_URI) { 17 | await connectToDB(envConstants.MONGODB_URI); 18 | } 19 | console.log(`Using ${envConstants.isMockRepository ? 'Mock' : 'DataBase'} for session storage`) 20 | console.log(`Server ready at http://localhost:${envConstants.PORT}${envConstants.API_URL}`); 21 | }); 22 | 23 | createSocketServer(appServer); 24 | 25 | -------------------------------------------------------------------------------- /back/src/pods/room/room-name-generator.business.ts: -------------------------------------------------------------------------------- 1 | import { baseNames } from './baseNames'; 2 | import { baseTrainerTokens } from './baseTrainerTokens'; 3 | 4 | const generateRandomNumberRange = (min: number, max: number) => { 5 | return Math.floor(Math.random() * max) + min; 6 | }; 7 | 8 | // TODO Unit test this 9 | 10 | const chooseRandomStringFromArray = (valueCollection: string[]) => { 11 | const maxNumber = valueCollection.length - 1; 12 | const randomNumber = generateRandomNumberRange(0, maxNumber); 13 | 14 | return valueCollection[randomNumber]; 15 | }; 16 | 17 | const generateRandomRoomSuffix = (): string => { 18 | const randomNumber = generateRandomNumberRange(0, 99999); 19 | 20 | return randomNumber.toString(); 21 | }; 22 | 23 | export const generateNewRoomName = () => 24 | `${chooseRandomStringFromArray(baseNames)}-${generateRandomRoomSuffix()}`; 25 | 26 | export const generateNewTrainerToken = () => 27 | `${chooseRandomStringFromArray( 28 | baseTrainerTokens 29 | )}-${generateRandomRoomSuffix()}`; 30 | -------------------------------------------------------------------------------- /back/src/pods/room/room-name-generator.business.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateNewRoomName, 3 | generateNewTrainerToken, 4 | } from './room-name-generator.business'; 5 | 6 | describe('Room name generator business spec', () => { 7 | it('Newly generated room name generator should contain a name followed by a number of 4 digits separated by a - ', () => { 8 | const newRoomName = generateNewRoomName(); 9 | const regexForMatchRoomName = /^[a-z]*-[0-9]{1,5}$/gi; 10 | const resultMatchRoomName = regexForMatchRoomName.test(newRoomName); 11 | expect(resultMatchRoomName).toBe(true); 12 | }); 13 | it('Newly generated trainer token generator should contain a name followed by a number of 4 digits separated by a - ', () => { 14 | const newTrainerToken = generateNewTrainerToken(); 15 | const regexForMatchTrainerToken = /^[a-z]*-[0-9]{1,5}$/gi; 16 | const resultMatchTrainerToken = regexForMatchTrainerToken.test( 17 | newTrainerToken 18 | ); 19 | expect(resultMatchTrainerToken).toBe(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /back/src/core/servers/cors.ts: -------------------------------------------------------------------------------- 1 | import { envConstants } from 'core/constants'; 2 | import cors from 'cors'; 3 | 4 | const options: cors.CorsOptions = { 5 | allowedHeaders: [ 6 | 'Origin', 7 | 'X-Requested-With', 8 | 'Content-Type', 9 | 'Accept', 10 | 'X-Access-Token', 11 | ], 12 | credentials: true, 13 | methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE', 14 | // IMPORTANT LIMIT HERE YOUR CLIENT APPS DOMAINS 15 | origin: (origin, callback) => { 16 | const allowedOrigins = [ 17 | 'http://localhost:8080', 18 | 'http://localhost:8081', 19 | 'https://codepaster.net', 20 | 'https://www.codepaster.net', 21 | ]; 22 | // Permitir peticiones sin origin (como Postman o curl) 23 | if (!origin) return callback(null, true); 24 | if (allowedOrigins.includes(origin)) { 25 | return callback(null, true); 26 | } else { 27 | return callback(new Error('Not allowed by CORS')); 28 | } 29 | }, 30 | preflightContinue: false, 31 | }; 32 | 33 | export const corsOptions = cors(options); -------------------------------------------------------------------------------- /front/src/pods/trainer/trainer.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { breakpoints, palette } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const root = css` 8 | display: grid; 9 | grid-template-columns: 1fr; 10 | grid-row-gap: 1rem; 11 | grid-column-gap: 1rem; 12 | padding-top: 1rem; 13 | padding-bottom: 1rem; 14 | 15 | & > :nth-child(n) { 16 | padding: 1rem; 17 | } 18 | 19 | @media (min-width: ${breakpoints.values.md}px) { 20 | grid-template-columns: 1fr 6fr 1fr; 21 | & > :nth-child(n) { 22 | grid-column-start: 2; 23 | grid-column-end: 3; 24 | } 25 | } 26 | 27 | @media (min-width: ${breakpoints.values.lg}px) { 28 | grid-template-columns: 1fr 4fr 1fr; 29 | } 30 | `; 31 | 32 | export const divider = css` 33 | && { 34 | padding: 0; 35 | @media (min-width: ${breakpoints.values.md}px) { 36 | grid-column-start: 1; 37 | grid-column-end: 4; 38 | } 39 | border-top: 1px solid ${color.greyMedium}; 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /front/src/assets/github-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /back/src/dals/session/repository/session.repository.ts: -------------------------------------------------------------------------------- 1 | import { UserSession, UserSessionContext } from 'dals' 2 | 3 | export const addNewUser = async ( newUser: UserSession): Promise => { 4 | await UserSessionContext.create(newUser); 5 | }; 6 | 7 | export const getRoomFromConnectionId = async (connectionId: string): Promise => { 8 | const session: UserSession = await getSessionFromConnectionId(connectionId); 9 | return session ? session.room : ''; 10 | }; 11 | 12 | export const isTrainerUser = async (connectionId: string): Promise => { 13 | const session: UserSession = await getSessionFromConnectionId(connectionId); 14 | return session ? session.isTrainer : false; 15 | }; 16 | 17 | export const isExistingConnection = async (connectionId: string): Promise => { 18 | const session: UserSession = await getSessionFromConnectionId(connectionId); 19 | return Boolean(session); 20 | } 21 | 22 | const getSessionFromConnectionId = async (connectionId: string): Promise => { 23 | return (await UserSessionContext.findOne({connectionId: connectionId}).lean()); 24 | }; -------------------------------------------------------------------------------- /front/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lemoncode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /back/src/dals/session/repository/session.mock.ts: -------------------------------------------------------------------------------- 1 | import { UserSession } from 'dals' 2 | 3 | let userCollectionSession: UserSession[] = []; 4 | 5 | export const addNewUser = async (newUser: UserSession): Promise => { 6 | const {connectionId, room, trainerToken, isTrainer } = newUser; 7 | userCollectionSession = [ 8 | ...userCollectionSession, 9 | { 10 | connectionId, 11 | room, 12 | trainerToken, 13 | isTrainer, 14 | }, 15 | ]; 16 | }; 17 | 18 | export const isTrainerUser = async (connectionId: string): Promise => { 19 | const session = userCollectionSession.find( 20 | (session) => session.connectionId === connectionId && session.isTrainer 21 | ); 22 | return session ? true : false; 23 | }; 24 | 25 | export const isExistingConnection = async (connectionId: string): Promise => 26 | userCollectionSession.findIndex( 27 | (session) => session.connectionId === connectionId 28 | ) !== -1; 29 | 30 | export const getRoomFromConnectionId = async (connectionId: string): Promise => { 31 | const session = userCollectionSession.find( 32 | (session) => session.connectionId === connectionId 33 | ); 34 | return session ? session.room : ''; 35 | }; -------------------------------------------------------------------------------- /front/config/webpack/dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const Dotenv = require('dotenv-webpack'); 3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | const base = require('./base'); 5 | 6 | module.exports = merge(base, { 7 | mode: 'development', 8 | devtool: 'eval-source-map', 9 | output: { 10 | filename: '[name].js', 11 | }, 12 | devServer: { 13 | host: 'localhost', 14 | port: 8080, 15 | hot: true, 16 | historyApiFallback: true, 17 | proxy: { 18 | '/api': 'http://localhost:3001', 19 | }, 20 | devMiddleware: { 21 | stats: 'errors-warnings', 22 | }, 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | exclude: /node_modules/, 29 | use: { 30 | loader: 'babel-loader', 31 | options: { 32 | plugins: [require.resolve('react-refresh/babel')], 33 | }, 34 | }, 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: ['style-loader', 'css-loader'], 39 | }, 40 | ], 41 | }, 42 | plugins: [ 43 | new ReactRefreshWebpackPlugin(), 44 | new Dotenv({ 45 | path: 'dev.env', 46 | }), 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /front/src/common-app/components/appbar/appbar.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { palette, spacing, breakpoints } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const appbarContainer = css` 8 | display: flex; 9 | align-items: center; 10 | width: 100%; 11 | height: ${spacing(10.75)}; 12 | background-image: linear-gradient( 13 | 60deg, 14 | white ${spacing(31.25)}, 15 | ${color.primary} ${spacing(31.25)}, 16 | white 90% 17 | ); 18 | border-bottom: 2px solid ${color.secondary}; 19 | @media (max-width: ${breakpoints.values.sm}px) { 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | height: auto; 24 | min-height: ${spacing(10.75)}; 25 | background-image: linear-gradient( 26 | 60deg, 27 | ${color.background}, 28 | ${color.background} 29 | ); 30 | } 31 | `; 32 | 33 | export const logo = css` 34 | height: ${spacing(7.5)}; 35 | margin-top: ${spacing(0.625)}; 36 | margin-left: ${spacing(4.8)}; 37 | fill: ${color.secondary}; 38 | @media (max-width: ${breakpoints.values.sm}px) { 39 | margin-top: spacing(3); 40 | margin-left: 0; 41 | } 42 | `; 43 | -------------------------------------------------------------------------------- /front/src/pods/create-session/create-session.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as classes from './create-session.styles'; 3 | // Material UI ~ components 4 | import Typography from '@mui/material/Typography'; 5 | import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'; 6 | 7 | interface Props { 8 | handleCreateSession: () => void; 9 | } 10 | 11 | export const CreateSessionComponent: React.FunctionComponent = props => { 12 | const { handleCreateSession } = props; 13 | const { 14 | mainContainer, 15 | buttonContainer, 16 | descriptionText, 17 | createSessionBtn, 18 | arrowIcon, 19 | } = classes; 20 | 21 | return ( 22 | <> 23 |
24 |
25 | 26 | The best tool for sharing code with your students! 27 | 28 | 35 |
36 |
37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /front/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code-Paster Tool - Lemoncode 7 | 8 | 13 | 19 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /front/src/pods/trainer/trainer.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, screen } from '@testing-library/react'; 3 | import { TrainerComponent } from './trainer.component'; 4 | 5 | describe('TrainerComponent tests', () => { 6 | it('should render all the expected sub components', () => { 7 | // Arrange 8 | const props = { 9 | handleAppendTrainerText: (trainerText: string) => {}, 10 | handleSendFullContentLog: (fullContent: string) => {}, 11 | currentTrainerUrl: 'http://current.trainer.url', 12 | currentStudentUrl: 'http://current.student.url', 13 | log: 'This is the log', 14 | }; 15 | 16 | // Act 17 | render(); 18 | 19 | const trainerLink = screen.getByRole('textbox', {name: 'Trainer Link'}); 20 | const sendButton = screen.getByRole('button', {name: 'Send'}); 21 | const sendFullContentButton = screen.getByRole('button', {name: 'Send Full Content'}); 22 | 23 | // Assert 24 | 25 | // rendered 26 | expect(trainerLink).toBeInTheDocument(); 27 | 28 | // rendered 29 | expect(sendButton).toBeInTheDocument(); 30 | 31 | // rendered 32 | expect(sendFullContentButton).toBeInTheDocument(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /front/config/webpack/prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const Dotenv = require('dotenv-webpack'); 3 | const base = require('./base'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | 6 | module.exports = merge(base, { 7 | mode: 'production', 8 | output: { 9 | filename: 'js/[name].[chunkhash].js', 10 | assetModuleFilename: 'images/[hash][ext][query]', 11 | }, 12 | optimization: { 13 | runtimeChunk: 'single', 14 | splitChunks: { 15 | cacheGroups: { 16 | vendor: { 17 | chunks: 'all', 18 | name: 'vendor', 19 | test: /[\\/]node_modules[\\/]/, 20 | enforce: true, 21 | }, 22 | }, 23 | }, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | exclude: /node_modules/, 30 | loader: 'babel-loader', 31 | }, 32 | { 33 | test: /\.css$/, 34 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new Dotenv({ 40 | path: 'prod.env', 41 | systemvars: true, 42 | }), 43 | new MiniCssExtractPlugin({ 44 | filename: './css/[chunkhash].[name].css', 45 | chunkFilename: '[chunkhash].[id].css', 46 | }), 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /.github/workflows/prod-cd.yml: -------------------------------------------------------------------------------- 1 | name: Production CD workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | env: 9 | IMAGE_NAME: ghcr.io/lemoncode/code-paster:prod-${{github.sha}}-${{github.run_attempt}} 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: "Production" 16 | url: https://codepaster.net/ 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Log in to GitHub container registry 22 | uses: docker/login-action@v3 23 | with: 24 | registry: ghcr.io 25 | username: ${{ github.actor }} 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Build and push docker image 29 | run: | 30 | docker build \ 31 | --build-arg BASE_API_URL=https://codepaster.net \ 32 | --build-arg BASE_SOCKET_URL=https://codepaster.net \ 33 | -t ${{env.IMAGE_NAME}} . 34 | docker push ${{env.IMAGE_NAME}} 35 | 36 | - name: Deploy to Azure 37 | uses: azure/webapps-deploy@v3 38 | with: 39 | app-name: ${{ secrets.PROD_AZURE_APP_NAME }} 40 | publish-profile: ${{ secrets.PROD_AZURE_PUBLISH_PROFILE }} 41 | images: ${{env.IMAGE_NAME}} 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS base 2 | RUN mkdir -p /usr/app 3 | WORKDIR /usr/app 4 | 5 | # Build front 6 | FROM base AS build-frontend 7 | ARG BASE_API_URL 8 | ENV BASE_API_URL=$BASE_API_URL 9 | ARG BASE_SOCKET_URL 10 | ENV BASE_SOCKET_URL=$BASE_SOCKET_URL 11 | COPY ./front ./ 12 | RUN npm ci 13 | RUN npm run build 14 | 15 | # Build backend 16 | FROM base AS build-backend 17 | COPY ./back ./ 18 | RUN npm ci 19 | RUN npm run build 20 | 21 | # Release 22 | FROM base AS release 23 | ENV NODE_ENV=production 24 | ENV STATIC_FILES_PATH=./public 25 | COPY --from=build-backend /usr/app/dist ./ 26 | COPY --from=build-frontend /usr/app/dist $STATIC_FILES_PATH 27 | COPY ./back/package.json ./ 28 | COPY ./back/package-lock.json ./ 29 | RUN npm ci --only=production 30 | 31 | FROM nasdan/azure-pm2-nginx:nodejs-20-nginx-1.24 32 | ENV NODE_ENV=production 33 | ENV STATIC_FILES_PATH=./public 34 | ENV MOCK_REPOSITORY=false 35 | ENV CORS_ORIGIN=false 36 | ENV API_URL=/api 37 | COPY --from=release /usr/app ./ 38 | 39 | COPY nginx.conf /etc/nginx/conf.d/default.conf 40 | 41 | ENV INTERNAL_PORT=3000 42 | RUN sed -i -e 's|INTERNAL_PORT|'"$INTERNAL_PORT"'|g' /etc/nginx/conf.d/default.conf 43 | 44 | CMD sh docker-entrypoint.sh && \ 45 | sed -i -e 's|PORT|80|g' /etc/nginx/conf.d/default.conf && \ 46 | pm2 start ./index.js --name "app" --env production && \ 47 | nginx -g 'daemon off;' 48 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/new-text.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { palette, typography, spacing } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const root = css` 8 | display: grid; 9 | grid-row-gap: 1rem; 10 | grid-column-gap: 1rem; 11 | `; 12 | 13 | export const label = css` 14 | font-family: ${typography.fontFamily}; 15 | font-size: 1.125rem; 16 | `; 17 | 18 | export const textarea = css` 19 | box-sizing: border-box; 20 | padding: ${spacing(2)}; 21 | font-family: ${typography.fontFamily}; 22 | font-size: 1rem; 23 | background-color: ${color.background}; 24 | border: 2px solid ${color.secondary}; 25 | white-space: nowrap; 26 | resize: none; 27 | &:focus { 28 | border: 2px solid ${color.secondary}; 29 | outline: none; 30 | } 31 | `; 32 | 33 | export const button = css` 34 | padding: ${spacing(1.25)} 0; 35 | font-size: 1.188rem; 36 | font-weight: 400; 37 | text-transform: capitalize; 38 | color: #fff; 39 | background-color: ${color.successLight}; 40 | border-radius: 0; 41 | transition: all 0.2s; 42 | -webkit-transition: all 0.2s; 43 | &:hover { 44 | color: #fff; 45 | background-color: ${color.successDark}; 46 | } 47 | `; 48 | 49 | export const buttonIcon = css` 50 | margin-left: ${spacing(1.25)}; 51 | font-size: 1.25rem; 52 | `; 53 | -------------------------------------------------------------------------------- /front/src/pods/trainer/trainer.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { HeaderComponent } from './components/header.component'; 3 | import { NewTextComponent } from './components/new-text.component'; 4 | import { SessionComponent } from './components/session.component'; 5 | import * as innerClasses from './trainer.styles'; 6 | 7 | interface Props { 8 | handleAppendTrainerText: (trainerText: string) => void; 9 | handleSendFullContentLog: (fullContent: string) => void; 10 | currentTrainerUrl: string; 11 | currentStudentUrl: string; 12 | log: string; 13 | } 14 | 15 | export const TrainerComponent: React.FC = props => { 16 | const { 17 | handleAppendTrainerText, 18 | handleSendFullContentLog, 19 | currentTrainerUrl, 20 | currentStudentUrl, 21 | log, 22 | } = props; 23 | 24 | return ( 25 | <> 26 |
27 | 31 |
32 | 33 |
34 | 38 |
39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /.github/workflows/dev-cd.yml: -------------------------------------------------------------------------------- 1 | name: Develop CD workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - develop 8 | env: 9 | IMAGE_NAME: ghcr.io/lemoncode/code-paster:dev-${{github.sha}}-${{github.run_attempt}} 10 | 11 | permissions: 12 | contents: 'read' 13 | packages: 'write' 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | environment: 19 | name: 'Development' 20 | url: https://dev-codepaster.azurewebsites.net 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Log in to GitHub container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Build and push docker image 33 | run: | 34 | docker build \ 35 | --build-arg BASE_API_URL=https://dev-codepaster.azurewebsites.net \ 36 | --build-arg BASE_SOCKET_URL=https://dev-codepaster.azurewebsites.net \ 37 | -t ${{env.IMAGE_NAME}} . 38 | docker push ${{env.IMAGE_NAME}} 39 | 40 | - name: Deploy to Azure 41 | uses: azure/webapps-deploy@v3 42 | with: 43 | app-name: ${{ secrets.DEV_AZURE_APP_NAME }} 44 | publish-profile: ${{ secrets.DEV_AZURE_PUBLISH_PROFILE }} 45 | images: ${{env.IMAGE_NAME}} 46 | -------------------------------------------------------------------------------- /front/src/pods/student/student.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { typography, spacing, palette, breakpoints } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const root = css` 8 | display: grid; 9 | grid-template-columns: auto; 10 | grid-row-gap: 1rem; 11 | grid-column-gap: 1rem; 12 | padding: 1rem; 13 | 14 | @media (min-width: ${breakpoints.values.md}px) { 15 | padding: 2rem; 16 | grid-template-columns: 1fr 6fr 1fr; 17 | & > :nth-child(n) { 18 | grid-column-start: 2; 19 | grid-column-end: 3; 20 | } 21 | } 22 | 23 | @media (min-width: ${breakpoints.values.lg}px) { 24 | grid-template-columns: 1fr 4fr 1fr; 25 | } 26 | `; 27 | 28 | export const sessionName = css` 29 | @media (min-width: ${breakpoints.values.md}px) { 30 | justify-self: start; 31 | } 32 | font-size: 1.125rem; 33 | text-align: center; 34 | border-bottom: 2px solid ${color.primary}; 35 | `; 36 | 37 | export const label = css` 38 | display: block; 39 | font-family: ${typography.fontFamily}; 40 | font-size: 1.125rem; 41 | `; 42 | 43 | export const textarea = css` 44 | width: 100%; 45 | box-sizing: border-box; 46 | padding: ${spacing(2)}; 47 | font-family: ${typography.fontFamily}; 48 | font-size: 1rem; 49 | white-space: pre-wrap; 50 | resize: none; 51 | background-color: ${color.background}; 52 | border: 2px solid ${color.secondary}; 53 | &:focus { 54 | outline: none; 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /front/src/pods/student/student.container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | createSocket, 4 | SocketOuputMessageLiteral, 5 | SocketReceiveMessageTypes, 6 | } from 'core'; 7 | import { useLog } from 'core'; 8 | import { StudentComponent } from './student.component'; 9 | import { useParams } from 'react-router-dom'; 10 | 11 | interface Params extends Record { 12 | room: string; 13 | } 14 | 15 | export const StudentContainer = () => { 16 | const { room } = useParams(); 17 | const { log, appendToLog, setLog } = useLog(); 18 | 19 | const handleConnection = () => { 20 | // Connect to socket 21 | const socket = createSocket({ 22 | room: room, 23 | trainertoken: '', 24 | }); 25 | 26 | socket.on(SocketOuputMessageLiteral.MESSAGE, (msg) => { 27 | if (msg.type) { 28 | const { type, payload } = msg; 29 | 30 | switch (type) { 31 | case SocketReceiveMessageTypes.APPEND_TEXT: 32 | appendToLog(payload); 33 | break; 34 | case SocketReceiveMessageTypes.STUDENT_GET_FULL_CONTENT: 35 | case SocketReceiveMessageTypes.UPDATE_FULL_CONTENT: 36 | setLog(payload); 37 | break; 38 | default: 39 | break; 40 | } 41 | } 42 | }); 43 | }; 44 | 45 | React.useEffect(() => { 46 | handleConnection(); 47 | }, []); 48 | 49 | return ( 50 | <> 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /front/src/pods/create-session/create-session.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { CreateSessionComponent } from './create-session.component'; 5 | 6 | describe('CreateSessionComponent unit tests', () => { 7 | it('should show the elements with the expected roles', () => { 8 | // Arrange 9 | 10 | const props = { 11 | handleCreateSession: jest.fn(), 12 | }; 13 | 14 | // Act 15 | 16 | render(); 17 | 18 | const main = screen.getByRole('main'); 19 | const button = screen.getByRole('button'); 20 | const description = screen.getByText( 21 | 'The best tool for sharing code with your students!' 22 | ); 23 | 24 | // Assert 25 | 26 | expect(main).toBeInTheDocument(); 27 | expect(button).toBeInTheDocument(); 28 | expect(button).toHaveTextContent('Create Session'); 29 | expect(description).toBeInTheDocument(); 30 | }); 31 | 32 | it('should create a new session when clicking the create session button', async () => { 33 | // Arrange 34 | 35 | const props = { 36 | handleCreateSession: jest.fn(), 37 | }; 38 | 39 | // Act 40 | 41 | render(); 42 | 43 | const createSessionButton = screen.getByRole('button'); 44 | await userEvent.click(createSessionButton); 45 | 46 | // Assert 47 | 48 | expect(props.handleCreateSession).toHaveBeenCalledTimes(1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /front/src/core/router/routes.spec.ts: -------------------------------------------------------------------------------- 1 | import { routes, switchRoutes } from './routes'; 2 | 3 | describe('routes spec', () => { 4 | it('"switchRoutes" should have expected value by default', () => { 5 | // Arrange 6 | interface SwitchRoutes { 7 | root: string; 8 | student: string; 9 | trainer: string; 10 | } 11 | 12 | const expectedValue: SwitchRoutes = { 13 | root: '/', 14 | student: '/student/:room', 15 | trainer: '/trainer/:room/:token', 16 | }; 17 | 18 | // Act 19 | 20 | // Assert 21 | expect(switchRoutes).toEqual(expectedValue); 22 | }); 23 | 24 | it('"routes.student" & "routes.trainer" should be functions by default', () => { 25 | // Arrange 26 | 27 | // Act 28 | 29 | // Assert 30 | expect(routes.student).toEqual(expect.any(Function)); 31 | expect(routes.trainer).toEqual(expect.any(Function)); 32 | }); 33 | 34 | it('"routes.student" should return switchRoutes.student path with room name when called', () => { 35 | // Arrange 36 | const expectedResult: string = '/student/new-room'; 37 | 38 | // Act 39 | const result: string = routes.student('new-room'); 40 | 41 | // Assert 42 | expect(result).toEqual(expectedResult); 43 | }); 44 | 45 | it('"routes.trainer" should return switchRoutes.trainer path with room name when called', () => { 46 | // Arrange 47 | const expectedResult: string = '/trainer/new-room/new-token'; 48 | 49 | // Act 50 | const result: string = routes.trainer('new-room', 'new-token'); 51 | 52 | // Assert 53 | expect(result).toEqual(expectedResult); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /back/src/dals/room/repository/room.repository.ts: -------------------------------------------------------------------------------- 1 | import { RoomInfo, RoomContext } from 'dals'; 2 | 3 | export const isRoomAvailable = async (room: string): Promise => { 4 | return (await RoomContext.countDocuments({ room: room })) === 0; 5 | }; 6 | 7 | export const saveRoomInfo = async ( 8 | newRoomInfo: RoomInfo, 9 | setFullText: boolean 10 | ): Promise => { 11 | (await isRoomAvailable(newRoomInfo.room)) 12 | ? await insertRoom(newRoomInfo) 13 | : await updateRoomContent(newRoomInfo, setFullText); 14 | }; 15 | 16 | export const getRoomContent = async (room: string): Promise => { 17 | const roomInfo: RoomInfo = await getRoomByName(room); 18 | return roomInfo ? roomInfo.content : ''; 19 | }; 20 | 21 | const getRoomByName = async (room: string): Promise => { 22 | return await RoomContext.findOne({ room: room }).lean(); 23 | }; 24 | 25 | const appendRoomContent = async (newRoomInfo: RoomInfo): Promise => { 26 | const room: RoomInfo = await getRoomByName(newRoomInfo.room); 27 | 28 | if (room) { 29 | const newContent = `${room.content}\n${newRoomInfo.content}`; 30 | await RoomContext.updateOne( 31 | { room: newRoomInfo.room }, 32 | { content: newContent } 33 | ); 34 | } 35 | }; 36 | 37 | const replaceRoomContent = async (newRoomInfo: RoomInfo): Promise => { 38 | await RoomContext.updateOne( 39 | { room: newRoomInfo.room }, 40 | { content: newRoomInfo.content } 41 | ); 42 | }; 43 | 44 | const updateRoomContent = async ( 45 | newRoomInfo: RoomInfo, 46 | setFullText: boolean 47 | ): Promise => 48 | setFullText 49 | ? replaceRoomContent(newRoomInfo) 50 | : appendRoomContent(newRoomInfo); 51 | 52 | const insertRoom = async (newRoomInfo: RoomInfo): Promise => { 53 | await RoomContext.create(newRoomInfo); 54 | }; 55 | -------------------------------------------------------------------------------- /front/config/webpack/base.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const {merge} = require('webpack-merge'); 3 | const helpers = require('./helpers'); 4 | const CopyPlugin = require('copy-webpack-plugin'); 5 | 6 | module.exports = merge( 7 | {}, 8 | { 9 | context: helpers.resolveFromRootPath('src'), 10 | resolve: { 11 | alias: { 12 | layout: helpers.resolveFromRootPath('src/layout'), 13 | assets: helpers.resolveFromRootPath('src/assets'), 14 | core: helpers.resolveFromRootPath('src/core'), 15 | scenes: helpers.resolveFromRootPath('src/scenes'), 16 | pods: helpers.resolveFromRootPath('src/pods'), 17 | common: helpers.resolveFromRootPath('src/common'), 18 | 'common-app': helpers.resolveFromRootPath('src/common-app'), 19 | }, 20 | extensions: ['.js', '.ts', '.tsx'], 21 | }, 22 | entry: { 23 | app: ['regenerator-runtime/runtime', './index.tsx'], 24 | }, 25 | output: { 26 | path: helpers.resolveFromRootPath('dist'), 27 | publicPath: '/', 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.svg$/, 33 | use: ['@svgr/webpack'], 34 | }, 35 | { 36 | test: /\.(png|jpg|gif)$/, 37 | exclude: /node_modules/, 38 | type: 'asset/resource', 39 | }, 40 | ], 41 | }, 42 | plugins: [ 43 | new HtmlWebpackPlugin({ 44 | favicon: 'assets/favicon/favicon.ico', 45 | template: 'app.html', 46 | filename: 'app.html', 47 | }), 48 | new CopyPlugin({ 49 | patterns: [ 50 | { from: '../static/index.html', to: 'index.html' }, 51 | { from: '../static/styles.css', to: 'styles.css' }, 52 | { from: '../static/assets', to: 'assets' }, 53 | ], 54 | }), 55 | ], 56 | } 57 | ); 58 | -------------------------------------------------------------------------------- /front/src/common-app/components/footer/footer.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LemoncodeLogo from 'assets/lemoncode-logo.svg'; 3 | import GithubIcon from 'assets/github-logo.svg'; 4 | import * as classes from './footer.styles'; 5 | import TwitterIcon from '@mui/icons-material/Twitter'; 6 | import EmailRoundedIcon from '@mui/icons-material/EmailRounded'; 7 | import LanguageIcon from '@mui/icons-material/Language'; 8 | 9 | export const FooterComponent: React.FC = () => { 10 | const { 11 | footerContainer, 12 | topContainerCenter, 13 | lemoncodeIcon, 14 | bottomContainer, 15 | iconContainer, 16 | iconListItem, 17 | icon, 18 | githubIcon, 19 | copyright, 20 | } = classes; 21 | return ( 22 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /front/src/pods/student/student.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextareaAutosize from '@mui/material/TextareaAutosize'; 3 | import Typography from '@mui/material/Typography'; 4 | 5 | import Checkbox from '@mui/material/Checkbox'; 6 | import FormControlLabel from '@mui/material/FormControlLabel'; 7 | 8 | import * as innerClasses from './student.styles'; 9 | import { useAutoScroll } from 'common/hooks/auto-scroll.hook'; 10 | 11 | interface Props { 12 | room: string; 13 | log: string; 14 | } 15 | 16 | export const StudentComponent: React.FC = props => { 17 | const { room, log } = props; 18 | 19 | const { 20 | isAutoScrollEnabled, 21 | setIsAutoScrollEnabled, 22 | textAreaRef, 23 | doAutoScroll, 24 | } = useAutoScroll(); 25 | 26 | React.useEffect(() => { 27 | doAutoScroll(); 28 | }, [log]); 29 | 30 | return ( 31 | <> 32 |
33 | 38 | Session name: {room ?? ''} 39 | 40 | 43 |
44 | 54 |
55 | setIsAutoScrollEnabled(e.target.checked)} 61 | color="primary" 62 | /> 63 | } 64 | /> 65 |
66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /back/src/pods/messages/messages.socket-events.ts: -------------------------------------------------------------------------------- 1 | import SocketIOClient, { Socket } from 'socket.io'; 2 | import { Action, SocketInfo } from './messages.model'; 3 | import { 4 | processInputMessage, 5 | processOutputMessageCollection, 6 | } from './processors'; 7 | import { InputMessageTypes } from './messages.consts'; 8 | 9 | export const messageSocketEvents = async ( 10 | socket: Socket, 11 | io: SocketIOClient.Socket 12 | ) => { 13 | // WATCH OUT !! Reconnect is not implemented 14 | // In the connection input processing, we should 15 | // check if connectionId matches with userId and RoomId 16 | // if not reject, if it's accepted send connection 17 | // reestablished 18 | const { room, trainertoken } = socket.handshake.query; 19 | console.log(`room request: ${room}`); 20 | console.log(`trainer token: ${trainertoken}`); 21 | console.log('*** Session ID:', socket.conn.id); 22 | 23 | let outputMessageCollection: Action[] = []; 24 | const socketInfo: SocketInfo = { 25 | socket: socket, 26 | io, 27 | connectionId: socket.conn.id, 28 | }; 29 | 30 | if (trainertoken) { 31 | outputMessageCollection = await processInputMessage(socketInfo, { 32 | type: InputMessageTypes.ESTABLISH_CONNECTION_TRAINER, 33 | payload: { 34 | room, 35 | trainertoken, 36 | }, 37 | }); 38 | } else { 39 | outputMessageCollection = await processInputMessage(socketInfo, { 40 | type: InputMessageTypes.ESTABLISH_CONNECTION_STUDENT, 41 | payload: { 42 | room, 43 | }, 44 | }); 45 | } 46 | 47 | await processOutputMessageCollection(socketInfo, outputMessageCollection); 48 | 49 | socket.on('message', async function (message: any) { 50 | console.log(`socket.on 'message': ${JSON.stringify(message)}`); 51 | if (message && message.type) { 52 | const outputMessageCollection: Action[] = await processInputMessage( 53 | socketInfo, 54 | message 55 | ); 56 | 57 | await processOutputMessageCollection(socketInfo, outputMessageCollection); 58 | } 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/header.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent unit tests', () => { 6 | it('should show the trainer and the students links when passing valid values', () => { 7 | // Arrange 8 | const props = { 9 | currentTrainerUrl: 'http://current.trainer.url', 10 | currentStudentUrl: 'http://current.student.url', 11 | }; 12 | 13 | // Act 14 | render(); 15 | const linkCopyInputs = screen.getAllByRole('textbox'); 16 | const linkCopyButtons = screen.getAllByRole('button'); 17 | 18 | // Assert 19 | 20 | expect(linkCopyInputs.length).toBe(2); 21 | expect(linkCopyButtons.length).toBe(2); 22 | linkCopyInputs.forEach(input => expect(input).toHaveAttribute('readonly')); 23 | expect(linkCopyInputs[0]).toHaveValue(props.currentTrainerUrl); 24 | expect(linkCopyInputs[1]).toHaveValue(props.currentStudentUrl); 25 | }); 26 | 27 | it('should show empty trainer and the students link when passing undefined values', () => { 28 | // Arrange 29 | const props = { 30 | currentTrainerUrl: undefined, 31 | currentStudentUrl: undefined, 32 | }; 33 | 34 | // Act 35 | render(); 36 | const linkCopyInputs = screen.getAllByRole('textbox'); 37 | 38 | // Assert 39 | 40 | expect(linkCopyInputs[0]).toBeEmpty(); 41 | expect(linkCopyInputs[1]).toBeEmpty(); 42 | }); 43 | 44 | it('should show empty trainer and the students link when passing null values', () => { 45 | // Arrange 46 | const props = { 47 | currentTrainerUrl: null, 48 | currentStudentUrl: null, 49 | }; 50 | 51 | // Act 52 | render(); 53 | const linkCopyInputs = screen.getAllByRole('textbox'); 54 | 55 | // Assert 56 | 57 | expect(linkCopyInputs[0]).toBeEmpty(); 58 | expect(linkCopyInputs[1]).toBeEmpty(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/new-text.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cx } from '@emotion/css'; 3 | import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded'; 4 | import Button from '@mui/material/Button'; 5 | import TextareaAutosize from '@mui/material/TextareaAutosize'; 6 | import * as innerClasses from './new-text.styles'; 7 | 8 | interface Props { 9 | handleAppendTrainerText: (trainerText: string) => void; 10 | className?: string; 11 | } 12 | 13 | export const NewTextComponent: React.FC = (props) => { 14 | const { handleAppendTrainerText, className } = props; 15 | const [trainerText, setTrainerText] = React.useState(''); 16 | 17 | const handleAppendTrainerTextInternal = (): void => { 18 | if (trainerText) { 19 | handleAppendTrainerText(trainerText); 20 | setTrainerText(''); 21 | } 22 | }; 23 | 24 | const handleOnChange = (e: React.ChangeEvent): void => { 25 | setTrainerText(e.target.value); 26 | }; 27 | 28 | return ( 29 |
30 | 33 | handleOnChange(e)} 38 | onKeyDown={(event) => { 39 | if (event.key === 'Enter' && event.ctrlKey) { 40 | handleAppendTrainerTextInternal(); 41 | } 42 | }} 43 | value={trainerText} 44 | /> 45 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /back/src/dals/room/repository/room.mock.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | import { RoomInfo, UserSession } from 'dals' 3 | 4 | let userCollectionSession: UserSession[] = []; 5 | let roomCollectionSession: RoomInfo[] = []; 6 | 7 | export const isRoomAvailable = async (room: string): Promise => 8 | !userCollectionSession.find((session) => session.room === room); 9 | 10 | export const saveRoomInfo = async (newRoomInfo: RoomInfo, setFullText: boolean): Promise => { 11 | const roomIndex: number = await getRoomIndexByName(newRoomInfo); 12 | const content: string = newRoomInfo.content; 13 | roomCollectionSession = 14 | roomIndex === -1 15 | ? await insertRoom(newRoomInfo) 16 | : await updateRoomContent(roomIndex, content, setFullText); 17 | }; 18 | 19 | export const getRoomContent = async (room: string): Promise => { 20 | const roomInfo = roomCollectionSession.find( 21 | (roomInfo: RoomInfo) => roomInfo.room === room 22 | ); 23 | 24 | return roomInfo ? roomInfo.content : ''; 25 | }; 26 | 27 | const getRoomIndexByName = async (newRoomInfo: RoomInfo): Promise => { 28 | return roomCollectionSession.findIndex( 29 | (roomInfo: RoomInfo) => roomInfo.room === newRoomInfo.room 30 | ); 31 | }; 32 | 33 | const updateRoomContent = async (index: number, content: string, setFullText:boolean): Promise => { 34 | if (!Boolean(content)) 35 | return roomCollectionSession; 36 | 37 | const newRoomCollectionSession = produce(roomCollectionSession, (draftState: RoomInfo[]) => { 38 | const someCurrentContent: boolean = Boolean(draftState[index].content); 39 | if (someCurrentContent && !setFullText) { 40 | draftState[index].content += '\n'; 41 | } 42 | draftState[index].content = setFullText ? content : (draftState[index].content + content); 43 | }); 44 | 45 | return newRoomCollectionSession; 46 | }; 47 | 48 | const insertRoom = async (newRoomInfo: RoomInfo): Promise => { 49 | return produce(roomCollectionSession, (draftState: RoomInfo[]) => { 50 | draftState.push(newRoomInfo); 51 | }); 52 | }; -------------------------------------------------------------------------------- /front/src/pods/trainer/components/new-text.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { NewTextComponent } from './new-text.component'; 5 | 6 | describe('NewTextComponent unit tests', () => { 7 | it('should handle the entered text when clicking on the send button', async () => { 8 | // Arrange 9 | const handleAppendTrainerText = jest.fn(); 10 | 11 | // Act 12 | render( 13 | 14 | ); 15 | 16 | const inputText = screen.getByRole('textbox'); 17 | const sendButton = screen.getByRole('button'); 18 | 19 | await userEvent.type(inputText, 'Test text'); 20 | await userEvent.click(sendButton); 21 | 22 | // Assert 23 | expect(handleAppendTrainerText).toHaveBeenCalledTimes(1); 24 | }); 25 | 26 | it('should handle the entered text when pressing Ctrl + enter', async () => { 27 | // Arrange 28 | const handleAppendTrainerText = jest.fn(); 29 | 30 | // Act 31 | render( 32 | 33 | ); 34 | 35 | const inputText = screen.getByRole('textbox'); 36 | 37 | await userEvent.type(inputText, 'Test text{Control>}{Enter}{/Control}'); 38 | 39 | // Assert 40 | expect(handleAppendTrainerText).toHaveBeenCalledTimes(1); 41 | }); 42 | 43 | it('should not handle any text when there is not entered text and clicking on the send button', async () => { 44 | // Arrange 45 | const handleAppendTrainerText = jest.fn(); 46 | 47 | // Act 48 | render( 49 | 50 | ); 51 | 52 | const sendButton = screen.getByRole('button'); 53 | 54 | await expect(() => userEvent.click(sendButton)).rejects.toThrow( 55 | /pointer-events: none/ 56 | ); 57 | }); 58 | 59 | it('should not handle any text when there is not entered text and pressing Ctrl + enter', async () => { 60 | // Arrange 61 | const handleAppendTrainerText = jest.fn(); 62 | 63 | // Act 64 | render( 65 | 66 | ); 67 | 68 | const inputText = screen.getByRole('textbox'); 69 | 70 | await userEvent.type(inputText, '{Control>}{Enter}{/Control}'); 71 | 72 | // Assert 73 | expect(handleAppendTrainerText).not.toHaveBeenCalled(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /back/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaffolding-express-typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "run-p -l type-check:watch start:dev start:local-db", 8 | "start:mock": "cross-env MOCK_REPOSITORY=true run-p -l type-check:watch start:dev", 9 | "start:dev": "nodemon --exec babel-node --extensions \".ts\" src/index.ts", 10 | "start:debug": "run-p -l type-check:watch \"start:dev -- --inspect-brk\"", 11 | "start:prod": "node ./dist/index.js", 12 | "start:local-db": "docker-compose up -d || echo \"Run docker-compose up manually\"", 13 | "stop:local-db": "docker-compose down || echo \"Run docker-compose down manually\"", 14 | "build": "run-p -l type-check build:prod", 15 | "build:prod": "npm run clean && babel src -d dist --ignore=\"./src/test-runners\" --extensions \".ts\"", 16 | "build:dev": "npm run clean && babel src -d dist --ignore=\"./src/test-runners\" --extensions \".ts\" --source-maps", 17 | "type-check": "tsc --noEmit", 18 | "type-check:watch": "npm run type-check -- --watch", 19 | "test": "cross-env NODE_ENV=test jest -c ./config/test/jest.js --verbose", 20 | "test:watch": "npm run test -- --watchAll -i --no-cache", 21 | "clean": "rimraf dist" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Lemoncode/scaffolding-express-typescript.git" 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/Lemoncode/scaffolding-express-typescript/issues" 32 | }, 33 | "homepage": "https://github.com/Lemoncode/scaffolding-express-typescript#readme", 34 | "devDependencies": { 35 | "@babel/cli": "^7.20.7", 36 | "@babel/core": "^7.20.12", 37 | "@babel/node": "^7.20.7", 38 | "@babel/plugin-proposal-optional-chaining": "^7.20.7", 39 | "@babel/preset-env": "^7.20.2", 40 | "@babel/preset-typescript": "^7.18.6", 41 | "@types/cors": "^2.8.13", 42 | "@types/express": "^4.17.17", 43 | "@types/jest": "^29.4.0", 44 | "@types/socket.io": "^2.1.10", 45 | "babel-plugin-module-resolver": "^5.0.0", 46 | "cross-env": "^7.0.3", 47 | "jest": "^29.4.2", 48 | "nodemon": "^2.0.20", 49 | "npm-run-all": "^4.1.5", 50 | "rimraf": "^4.1.2", 51 | "ts-jest": "^29.0.5", 52 | "typescript": "^4.9.5" 53 | }, 54 | "dependencies": { 55 | "cors": "^2.8.5", 56 | "dotenv": "^16.0.3", 57 | "express": "^4.18.2", 58 | "immer": "^9.0.19", 59 | "mongoose": "^6.9.1", 60 | "regenerator-runtime": "^0.13.11", 61 | "socket.io": "^2.5.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /front/src/pods/student/student.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { StudentComponent } from './student.component'; 4 | 5 | describe('StudentComponent tests', () => { 6 | it('It should show the session name and the text when passing valid parameters', () => { 7 | // Arrange 8 | const props = { 9 | room: 'Room I', 10 | log: 'This is test', 11 | }; 12 | 13 | const expectedSessionNameText = `Session name: ${props.room}`; 14 | 15 | // Act 16 | render(); 17 | 18 | // Assert 19 | const sessionName = screen.getByRole('heading'); 20 | const textArea = screen.getByTestId('session'); 21 | 22 | expect(sessionName).toHaveTextContent(expectedSessionNameText); 23 | expect(textArea).toHaveValue(props.log); 24 | }); 25 | 26 | it('It should show an empty session name when passing an undefined room value', () => { 27 | // Arrange 28 | const props = { 29 | room: undefined, 30 | log: 'This is test', 31 | }; 32 | 33 | const expectedSessionNameText = `Session name:`; 34 | 35 | // Act 36 | render(); 37 | 38 | // Assert 39 | const sessionName = screen.getByRole('heading'); 40 | expect(sessionName).toHaveTextContent(expectedSessionNameText); 41 | }); 42 | 43 | it('It should show an empty session name when passing a null room value', () => { 44 | // Arrange 45 | const props = { 46 | room: null, 47 | log: 'This is test', 48 | }; 49 | 50 | const expectedSessionNameText = `Session name:`; 51 | 52 | // Act 53 | render(); 54 | 55 | // Assert 56 | const sessionName = screen.getByRole('heading'); 57 | 58 | expect(sessionName).toHaveTextContent(expectedSessionNameText); 59 | }); 60 | 61 | it('It should show an empty text when passing an undefined log value', () => { 62 | // Arrange 63 | const props = { 64 | room: 'Room I', 65 | log: undefined, 66 | }; 67 | 68 | // Act 69 | render(); 70 | 71 | // Assert 72 | const textArea = screen.getByTestId('session'); 73 | expect(textArea).toHaveTextContent(''); 74 | }); 75 | 76 | it('It should show an empty text when passing a null log value', () => { 77 | // Arrange 78 | const props = { 79 | room: 'Room I', 80 | log: null, 81 | }; 82 | 83 | // Act 84 | render(); 85 | 86 | // Assert 87 | const textArea = screen.getByRole('log'); 88 | expect(textArea).toHaveTextContent(''); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/header.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { palette, typography, spacing, breakpoints } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const root = css` 8 | display: grid; 9 | grid-row-gap: 1rem; 10 | grid-column-gap: 1rem; 11 | `; 12 | export const labelContainer = css` 13 | border-bottom: 1px solid rgba(0, 0, 0, 0.12); 14 | `; 15 | 16 | export const label = css` 17 | font-family: ${typography.fontFamily}; 18 | font-size: 1.125rem; 19 | color: ${palette.text.primary}; 20 | display: flex; 21 | justify-content: space-between; 22 | width: 50%; 23 | cursor: pointer; 24 | & :hover { 25 | background-color: rgba(0, 0, 0, 0.12); 26 | } 27 | 28 | @media (min-width: ${theme.breakpoints.values.sm}px) { 29 | width: 30%; 30 | } 31 | @media (min-width: ${theme.breakpoints.values.lg}px) { 32 | width: 25%; 33 | } 34 | `; 35 | 36 | export const collapseIcon = css` 37 | font-size: 1.7rem; 38 | margin: 0 1%; 39 | `; 40 | 41 | export const inputContainer = css` 42 | display: flex; 43 | flex-direction: row; 44 | `; 45 | 46 | export const input = css` 47 | flex: 1; 48 | padding: ${spacing(2.25)}; 49 | font-family: ${typography.fontFamily}; 50 | font-size: 1rem; 51 | color: ${color.secondary}; 52 | border: 2px solid ${color.greyMedium}; 53 | border-top-left-radius: 0.4rem; 54 | border-bottom-left-radius: 0.4rem; 55 | &:hover, 56 | &:active, 57 | &:focus { 58 | border: 2px solid ${color.greyMedium}; 59 | outline: none; 60 | } 61 | &::selection { 62 | background: ${color.greyMedium}; 63 | } 64 | `; 65 | 66 | export const icon = css` 67 | font-size: 1.875rem; 68 | `; 69 | 70 | export const trainerBackgroundColor = css` 71 | background-color: #F0A0A0; 72 | `; 73 | 74 | export const studentBackgroundColor = css` 75 | background-color: #62A39B; 76 | `; 77 | 78 | export const button = css` 79 | width: ${spacing(7.375)}; 80 | height: ${spacing(7.375)}; 81 | background-color: ${color.background}; 82 | border-right: 2px solid ${color.greyMedium}; 83 | border-top: 2px solid ${color.greyMedium}; 84 | border-bottom: 2px solid ${color.greyMedium}; 85 | border-left: none; 86 | border-top-right-radius: 0.4rem; 87 | border-bottom-right-radius: 0.4rem; 88 | outline: none; 89 | &:hover { 90 | cursor: pointer; 91 | background-color: ${color.greyMedium}; 92 | } 93 | &:active { 94 | background-color: ${color.background}; 95 | } 96 | @media (max-width: ${breakpoints.values.xs}px) { 97 | width: ${spacing(6.5)}; 98 | height: ${spacing(6.5)}; 99 | } 100 | `; 101 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/session.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { SessionComponent } from './session.component'; 5 | 6 | describe('SessionComponent unit tests', () => { 7 | it('should send the concatenation of the session saved text plus the entered one when clicking the send full content button', async () => { 8 | // Arrange 9 | const props = { 10 | log: 'Hello! ', 11 | handleSendFullContentLog: jest.fn(), 12 | }; 13 | const expectedSentText = 'Hello! How are you doing?'; 14 | 15 | // Act 16 | 17 | render(); 18 | 19 | const textArea = screen.getByRole('log'); 20 | const sendFullContentButton = screen.getAllByRole('button')[1]; 21 | 22 | await userEvent.type(textArea, 'How are you doing?'); 23 | await userEvent.click(sendFullContentButton); 24 | 25 | // Assert 26 | 27 | expect(props.handleSendFullContentLog).toHaveBeenCalledTimes(1); 28 | expect(props.handleSendFullContentLog).toHaveBeenCalledWith( 29 | expectedSentText 30 | ); 31 | }); 32 | 33 | it('should send the concatenation of the previous session text plus the entered one when clicking the send full content button', async () => { 34 | // Arrange 35 | const props = { 36 | log: 'Hello! ', 37 | handleSendFullContentLog: jest.fn(), 38 | }; 39 | const expectedSentText = 'Hello! How are you doing?'; 40 | 41 | // Act 42 | 43 | render(); 44 | 45 | const textArea = screen.getByRole('log'); 46 | const sendFullContentButton = screen.getAllByRole('button')[1]; 47 | 48 | await userEvent.type(textArea, 'How are you doing?'); 49 | await userEvent.click(sendFullContentButton); 50 | 51 | // Assert 52 | 53 | expect(props.handleSendFullContentLog).toHaveBeenCalledTimes(1); 54 | expect(props.handleSendFullContentLog).toHaveBeenCalledWith( 55 | expectedSentText 56 | ); 57 | }); 58 | 59 | it('should reset the session text to the original one when clicking the undo button', async () => { 60 | // Arrange 61 | const props = { 62 | log: 'Hello! ', 63 | handleSendFullContentLog: jest.fn(), 64 | }; 65 | const expectedSentText = 'Hello! '; 66 | 67 | // Act 68 | 69 | render(); 70 | 71 | const textArea = screen.getByRole('log'); 72 | const undoButton = screen.getAllByRole('button')[0]; 73 | 74 | await userEvent.type(textArea, 'How are you doing?'); 75 | await userEvent.click(undoButton); 76 | 77 | // Assert 78 | 79 | expect(textArea).toHaveValue(expectedSentText); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /front/src/common-app/components/footer/footer.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { palette, spacing, breakpoints } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const footerContainer = css` 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | height: ${spacing(37.5)}; 12 | padding: ${spacing(2.5)} ${spacing(12.5)}; 13 | background-color: ${color.secondary}; 14 | @media (max-width: ${breakpoints.values.md}px) { 15 | padding: ${spacing(2.5)} ${spacing(5)} ${spacing(3.75)} ${spacing(5)}; 16 | } 17 | @media (max-width: ${breakpoints.values.xs}px) { 18 | height: ${spacing(43.75)}; 19 | } 20 | `; 21 | 22 | export const topContainer = css` 23 | display: flex; 24 | justify-content: space-between; 25 | height: ${spacing(24.375)}; 26 | padding: 0 ${spacing(2)}; 27 | align-items: center; 28 | @media (max-width: ${breakpoints.values.xs}px) { 29 | flex-direction: column-reverse; 30 | justify-content: center; 31 | align-items: center; 32 | height: ${spacing(27.5)}; 33 | } 34 | `; 35 | 36 | export const topContainerCenter = css` 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | height: ${spacing(24.375)}; 41 | padding: 0 ${spacing(2)}; 42 | `; 43 | 44 | export const bottomContainer = css` 45 | display: flex; 46 | justify-content: space-between; 47 | padding-top: ${spacing(2)}; 48 | border-top: 1px solid ${color.primary}; 49 | @media (max-width: ${breakpoints.values.xs}px) { 50 | flex-direction: column; 51 | justify-content: center; 52 | align-items: center; 53 | } 54 | `; 55 | 56 | export const lemoncodeIcon = css` 57 | height: ${spacing(12.5)}; 58 | `; 59 | 60 | export const iconContainer = css` 61 | display: flex; 62 | margin: 0; 63 | padding: 0; 64 | list-style: none; 65 | `; 66 | 67 | export const iconListItem = css` 68 | margin-right: ${spacing(1)}; 69 | &:first-of-type { 70 | margin-right: ${spacing(1.4)}; 71 | } 72 | &:last-of-type { 73 | margin-right: 0; 74 | } 75 | `; 76 | 77 | export const icon = css` 78 | font-size: 1.5rem; 79 | color: ${color.greyLight}; 80 | &:hover { 81 | cursor: pointer; 82 | color: ${color.primary}; 83 | } 84 | `; 85 | 86 | export const githubIcon = css` 87 | height: ${spacing(2.75)}; 88 | fill: ${color.greyLight}; 89 | &:hover { 90 | cursor: pointer; 91 | fill: ${color.primary}; 92 | } 93 | `; 94 | 95 | export const copyright = css` 96 | color: ${color.greyLight}; 97 | @media (max-width: ${breakpoints.values.xs}px) { 98 | margin-top: ${spacing(0.8)}; 99 | } 100 | `; 101 | -------------------------------------------------------------------------------- /front/src/pods/create-session/create-session.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | import background from 'assets/bg-create-session.png'; 4 | 5 | const { spacing, breakpoints, palette } = theme; 6 | const color = palette.customPalette; 7 | 8 | export const mainContainer = css` 9 | position: relative; 10 | display: flex; 11 | align-items: center; 12 | width: 100%; 13 | height: calc(100vh - ${spacing(10.75)}); 14 | margin: 0; 15 | padding: 0; 16 | background-image: url(${background}); 17 | background-repeat: no-repeat; 18 | background-position: right bottom; 19 | @media (max-width: ${breakpoints.values.lg}px) { 20 | background-image: none; 21 | } 22 | @media (max-width: ${breakpoints.values.sm}px) { 23 | width: 90%; 24 | height: calc(100vh - ${spacing(18.375)}); 25 | margin: 0 auto; 26 | } 27 | `; 28 | 29 | export const buttonContainer = css` 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | width: ${spacing(50)}; 34 | margin-left: 15%; 35 | @media (max-width: ${breakpoints.values.lg}px) { 36 | margin-left: auto; 37 | margin-right: auto; 38 | } 39 | `; 40 | 41 | export const descriptionText = css` 42 | text-align: center; 43 | font-size: 1.8rem; 44 | font-weight: 400; 45 | @media (max-width: ${breakpoints.values.sm}px) { 46 | font-size: 1.6rem; 47 | } 48 | @media (max-width: ${breakpoints.values.xs}px) { 49 | font-size: 1.5rem; 50 | } 51 | `; 52 | 53 | export const createSessionBtn = css` 54 | display: flex; 55 | align-items: center; 56 | margin-top: ${spacing(4)}; 57 | padding: ${spacing(2.5)} ${spacing(3.75)}; 58 | text-transform: capitalize; 59 | font-size: 1.4rem; 60 | font-weight: 400; 61 | color: ${color.alertLight}; 62 | background: linear-gradient( 63 | to right, 64 | ${color.alertLight}, 65 | ${color.alertLight} 66 | ); 67 | background-repeat: no-repeat; 68 | background-size: 0 100%; 69 | transition: all 0.4s 0s; 70 | border: 2px solid ${color.alertLight}; 71 | &:hover, 72 | &:active { 73 | cursor: pointer; 74 | background-size: 100% 100%; 75 | color: white; 76 | outline: none; 77 | } 78 | @media (max-width: ${breakpoints.values.sm}px) { 79 | color: white; 80 | background: none; 81 | background-color: ${color.alertLight}; 82 | border: none; 83 | &:hover, 84 | &:active { 85 | background-color: ${color.secondary}; 86 | outline: none; 87 | } 88 | } 89 | @media (max-width: ${breakpoints.values.xs}px) { 90 | padding: ${spacing(2.5)} ${spacing(3)}; 91 | font-size: 1.2rem; 92 | } 93 | `; 94 | 95 | export const arrowIcon = css` 96 | margin-left: ${spacing(1.6)}; 97 | font-size: 2rem; 98 | @media (max-width: ${breakpoints.values.xs}px) { 99 | font-size: 1.6rem; 100 | } 101 | `; 102 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/header.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cx } from '@emotion/css'; 3 | import Collapse from '@mui/material/Collapse'; 4 | import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; 5 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 6 | import ExpandLessIcon from '@mui/icons-material/ExpandLess'; 7 | import * as innerClasses from './header.styles'; 8 | 9 | interface Props { 10 | currentTrainerUrl: string; 11 | currentStudentUrl: string; 12 | className?: string; 13 | } 14 | 15 | export const HeaderComponent: React.FC = props => { 16 | const { currentStudentUrl, currentTrainerUrl, className } = props; 17 | 18 | return ( 19 |
20 | 26 | 32 |
33 | ); 34 | }; 35 | 36 | // CopyField Component - - - - - - - - - - - - - - - 37 | 38 | interface CopyFieldProps { 39 | labelName: string; 40 | inputId: string; 41 | urlLink: string; 42 | className?: string; 43 | } 44 | 45 | export const CopyFieldComponent: React.FC = props => { 46 | const { labelName, inputId, urlLink, className } = props; 47 | 48 | const [open, setOpen] = React.useState(true); 49 | 50 | const handleClick = () => { 51 | setOpen(!open); 52 | }; 53 | 54 | return ( 55 | <> 56 |
57 | 69 |
70 | 71 |
72 | 80 | 87 |
88 |
89 | 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /back/src/pods/messages/processors/output-processor.ts: -------------------------------------------------------------------------------- 1 | import { sessionRepository, roomRepository } from 'dals'; 2 | import { 3 | OutputMessageTypes, 4 | SocketOuputMessageLiteral, 5 | } from '../messages.consts'; 6 | import { Action, SocketInfo } from '../messages.model'; 7 | import { ResponseBase, responseType } from './response'; 8 | 9 | export const processOutputMessageCollection = async ( 10 | socketInfo: SocketInfo, 11 | actionCollection: Action[] 12 | ): Promise => { 13 | if (actionCollection) { 14 | // TODO: Error handling 15 | for (const action of actionCollection) { 16 | await processOuputMessage(socketInfo, action); 17 | } 18 | } 19 | }; 20 | 21 | export const processOuputMessage = async ( 22 | socketInfo: SocketInfo, 23 | action: Action 24 | ) => { 25 | switch (action.type) { 26 | case OutputMessageTypes.CONNECTION_ESTABLISHED_TRAINER: 27 | await handleNotifyConnectionEstablishedTrainer(socketInfo); 28 | break; 29 | case OutputMessageTypes.CONNECTION_ESTABLISHED_STUDENT: 30 | await handleNotifyConnectionEstablishedStudent(socketInfo); 31 | break; 32 | case OutputMessageTypes.APPEND_TEXT: 33 | await handleAppendText(socketInfo, action.payload); 34 | break; 35 | case OutputMessageTypes.UPDATE_FULL_CONTENT: 36 | await handleUpdateFullContent(socketInfo); 37 | break; 38 | default: 39 | break; 40 | } 41 | }; 42 | 43 | const handleNotifyConnectionEstablishedTrainer = async ( 44 | socketInfo: SocketInfo 45 | ) => { 46 | await handleSendFullContent( 47 | socketInfo, 48 | responseType.TRAINER_GET_FULL_CONTENT, 49 | false 50 | ); 51 | }; 52 | 53 | const handleNotifyConnectionEstablishedStudent = async ( 54 | socketInfo: SocketInfo 55 | ) => { 56 | await handleSendFullContent( 57 | socketInfo, 58 | responseType.STUDENT_GET_FULL_CONTENT, 59 | false 60 | ); 61 | }; 62 | 63 | const handleAppendText = async (socketInfo: SocketInfo, text: string) => { 64 | const room = await sessionRepository.getRoomFromConnectionId( 65 | socketInfo.connectionId 66 | ); 67 | socketInfo.io.in(room).emit(SocketOuputMessageLiteral.MESSAGE, { 68 | type: responseType.APPEND_TEXT, 69 | payload: text, 70 | }); 71 | }; 72 | 73 | const handleUpdateFullContent = async (socketInfo: SocketInfo) => { 74 | await handleSendFullContent( 75 | socketInfo, 76 | responseType.UPDATE_FULL_CONTENT, 77 | true 78 | ); 79 | }; 80 | 81 | const handleSendFullContent = async ( 82 | socketInfo: SocketInfo, 83 | responseType: string, 84 | sendToAll: boolean 85 | ) => { 86 | const room = await sessionRepository.getRoomFromConnectionId( 87 | socketInfo.connectionId 88 | ); 89 | const content = await roomRepository.getRoomContent(room); 90 | const msg = { type: responseType, payload: content }; 91 | sendToAll 92 | ? socketInfo.io.in(room).emit(SocketOuputMessageLiteral.MESSAGE, msg) 93 | : socketInfo.socket.emit(SocketOuputMessageLiteral.MESSAGE, msg); 94 | }; 95 | -------------------------------------------------------------------------------- /front/src/pods/trainer/trainer.container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useParams } from 'react-router'; 3 | import { lineSeparator } from 'core/const'; 4 | import { routes } from 'core/router/routes'; 5 | import { 6 | createSocket, 7 | SocketOuputMessageLiteral, 8 | SocketEmitMessageTypes, 9 | SocketReceiveMessageTypes, 10 | } from 'core'; 11 | import { useLog } from 'core'; 12 | import { TrainerComponent } from './trainer.component'; 13 | import { useWithRef, getHostBaseUrl } from 'common'; 14 | 15 | interface Params extends Record { 16 | token: string; 17 | room: string; 18 | } 19 | 20 | export const TrainerContainer = () => { 21 | const { token, room } = useParams(); 22 | const { log, appendToLog, setLog } = useLog(); 23 | const [_, setSocket, socketRef] = useWithRef(null); 24 | 25 | const [currentTrainerUrl, setCurrentTrainerUrl] = React.useState(''); 26 | const [currentStudentUrl, setCurrentStudentUrl] = React.useState(''); 27 | 28 | const handleConnection = () => { 29 | const socket = createSocket({ room: room, trainertoken: token }); 30 | setSocket(socket); 31 | socket.on(SocketOuputMessageLiteral.MESSAGE, (msg) => { 32 | if (msg.type) { 33 | const { type, payload } = msg; 34 | 35 | switch (type) { 36 | case SocketReceiveMessageTypes.APPEND_TEXT: 37 | appendToLog(payload); 38 | break; 39 | case SocketReceiveMessageTypes.TRAINER_GET_FULL_CONTENT: 40 | case SocketReceiveMessageTypes.UPDATE_FULL_CONTENT: 41 | setLog(payload); 42 | break; 43 | default: 44 | break; 45 | } 46 | } 47 | }); 48 | }; 49 | 50 | React.useEffect(() => { 51 | setCurrentTrainerUrl(`${getHostBaseUrl()}#${routes.trainer(room, token)}`); 52 | setCurrentStudentUrl(`${getHostBaseUrl()}#${routes.student(room)}`); 53 | handleConnection(); 54 | }, []); 55 | 56 | const appendLineSeparator = (text: string): string => 57 | `${text}${lineSeparator}`; 58 | 59 | const sendTrainerTextToServer = (text: string, action: string): void => { 60 | socketRef.current.emit(SocketOuputMessageLiteral.MESSAGE, { 61 | type: action, 62 | payload: text, 63 | }); 64 | }; 65 | 66 | const handleAppendTrainerText = (trainerText: string): void => { 67 | const finalText = appendLineSeparator(trainerText); 68 | sendTrainerTextToServer( 69 | finalText, 70 | SocketEmitMessageTypes.TRAINER_APPEND_TEXT 71 | ); 72 | }; 73 | 74 | const handleSendFullContentLog = (fullContent: string): void => { 75 | fullContent && 76 | sendTrainerTextToServer( 77 | fullContent, 78 | SocketEmitMessageTypes.TRAINER_SET_FULL_TEXT 79 | ); 80 | }; 81 | 82 | return ( 83 | 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/session.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cx } from '@emotion/css'; 3 | import TextareaAutosize from '@mui/material/TextareaAutosize'; 4 | import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded'; 5 | import UndoIcon from '@mui/icons-material/Undo'; 6 | import Button from '@mui/material/Button'; 7 | 8 | import FormControlLabel from '@mui/material/FormControlLabel'; 9 | import Checkbox from '@mui/material/Checkbox'; 10 | 11 | import * as innerClasses from './session.styles'; 12 | import { useAutoScroll } from 'common/hooks/auto-scroll.hook'; 13 | interface Props { 14 | log: string; 15 | handleSendFullContentLog: (fullContent: string) => void; 16 | className?: string; 17 | } 18 | 19 | const getTextArea = (elementId: string): HTMLInputElement => 20 | document.getElementById('session') as HTMLInputElement; 21 | 22 | const handleSetSessionContent = (sessionContent: string) => { 23 | const sessionTextArea: HTMLInputElement = getTextArea('session'); 24 | sessionTextArea ? (sessionTextArea.value = sessionContent) : undefined; 25 | }; 26 | 27 | const getFullContent = (currenSessionContent: string) => { 28 | const sessionTextArea: HTMLInputElement = getTextArea('session'); 29 | return sessionTextArea && sessionTextArea.value != currenSessionContent 30 | ? sessionTextArea.value 31 | : undefined; 32 | }; 33 | 34 | export const SessionComponent: React.FC = (props) => { 35 | const { log, handleSendFullContentLog, className } = props; 36 | 37 | const { 38 | isAutoScrollEnabled, 39 | setIsAutoScrollEnabled, 40 | textAreaRef, 41 | doAutoScroll, 42 | } = useAutoScroll(); 43 | 44 | React.useEffect(() => { 45 | handleSetSessionContent(log); 46 | doAutoScroll(); 47 | }, [log]); 48 | 49 | return ( 50 |
51 | 54 | 55 | 63 | setIsAutoScrollEnabled(e.target.checked)} 69 | color="primary" 70 | /> 71 | } 72 | /> 73 | 83 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-paster", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "run-p -l type-check:watch start:dev", 8 | "start:dev": "webpack serve --config ./config/webpack/dev.js", 9 | "build": "run-p -l type-check build:prod", 10 | "build:prod": "npm run clean && webpack --config ./config/webpack/prod.js", 11 | "analyze": "npm run clean && npm run type-check && webpack --config ./config/webpack/analyze.js", 12 | "type-check": "tsc --noEmit", 13 | "type-check:watch": "npm run type-check -- --watch", 14 | "test": "jest -c ./config/test/jest.js --verbose", 15 | "test:watch": "npm run test -- --watchAll -i --no-cache", 16 | "clean": "rimraf dist", 17 | "prettier": "prettier {config,src}/**/*{.js,.jsx,.ts,.tsx}", 18 | "prettier:fix": "npm run prettier -- --fix" 19 | }, 20 | "repository": { 21 | "type": "git" 22 | }, 23 | "author": "Lemoncode", 24 | "license": "MIT", 25 | "homepage": "", 26 | "dependencies": { 27 | "@emotion/css": "^11.10.5", 28 | "@emotion/react": "^11.10.5", 29 | "@emotion/styled": "^11.10.5", 30 | "@mui/icons-material": "^5.11.0", 31 | "@mui/material": "^5.11.8", 32 | "axios": "^1.3.2", 33 | "lodash.merge": "^4.6.2", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-router-dom": "^6.8.1", 37 | "regenerator-runtime": "^0.13.11", 38 | "socket.io": "^2.5.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.20.7", 42 | "@babel/core": "^7.20.12", 43 | "@babel/preset-env": "^7.20.2", 44 | "@babel/preset-react": "^7.18.6", 45 | "@babel/preset-typescript": "^7.18.6", 46 | "@emotion/babel-plugin": "^11.10.5", 47 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 48 | "@svgr/webpack": "^6.5.1", 49 | "@testing-library/dom": "^8.20.0", 50 | "@testing-library/jest-dom": "^5.16.5", 51 | "@testing-library/react": "^13.4.0", 52 | "@testing-library/user-event": "^14.4.3", 53 | "@types/jest": "^29.4.0", 54 | "@types/react": "^18.0.28", 55 | "@types/react-dom": "^18.0.10", 56 | "@types/socket.io": "^2.1.13", 57 | "@typescript-eslint/eslint-plugin": "^5.51.0", 58 | "@typescript-eslint/parser": "^5.51.0", 59 | "babel-loader": "^9.1.2", 60 | "copy-webpack-plugin": "^11.0.0", 61 | "css-loader": "^6.7.3", 62 | "dotenv-webpack": "^8.0.1", 63 | "eslint": "^8.34.0", 64 | "eslint-config-prettier": "^8.6.0", 65 | "eslint-plugin-prettier": "^4.2.1", 66 | "eslint-plugin-react": "^7.32.2", 67 | "file-loader": "^6.2.0", 68 | "html-webpack-plugin": "^5.5.0", 69 | "jest": "^29.4.2", 70 | "jest-environment-jsdom": "^29.4.2", 71 | "lint-staged": "^13.1.1", 72 | "mini-css-extract-plugin": "^2.7.2", 73 | "npm-run-all": "^4.1.5", 74 | "prettier": "^2.8.4", 75 | "pretty-quick": "^3.1.3", 76 | "react-test-renderer": "^18.2.0", 77 | "rimraf": "^4.1.2", 78 | "style-loader": "^3.3.1", 79 | "ts-jest": "^29.0.5", 80 | "typescript": "^4.9.5", 81 | "url-loader": "^4.1.1", 82 | "webpack": "^5.75.0", 83 | "webpack-bundle-analyzer": "^4.8.0", 84 | "webpack-cli": "^5.0.1", 85 | "webpack-dev-server": "^4.11.1", 86 | "webpack-merge": "^5.8.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /front/src/pods/trainer/components/session.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { theme } from 'core/theme'; 3 | 4 | const { palette, spacing, typography, breakpoints } = theme; 5 | const color = palette.customPalette; 6 | 7 | export const root = css` 8 | display: grid; 9 | grid-row-gap: 1rem; 10 | grid-column-gap: 1rem; 11 | grid-template-columns: auto 1fr; 12 | grid-template-areas: 13 | 'label label' 14 | 'textarea textarea' 15 | 'undo send'; 16 | `; 17 | 18 | export const label = css` 19 | grid-area: label; 20 | display: block; 21 | font-size: 1.125rem; 22 | font-family: ${typography.fontFamily}; 23 | `; 24 | 25 | export const textarea = css` 26 | grid-area: textarea; 27 | box-sizing: border-box; 28 | font-family: ${typography.fontFamily}; 29 | font-size: 1rem; 30 | background-color: ${color.background}; 31 | border: 2px solid ${color.secondary}; 32 | white-space: pre-wrap; 33 | resize: none; 34 | &:focus { 35 | outline: none; 36 | } 37 | `; 38 | 39 | export const sendButton = css` 40 | grid-area: send; 41 | display: flex; 42 | align-items: center; 43 | padding: ${spacing(1.25)} ${spacing(1.875)}; 44 | flex: 1; 45 | font-size: 1.188rem; 46 | font-weight: 400; 47 | text-transform: capitalize; 48 | border-radius: 0; 49 | color: ${color.successLight}; 50 | background-color: white; 51 | border: 2px solid ${color.successLight}; 52 | transition: all 0.2s; 53 | &:hover, 54 | &:active { 55 | color: white; 56 | background-color: ${color.successLight}; 57 | border: 2px solid ${color.successLight}; 58 | outline: none; 59 | } 60 | @media (max-width: ${breakpoints.values.xs}px) { 61 | color: white; 62 | background-color: ${color.successLight}; 63 | border: none; 64 | &:hover, 65 | &:active { 66 | background-color: ${color.successDark}; 67 | border: none; 68 | } 69 | } 70 | `; 71 | 72 | export const sendIcon = css` 73 | margin-left: ${spacing(1.25)}; 74 | font-size: 1.25rem; 75 | display: none; 76 | @media (min-width: ${breakpoints.values.xs}px) { 77 | display: initial; 78 | } 79 | `; 80 | 81 | export const undoButton = css` 82 | grid-area: undo; 83 | display: flex; 84 | align-items: center; 85 | padding: ${spacing(1.25)} ${spacing(1.875)}; 86 | font-size: 1.188rem; 87 | font-weight: 400; 88 | text-transform: capitalize; 89 | color: ${color.alertLight}; 90 | background-color: white; 91 | border-radius: 0; 92 | border: 2px solid ${color.alertLight}; 93 | transition: all 0.2s; 94 | &:hover, 95 | &:active { 96 | color: white; 97 | background-color: ${color.alertLight}; 98 | border: 2px solid ${color.alertLight}; 99 | outline: none; 100 | } 101 | @media (max-width: ${breakpoints.values.xs}px) { 102 | color: white; 103 | background-color: ${color.alertLight}; 104 | border: none; 105 | &:hover, 106 | &:active { 107 | background-color: ${color.alertDark}; 108 | border: none; 109 | } 110 | } 111 | `; 112 | 113 | export const undoIcon = css` 114 | margin-right: ${spacing(1.25)}; 115 | font-size: 1.25rem; 116 | display: none; 117 | @media (min-width: ${breakpoints.values.xs}px) { 118 | display: initial; 119 | } 120 | `; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | [![Contributors][contributors-shield]][contributors-url] 11 | [![Forks][forks-shield]][forks-url] 12 | [![Stargazers][stars-shield]][stars-url] 13 | [![Issues][issues-shield]][issues-url] 14 | [![MIT License][license-shield]][license-url] 15 | 16 | 17 |
18 |

19 | 20 | Logo 21 | 22 |

23 | Code Paster is a Teacher-to-Student communication tool for online programming lessons. Create a new session and easily share your code with all students connected so they can follow along with your examples. 24 |

25 |

26 | Click here to navigate to Codepaster 27 |

28 |

29 |
30 | 31 | 32 | 33 | ## Table of Contents 34 | 35 | - [About the Project](#about-the-project) 36 | - [Built With](#built-with) 37 | - [Roadmap](#roadmap) 38 | - [License](#license) 39 | 40 | 41 | 42 | ## About The Project 43 | 44 | During your programming online lessons you will write hundreds of lines of code and dozens of snippets. For students following along with the code there's nothing more annoying than falling behind the explanation because of a small typo or a missed variable. With Code Paster you can simply copy your code examples and share it in real time with your classroom. 45 | 46 | This project was developed by Lemoncode Frontend Master students: 47 | 48 | 49 | 50 | ### Built With 51 | 52 | - [React](https://github.com/facebook/react/) 53 | - [Material-UI](https://material-ui.com/) 54 | - [Socket.io](https://socket.io/) 55 | 56 | 57 | 58 | ## Roadmap 59 | 60 | See the [open issues](https://github.com/Lemoncode/code-paster/issues) for a list of proposed features (and known issues). 61 | 62 | 63 | 64 | ## License 65 | 66 | Distributed under the MIT License. See `LICENSE` for more information. 67 | 68 | 69 | 70 | 71 | [contributors-shield]: https://img.shields.io/github/contributors/Lemoncode/code-paster.svg?style=flat-square 72 | [contributors-url]: https://github.com/Lemoncode/code-paster/graphs/contributors 73 | [forks-shield]: https://img.shields.io/github/forks/Lemoncode/code-paster.svg?style=flat-square 74 | [forks-url]: https://github.com/Lemoncode/code-paster/network/members 75 | [stars-shield]: https://img.shields.io/github/stars/Lemoncode/code-paster.svg?style=flat-square 76 | [stars-url]: https://github.com/Lemoncode/code-paster/stargazers 77 | [issues-shield]: https://img.shields.io/github/issues/Lemoncode/code-paster.svg?style=flat-square 78 | [issues-url]: https://github.com/Lemoncode/code-paster/issues 79 | [license-shield]: https://img.shields.io/github/license/Lemoncode/code-paster.svg?style=flat-square 80 | [license-url]: https://github.com/Lemoncode/code-paster/blob/master/LICENSE.txt 81 | -------------------------------------------------------------------------------- /front/src/assets/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 18 | 24 | 27 | 30 | 33 | 36 | 43 | 46 | 49 | 60 | 62 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /back/src/pods/messages/processors/input-processor.ts: -------------------------------------------------------------------------------- 1 | import { sessionRepository, roomRepository, RoomInfo } from 'dals'; 2 | import { InputMessageTypes, OutputMessageTypes } from '../messages.consts'; 3 | import { 4 | Action, 5 | InputEstablishConnectionTrainer, 6 | SocketInfo, 7 | } from '../messages.model'; 8 | 9 | export const processInputMessage = async ( 10 | socketInfo: SocketInfo, 11 | action: Action 12 | ): Promise => { 13 | let outputActionCollection: Action[] = []; 14 | switch (action.type) { 15 | case InputMessageTypes.ESTABLISH_CONNECTION_TRAINER: 16 | const payloadECT: InputEstablishConnectionTrainer = action.payload; 17 | outputActionCollection = await handleEstablishConnectionTrainer( 18 | socketInfo, 19 | payloadECT.room, 20 | payloadECT.trainertoken 21 | ); 22 | break; 23 | case InputMessageTypes.ESTABLISH_CONNECTION_STUDENT: 24 | const payloadECS: InputEstablishConnectionTrainer = action.payload; 25 | outputActionCollection = await handleEstablishConnectionStudent( 26 | socketInfo, 27 | payloadECS.room 28 | ); 29 | break; 30 | case InputMessageTypes.TRAINER_APPEND_TEXT: 31 | outputActionCollection = await handleTrainerSendText( 32 | socketInfo, 33 | action.payload, 34 | InputMessageTypes.TRAINER_APPEND_TEXT 35 | ); 36 | break; 37 | case InputMessageTypes.TRAINER_SET_FULL_TEXT: 38 | outputActionCollection = await handleTrainerSendText( 39 | socketInfo, 40 | action.payload, 41 | InputMessageTypes.TRAINER_SET_FULL_TEXT 42 | ); 43 | break; 44 | default: 45 | break; 46 | } 47 | 48 | return outputActionCollection; 49 | }; 50 | 51 | const handleEstablishConnectionTrainer = async ( 52 | socketInfo: SocketInfo, 53 | room: string, 54 | trainerToken: string 55 | ): Promise => { 56 | if (!trainerToken || !room) { 57 | // Ignore 58 | return []; 59 | } 60 | 61 | const roomAvailable: boolean = await roomRepository.isRoomAvailable(room); 62 | 63 | if (roomAvailable) { 64 | roomRepository.saveRoomInfo({ room, content: '' }, false); 65 | } 66 | 67 | if ( 68 | roomAvailable || 69 | !(await sessionRepository.isExistingConnection(socketInfo.connectionId)) 70 | ) { 71 | await sessionRepository.addNewUser({ 72 | connectionId: socketInfo.connectionId, 73 | room, 74 | trainerToken, 75 | isTrainer: !!trainerToken, 76 | }); 77 | socketInfo.socket.join(room); 78 | } 79 | return [{ type: OutputMessageTypes.CONNECTION_ESTABLISHED_TRAINER }]; 80 | }; 81 | 82 | const handleEstablishConnectionStudent = async ( 83 | socketInfo: SocketInfo, 84 | room: string 85 | ): Promise => { 86 | if (!room) { 87 | // Ignore 88 | return []; 89 | } 90 | 91 | const roomAvailable: boolean = await roomRepository.isRoomAvailable(room); 92 | 93 | if ( 94 | roomAvailable || 95 | !(await sessionRepository.isExistingConnection(socketInfo.connectionId)) 96 | ) { 97 | await sessionRepository.addNewUser({ 98 | connectionId: socketInfo.connectionId, 99 | room, 100 | trainerToken: '', 101 | isTrainer: false, 102 | }); 103 | socketInfo.socket.join(room); 104 | } 105 | return [{ type: OutputMessageTypes.CONNECTION_ESTABLISHED_STUDENT }]; 106 | }; 107 | 108 | const handleTrainerSendText = async ( 109 | socketInfo: SocketInfo, 110 | text: string, 111 | action: string 112 | ): Promise => { 113 | if (!sessionRepository.isTrainerUser(socketInfo.connectionId)) { 114 | return []; 115 | } 116 | const room = await sessionRepository.getRoomFromConnectionId( 117 | socketInfo.connectionId 118 | ); 119 | const roomInfo: RoomInfo = { room, content: text }; 120 | 121 | switch (action) { 122 | case InputMessageTypes.TRAINER_APPEND_TEXT: 123 | await roomRepository.saveRoomInfo(roomInfo, false); 124 | return [{ type: OutputMessageTypes.APPEND_TEXT, payload: text }]; 125 | case InputMessageTypes.TRAINER_SET_FULL_TEXT: 126 | await roomRepository.saveRoomInfo(roomInfo, true); 127 | return [{ type: OutputMessageTypes.UPDATE_FULL_CONTENT }]; 128 | default: 129 | return []; 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /front/src/core/use-log.hook.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useLog } from './use-log.hook'; 3 | 4 | describe('use-log hook spec', () => { 5 | it('"log" state should be an empty string by default', () => { 6 | // Arrange 7 | 8 | // Act 9 | const { result } = renderHook(() => useLog()); 10 | 11 | // Assert 12 | expect(result.current.log).toEqual(''); 13 | }); 14 | 15 | it('"logRef.current" should be an empty string by default', () => { 16 | // Arrange 17 | 18 | // Act 19 | const { result } = renderHook(() => useLog()); 20 | 21 | // Assert 22 | expect(result.current.logRef.current).toEqual(''); 23 | }); 24 | 25 | it('"appendToLog" & "setLog" should be functions by default', () => { 26 | // Arrange 27 | 28 | // Act 29 | const { result } = renderHook(() => useLog()); 30 | 31 | // Assert 32 | expect(result.current.appendToLog).toEqual(expect.any(Function)); 33 | expect(result.current.setLog).toEqual(expect.any(Function)); 34 | }); 35 | 36 | it('"log" & "logRef.current" should return new content when calling "setLog"', () => { 37 | // Arrange 38 | const expectedResult: string = 'new-value'; 39 | 40 | // Act 41 | const { result } = renderHook(() => useLog()); 42 | act(() => { 43 | result.current.setLog('new-value'); 44 | }); 45 | 46 | // Assert 47 | expect(result.current.log).toEqual(expectedResult); 48 | expect(result.current.logRef.current).toEqual(expectedResult); 49 | }); 50 | 51 | it('"log" & "logRef.current" should return old value when feed "setLog" with "undefined" parameter', () => { 52 | // Arrange 53 | const expectedResult: string = 'old-value'; 54 | 55 | // Act 56 | const { result } = renderHook(() => useLog()); 57 | 58 | act(() => { 59 | result.current.setLog('old-value'); 60 | }); 61 | 62 | act(() => { 63 | result.current.setLog(undefined); 64 | }); 65 | 66 | // Assert 67 | expect(result.current.log).toEqual(expectedResult); 68 | expect(result.current.logRef.current).toEqual(expectedResult); 69 | }); 70 | 71 | it('"log" & "logRef.current" should return old value when feed "setLog" with "null" parameter', () => { 72 | // Arrange 73 | const expectedResult: string = 'old-value'; 74 | 75 | // Act 76 | const { result } = renderHook(() => useLog()); 77 | 78 | act(() => { 79 | result.current.setLog('old-value'); 80 | }); 81 | 82 | act(() => { 83 | result.current.setLog(null); 84 | }); 85 | 86 | // Assert 87 | expect(result.current.log).toEqual(expectedResult); 88 | expect(result.current.logRef.current).toEqual(expectedResult); 89 | }); 90 | 91 | it('"log" & "logRef.current" should return new value with one space when calling "appendToLog" for the first time', () => { 92 | // Arrange 93 | const expectedResult: string = `new-value `; 94 | 95 | // Act 96 | const { result } = renderHook(() => useLog()); 97 | 98 | act(() => { 99 | result.current.appendToLog('new-value'); 100 | }); 101 | 102 | // Assert 103 | expect(result.current.log).toEqual(expectedResult); 104 | expect(result.current.logRef.current).toEqual(expectedResult); 105 | }); 106 | 107 | it('"log" & "logRef.current" should return log value with one space and new value with one space in a new line when calling "appendToLog" after first time', () => { 108 | // Arrange 109 | const expectedResult: string = `old-value \nnew-value `; 110 | 111 | // Act 112 | const { result } = renderHook(() => useLog()); 113 | 114 | act(() => { 115 | result.current.appendToLog('old-value'); 116 | }); 117 | 118 | act(() => { 119 | result.current.appendToLog('new-value'); 120 | }); 121 | 122 | // Assert 123 | expect(result.current.log).toEqual(expectedResult); 124 | expect(result.current.logRef.current).toEqual(expectedResult); 125 | }); 126 | 127 | it('"log" & "logRef.current" should return the same text after calling "setLog" with that text', () => { 128 | // Arrange 129 | const sessionLog: string = `first line 130 | 131 | ********************************* 132 | 133 | second line 134 | 135 | ********************************* 136 | 137 | third line 138 | 139 | *********************************`; 140 | 141 | // Act 142 | const { result } = renderHook(() => useLog()); 143 | 144 | act(() => { 145 | result.current.setLog(sessionLog); 146 | }); 147 | 148 | // Assert 149 | expect(result.current.log).toEqual(sessionLog); 150 | expect(result.current.logRef.current).toEqual(sessionLog); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /back/src/pods/room/baseTrainerTokens.ts: -------------------------------------------------------------------------------- 1 | export const baseTrainerTokens = [ 2 | 'son_goku', 3 | 'vegeta', 4 | 'bardock', 5 | 'king_vegeta', 6 | 'nappa', 7 | 'toma', 8 | 'selypar', 9 | 'gine', 10 | 'bulma', 11 | 'piccolo', 12 | 'krilin', 13 | 'freezer', 14 | 'aika', 15 | 'aiko', 16 | 'aimi', 17 | 'aina', 18 | 'aini', 19 | 'airi', 20 | 'akane', 21 | 'akemi', 22 | 'aki', 23 | 'akihiro', 24 | 'akio', 25 | 'akira', 26 | 'amaterasu', 27 | 'ami', 28 | 'aoi', 29 | 'arata', 30 | 'asami', 31 | 'asuka', 32 | 'atsuko', 33 | 'aya', 34 | 'ayaka', 35 | 'ayako', 36 | 'ayame', 37 | 'ayane', 38 | 'ayano', 39 | 'ayumu', 40 | 'chie', 41 | 'chieko', 42 | 'chiharu', 43 | 'chika', 44 | 'chikako', 45 | 'chinatsu', 46 | 'chiyo', 47 | 'chiyoko', 48 | 'cho', 49 | 'chouko', 50 | 'dai', 51 | 'daichi', 52 | 'daiki', 53 | 'daisuke', 54 | 'eiji', 55 | 'eiko', 56 | 'emi', 57 | 'emiko', 58 | 'eri', 59 | 'etsuko', 60 | 'fumiko', 61 | 'fumio', 62 | 'gajeel', 63 | 'goro', 64 | 'hachiro', 65 | 'hajime', 66 | 'hana', 67 | 'hanako', 68 | 'haru', 69 | 'haruka', 70 | 'haruki', 71 | 'haruko', 72 | 'harumi', 73 | 'haruna', 74 | 'haruo', 75 | 'haruto', 76 | 'hayate', 77 | 'hayato', 78 | 'hibiki', 79 | 'hideaki', 80 | 'hideki', 81 | 'hideo', 82 | 'hikari', 83 | 'hikaru', 84 | 'hina', 85 | 'hinata', 86 | 'hiraku', 87 | 'hiro', 88 | 'hiroaki', 89 | 'hiroki', 90 | 'hiroko', 91 | 'hiromi', 92 | 'hironori', 93 | 'hiroshi', 94 | 'hiroto', 95 | 'hiroyuki', 96 | 'hisako', 97 | 'hisao', 98 | 'hisashi', 99 | 'hisoka', 100 | 'hitomi', 101 | 'hitoshi', 102 | 'honoka', 103 | 'hoshi', 104 | 'hoshiko', 105 | 'hotaka', 106 | 'hotaru', 107 | 'ichiro', 108 | 'isamu', 109 | 'isao', 110 | 'itsuki', 111 | 'izumi', 112 | 'jiro', 113 | 'jun', 114 | 'junichijunko', 115 | 'juro', 116 | 'kaede', 117 | 'kaito', 118 | 'kamiko', 119 | 'kanako', 120 | 'kanon', 121 | 'kaori', 122 | 'kaoru', 123 | 'kasumi', 124 | 'katashi', 125 | 'katsu', 126 | 'katsumi', 127 | 'katashi', 128 | 'katsu', 129 | 'katsumi', 130 | 'katsuo', 131 | 'katsuro', 132 | 'kazue', 133 | 'kazuhiko', 134 | 'kazuhiro', 135 | 'kazuki', 136 | 'kazuko', 137 | 'kazumi', 138 | 'kazuo', 139 | 'kei', 140 | 'keiko', 141 | 'ken', 142 | 'kenichi', 143 | 'kenta', 144 | 'kichiro', 145 | 'kiko', 146 | 'kiku', 147 | 'kimi', 148 | 'kimiko', 149 | 'kin', 150 | 'kiyoko', 151 | 'kiyomi', 152 | 'kiyoshi', 153 | 'kohaku', 154 | 'kokoro', 155 | 'kotone', 156 | 'kouki', 157 | 'kouta', 158 | 'kumiko', 159 | 'kunio', 160 | 'kuro', 161 | 'kyo', 162 | 'kyoko', 163 | 'madoka', 164 | 'mai', 165 | 'maiko', 166 | 'maki', 167 | 'makoto', 168 | 'mami', 169 | 'mamoru', 170 | 'mana', 171 | 'manabu', 172 | 'manami', 173 | 'mao', 174 | 'mariko', 175 | 'masa', 176 | 'masaaki', 177 | 'masahiko', 178 | 'masahiro', 179 | 'masaki', 180 | 'masami', 181 | 'masanori', 182 | 'masao', 183 | 'masaru', 184 | 'masashi', 185 | 'masato', 186 | 'masayoshi', 187 | 'masayuki', 188 | 'masumi', 189 | 'masuyo', 190 | 'mayu', 191 | 'mayumi', 192 | 'megumi', 193 | 'mei', 194 | 'mi', 195 | 'michi', 196 | 'michiko', 197 | 'michio', 198 | 'midori', 199 | 'mieko', 200 | 'miho', 201 | 'mika', 202 | 'miki', 203 | 'mikio', 204 | 'miku', 205 | 'minako', 206 | 'mio', 207 | 'misaki', 208 | 'mitsuko', 209 | 'mitsuo', 210 | 'mitsuru', 211 | 'miu', 212 | 'miwa', 213 | 'miyako', 214 | 'miyu', 215 | 'miyuki', 216 | 'mizuki', 217 | 'moe', 218 | 'momoe', 219 | 'momoka', 220 | 'momoko', 221 | 'moriko', 222 | 'nana', 223 | 'nanami', 224 | 'nao', 225 | 'naoki', 226 | 'naoko', 227 | 'naomi', 228 | 'natsu', 229 | 'natsuki', 230 | 'natsuko', 231 | 'natsumi', 232 | 'noa', 233 | 'noboru', 234 | 'nobu', 235 | 'nobuko', 236 | 'nobuo', 237 | 'noburu', 238 | 'nori', 239 | 'noriko', 240 | 'norio', 241 | 'osamu', 242 | 'ran', 243 | 'rei', 244 | 'reiko', 245 | 'ren', 246 | 'rie', 247 | 'rika', 248 | 'riko', 249 | 'riku', 250 | 'rikuto', 251 | 'rin', 252 | 'rina', 253 | 'rio', 254 | 'rokuro', 255 | 'ryo', 256 | 'ryoichi', 257 | 'ryoko', 258 | 'ryota', 259 | 'ryuu', 260 | 'ryuunosuke', 261 | 'saburo', 262 | 'sachiko', 263 | 'sadao', 264 | 'saki', 265 | 'sakura', 266 | 'sakurako', 267 | 'satoko', 268 | 'satomi', 269 | 'satuoru', 270 | 'satashi', 271 | 'sayuri', 272 | 'seiichi', 273 | 'seiji', 274 | 'setsuko', 275 | 'shichiro', 276 | 'shigeko', 277 | 'shika', 278 | 'shin', 279 | 'shinichi', 280 | 'shinobu', 281 | 'shiro', 282 | 'shizuka', 283 | 'shizuko', 284 | 'sho', 285 | 'shoichi', 286 | 'shoji', 287 | 'shouta', 288 | 'shuichi', 289 | 'shuji', 290 | 'shun', 291 | 'sora', 292 | 'souta', 293 | 'sumiko', 294 | 'susumu', 295 | 'suzu', 296 | 'suzume', 297 | 'tadao', 298 | 'tadashi', 299 | 'taiki', 300 | 'takao', 301 | 'takara', 302 | 'taro', 303 | 'tomiko', 304 | 'tomio', 305 | 'toru', 306 | 'tsubame', 307 | 'ume', 308 | 'umeko', 309 | 'usagi', 310 | 'wakana', 311 | 'wendy', 312 | 'yamoto', 313 | 'yasu', 314 | 'yoko', 315 | 'yori', 316 | 'yua', 317 | 'yui', 318 | 'yukio', 319 | 'yumi', 320 | 'yuri', 321 | 'yuu', 322 | 'yuuki', 323 | 'yuuto', 324 | 'yuzuki', 325 | ]; 326 | -------------------------------------------------------------------------------- /back/src/pods/room/baseNames.ts: -------------------------------------------------------------------------------- 1 | export const baseNames = [ 2 | 'fistro', 3 | 'pecador', 4 | 'pradera', 5 | 'nopuedor', 6 | 'jarl', 7 | 'react', 8 | 'curry', 9 | 'hooks', 10 | 'ketchup', 11 | 'turing', 12 | 'alan', 13 | 'chuck', 14 | 'norris', 15 | 'delaware', 16 | 'malaga', 17 | 'palo', 18 | 'valley', 19 | 'lemon', 20 | 'ada', 21 | 'lovelace', 22 | 'allene', 23 | 'virgen', 24 | 'dominque', 25 | 'lesa', 26 | 'brandon', 27 | 'wesley', 28 | 'randee', 29 | 'josue', 30 | 'aisha', 31 | 'marlo', 32 | 'junko', 33 | 'aurea', 34 | 'carroll', 35 | 'melynda', 36 | 'corina', 37 | 'damion', 38 | 'lavonda', 39 | 'susy', 40 | 'vasiliki', 41 | 'malena', 42 | 'carlota', 43 | 'alena', 44 | 'kristal', 45 | 'fe', 46 | 'eartha', 47 | 'raye', 48 | 'maryland', 49 | 'marisela', 50 | 'xuan', 51 | 'yasmin', 52 | 'adena', 53 | 'my', 54 | 'velia', 55 | 'ira', 56 | 'weston', 57 | 'zulma', 58 | 'graham', 59 | 'afton', 60 | 'ashlie', 61 | 'cori', 62 | 'marc', 63 | 'bell', 64 | 'felicitas', 65 | 'shantel', 66 | 'lina', 67 | 'jacob', 68 | 'nikhil', 69 | 'london', 70 | 'damaris', 71 | 'brice', 72 | 'devin', 73 | 'johnathan', 74 | 'grace', 75 | 'alicia', 76 | 'isabelle', 77 | 'glenn', 78 | 'mackenzie', 79 | 'kylie', 80 | 'kathryn', 81 | 'demarcus', 82 | 'luca', 83 | 'miya', 84 | 'adan', 85 | 'martha', 86 | 'maximillian', 87 | 'paityn', 88 | 'jamal', 89 | 'brenda', 90 | 'mariana', 91 | 'kieran', 92 | 'anya', 93 | 'stanley', 94 | 'jordon', 95 | 'leandro', 96 | 'priscilla', 97 | 'terrence', 98 | 'larissa', 99 | 'rhianna', 100 | 'jaime', 101 | 'edward', 102 | 'mckenna', 103 | 'carly', 104 | 'kale', 105 | 'jax', 106 | 'teresa', 107 | 'angela', 108 | 'sophie', 109 | 'ruth', 110 | 'eileen', 111 | 'sidney', 112 | 'belinda', 113 | 'willow', 114 | 'zaniyah', 115 | 'avah', 116 | 'nevaeh', 117 | 'micah', 118 | 'presley', 119 | 'sasha', 120 | 'veronica', 121 | 'brylee', 122 | 'dominic', 123 | 'greyson', 124 | 'ben', 125 | 'luke', 126 | 'phoenix', 127 | 'case', 128 | 'aaliyah', 129 | 'ross', 130 | 'agustin', 131 | 'kailee', 132 | 'janae', 133 | 'frida', 134 | 'isaias', 135 | 'kasey', 136 | 'johan', 137 | 'jordan', 138 | 'colt', 139 | 'dixie', 140 | 'sammy', 141 | 'frankie', 142 | 'mekhi', 143 | 'cassie', 144 | 'kadence', 145 | 'jamiya', 146 | 'vance', 147 | 'colton', 148 | 'aaliyah', 149 | 'ryker', 150 | 'dereon', 151 | 'jabari', 152 | 'jennifer', 153 | 'jayleen', 154 | 'terrence', 155 | 'jaiden', 156 | 'brycen', 157 | 'taliyah', 158 | 'matthew', 159 | 'santos', 160 | 'maximus', 161 | 'luz', 162 | 'melanie', 163 | 'chana', 164 | 'rudy', 165 | 'keith', 166 | 'carlos', 167 | 'jasmine', 168 | 'heidi', 169 | 'cordell', 170 | 'kyan', 171 | 'caitlin', 172 | 'dahlia', 173 | 'reilly', 174 | 'quincy', 175 | 'denise', 176 | 'jackson', 177 | 'luciano', 178 | 'kali', 179 | 'cornelius', 180 | 'jaslyn', 181 | 'timothy', 182 | 'leonidas', 183 | 'ricky', 184 | 'kathryn', 185 | 'landyn', 186 | 'ismael', 187 | 'terry', 188 | 'haven', 189 | 'destinee', 190 | 'landyn', 191 | 'sage', 192 | 'hudson', 193 | 'jadiel', 194 | 'logan', 195 | 'kamora', 196 | 'rene', 197 | 'maya', 198 | 'reid', 199 | 'breanna', 200 | 'keyon', 201 | 'tyrone', 202 | 'dane', 203 | 'carissa', 204 | 'emiliano', 205 | 'gunnar', 206 | 'cortez', 207 | 'lilah', 208 | 'anton', 209 | 'dulce', 210 | 'kaiya', 211 | 'shyann', 212 | 'kamila', 213 | 'tori', 214 | 'tucker', 215 | 'cierra', 216 | 'pierce', 217 | 'king', 218 | 'rachael', 219 | 'maxim', 220 | 'bobby', 221 | 'bryant', 222 | 'kale', 223 | 'jameson', 224 | 'danika', 225 | 'litzy', 226 | 'dillon', 227 | 'tyrese', 228 | 'ezra', 229 | 'duncan', 230 | 'harley', 231 | 'penelope', 232 | 'andrea', 233 | 'charlotte', 234 | 'clara', 235 | 'izabelle', 236 | 'zariah', 237 | 'kristian', 238 | 'sidney', 239 | 'darien', 240 | 'cailyn', 241 | 'yareli', 242 | 'grace', 243 | 'samson', 244 | 'janiyah', 245 | 'mylee', 246 | 'alena', 247 | 'arianna', 248 | 'amy', 249 | 'demetrius', 250 | 'leslie', 251 | 'paulina', 252 | 'hope', 253 | 'delilah', 254 | 'caiden', 255 | 'rodolfo', 256 | 'izayah', 257 | 'jaylin', 258 | 'august', 259 | 'gunnar', 260 | 'kameron', 261 | 'logan', 262 | 'roberto', 263 | 'jazlyn', 264 | 'josh', 265 | 'jason', 266 | 'zavier', 267 | 'freddy', 268 | 'hayley', 269 | 'martha', 270 | 'todd', 271 | 'yazmin', 272 | 'lyla', 273 | 'alani', 274 | 'joe', 275 | 'gillian', 276 | 'boston', 277 | 'jaidyn', 278 | 'erin', 279 | 'skyla', 280 | 'nicholas', 281 | 'albert', 282 | 'juliet', 283 | 'kate', 284 | 'susan', 285 | 'preston', 286 | 'alvaro', 287 | 'kymani', 288 | 'leandro', 289 | 'eliezer', 290 | 'aleah', 291 | 'dayton', 292 | 'marilyn', 293 | 'jax', 294 | 'jonah', 295 | 'ada', 296 | 'karma', 297 | 'maliyah', 298 | 'isaac', 299 | 'jessie', 300 | 'elijah', 301 | 'jaxson', 302 | 'kristian', 303 | 'vance', 304 | 'jaquan', 305 | 'lyric', 306 | 'clarissa', 307 | 'kianna', 308 | 'matteo', 309 | 'marina', 310 | 'alisa', 311 | 'caitlin', 312 | 'tiffany', 313 | 'cristal', 314 | 'vincent', 315 | 'jayvon', 316 | 'greta', 317 | 'neveah', 318 | 'kamron', 319 | 'marely', 320 | 'titus', 321 | 'hugo', 322 | 'kayla', 323 | 'quincy', 324 | 'elijah', 325 | 'ezra', 326 | 'brody', 327 | 'fabian', 328 | 'agustin', 329 | 'lilian', 330 | 'alfredo', 331 | 'jaidyn', 332 | 'olive', 333 | 'kellen', 334 | 'caroline', 335 | 'ramiro', 336 | 'kael', 337 | 'aspen', 338 | 'stephanie', 339 | 'nyasia', 340 | 'jessie', 341 | 'lilyana', 342 | 'rowan', 343 | 'angelina', 344 | 'laci', 345 | 'greta', 346 | 'steven', 347 | 'kody', 348 | 'leon', 349 | 'jimena', 350 | 'tyler', 351 | 'livia', 352 | 'elias', 353 | 'zayne', 354 | 'maren', 355 | 'todd', 356 | 'julianna', 357 | 'sydnee', 358 | 'adriel', 359 | 'gerald', 360 | 'niko', 361 | 'rory', 362 | 'isabell', 363 | 'violet', 364 | 'logan', 365 | 'daisy', 366 | ]; 367 | -------------------------------------------------------------------------------- /front/src/assets/lemoncode-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | 18 | 20 | 23 | 29 | 31 | 32 | 33 | 38 | 44 | 47 | 49 | 50 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /front/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/static/assets/sprite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/static/styles.css: -------------------------------------------------------------------------------- 1 | /* - - - basic - - - */ 2 | *, 3 | *::before, 4 | *::after { 5 | margin: 0; 6 | padding: 0; 7 | box-sizing: inherit; 8 | } 9 | 10 | html { 11 | /* custom properties */ 12 | --font-default: 'Roboto', sans-serif; 13 | --color-bg: #ffffff; 14 | --color-light: #ffffff; 15 | --color-dark: #2e2800; 16 | --color-grey-light: #eee; 17 | --color-grey-medium: #ccc; 18 | --color-primary-main: #d9d900; 19 | --color-alert: rgb(255, 87, 51); 20 | 21 | box-sizing: border-box; 22 | font-size: 62.4%; 23 | font-family: var(--font-default); 24 | } 25 | 26 | body { 27 | position: absolute; 28 | font-size: 1.6rem; 29 | background-color: var(--color-bg); 30 | color: var(--color-dark); 31 | min-height: 100vh; 32 | } 33 | 34 | img { 35 | max-width: 100%; 36 | } 37 | 38 | a { 39 | text-decoration: none; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | /* - - - common - - - */ 47 | .box-content { 48 | display: flex; 49 | justify-content: center; 50 | height: max-content; 51 | padding: 12rem 0; 52 | align-items: center; 53 | } 54 | 55 | .border-bottom { 56 | border-bottom: 1px solid var(--color-grey-medium); 57 | } 58 | 59 | .heading { 60 | width: max-content; 61 | text-align: center; 62 | font-weight: 300; 63 | font-size: 3rem; 64 | margin: 0 auto 4rem auto; 65 | text-transform: uppercase; 66 | border-bottom: 2px solid var(--color-primary-main); 67 | } 68 | 69 | .container { 70 | width: 70%; 71 | margin: 0 auto 8rem auto; 72 | padding: 3rem; 73 | background-color: var(--color-grey-light); 74 | } 75 | 76 | @media only screen and (max-width: 770px) { 77 | .container { 78 | width: 100%; 79 | } 80 | } 81 | 82 | .container p, 83 | .container h3 { 84 | margin-bottom: 1.5rem; 85 | } 86 | 87 | /* - - - - - navbar - - - - - */ 88 | .navbar { 89 | position: fixed; 90 | display: flex; 91 | align-items: center; 92 | width: 100%; 93 | height: 8.6rem; 94 | z-index: 1000; 95 | border-bottom: 2px solid var(--color-dark); 96 | background-image: linear-gradient( 97 | 60deg, 98 | var(--color-light) 25rem, 99 | var(--color-primary-main) 25rem, 100 | var(--color-light) 90% 101 | ); 102 | } 103 | 104 | .navbar__logo { 105 | min-height: 6rem; 106 | max-width: 17.5rem; 107 | margin-top: 0.6rem; 108 | margin-left: 3.8rem; 109 | fill: var(--color-dark); 110 | } 111 | 112 | .navbar__list { 113 | display: flex; 114 | margin-left: auto; 115 | margin-right: none; 116 | justify-content: space-between; 117 | align-items: center; 118 | } 119 | 120 | .navbar__list-container { 121 | display: flex; 122 | margin-left: auto; 123 | } 124 | 125 | .navbar__list__item { 126 | margin-right: 2.5rem; 127 | } 128 | 129 | .navbar__cta-button { 130 | display: flex; 131 | align-items: center; 132 | padding: 1rem 1.5rem; 133 | margin-right: 3rem; 134 | font-size: 1.7rem; 135 | font-weight: 300; 136 | text-transform: capitalize; 137 | letter-spacing: 1px; 138 | color: var(--color-alert); 139 | background-color: transparent; 140 | border: 2px solid var(--color-alert); 141 | transition: all 0.4s; 142 | -webkit-transition: all 0.4s; 143 | } 144 | 145 | .navbar__cta-button:hover, 146 | .navbar__cta-button:active { 147 | cursor: pointer; 148 | color: var(--color-light); 149 | background-color: var(--color-alert); 150 | outline: none; 151 | } 152 | 153 | .navbar__cta-button__icon { 154 | width: 2.5rem; 155 | height: 2.5rem; 156 | margin-left: 1rem; 157 | fill: var(--color-alert); 158 | transition: all 0.4s; 159 | -webkit-transition: all 0.4s; 160 | } 161 | 162 | .navbar__cta-button:hover .navbar__cta-button__icon, 163 | .navbar__cta-button:active .navbar__cta-button__icon { 164 | fill: var(--color-light); 165 | } 166 | 167 | .navbar__list__item a { 168 | position: relative; 169 | display: inline-block; 170 | margin-top: 0.5rem; 171 | padding-bottom: 0.5rem; 172 | font-size: 1.7rem; 173 | text-decoration: none; 174 | color: var(--color-dark); 175 | border-bottom: 2px solid transparent; 176 | transition: all 0.3s ease; 177 | -webkit-transition: all 0.3s ease; 178 | } 179 | 180 | .navbar__list__item a:hover { 181 | border-bottom: 2px solid var(--color-alert); 182 | color: var(--color-alert); 183 | } 184 | 185 | .navbar__list__item:last-of-type a { 186 | margin-right: 1.8rem; 187 | } 188 | 189 | @media only screen and (max-width: 770px) { 190 | .navbar { 191 | background-image: none; 192 | background-color: var(--color-light); 193 | } 194 | } 195 | 196 | @media only screen and (max-width: 728px) { 197 | .navbar { 198 | flex-direction: column; 199 | align-items: center; 200 | justify-content: center; 201 | align-content: center; 202 | height: auto; 203 | } 204 | 205 | .navbar__logo { 206 | height: 3rem; 207 | margin-left: 0; 208 | margin-top: 2rem; 209 | margin-bottom: 2rem; 210 | } 211 | 212 | .navbar__list-container { 213 | margin-left: 0; 214 | margin-bottom: 2rem; 215 | } 216 | 217 | .navbar__list { 218 | margin-right: auto; 219 | } 220 | 221 | .navbar__cta-button { 222 | display: inline-block; 223 | display: flex; 224 | justify-content: center; 225 | align-items: center; 226 | padding: 0.8rem 1.2rem; 227 | color: var(--color-light); 228 | background-color: var(--color-alert); 229 | border: none; 230 | } 231 | 232 | .navbar__cta-button:hover, 233 | .navbar__cta-button:active { 234 | background-color: var(--color-dark); 235 | } 236 | 237 | .navbar__cta-button__icon { 238 | fill: var(--color-light); 239 | } 240 | 241 | .navbar__list__item:last-of-type { 242 | margin-right: 0; 243 | } 244 | 245 | .navbar__list__item:last-of-type a { 246 | margin-right: 0; 247 | } 248 | } 249 | 250 | @media only screen and (max-width: 480px) { 251 | .navbar__list-container { 252 | flex-direction: column-reverse; 253 | } 254 | 255 | .navbar__list { 256 | margin-bottom: 2rem; 257 | } 258 | 259 | .navbar__cta-button { 260 | margin-right: 0; 261 | width: 17rem; 262 | margin: 0 auto; 263 | } 264 | } 265 | 266 | /* - - - - - top - - - - - */ 267 | .top-container { 268 | display: flex; 269 | align-items: center; 270 | min-height: calc(100vh - 8.7rem); 271 | margin-top: 8.8rem; 272 | } 273 | 274 | @media only screen and (max-width: 1250px) { 275 | .top-container { 276 | height: max-content; 277 | margin-top: 14rem; 278 | padding-bottom: 12rem; 279 | } 280 | } 281 | 282 | @media only screen and (max-width: 728px) { 283 | .top-container { 284 | margin-top: 6rem; 285 | padding-bottom: 0rem; 286 | } 287 | } 288 | 289 | /* - - - - - top - main button - - - - -*/ 290 | .cta-button-container { 291 | display: flex; 292 | justify-content: center; 293 | align-self: center; 294 | } 295 | 296 | .cta-button { 297 | display: flex; 298 | align-items: center; 299 | padding: 2.5rem 3.5rem; 300 | font-size: 2.2rem; 301 | font-weight: 300; 302 | text-transform: capitalize; 303 | letter-spacing: 1px; 304 | color: var(--color-alert); 305 | border: 2px solid var(--color-alert); 306 | background-color: transparent; 307 | transition: all 0.4s; 308 | -webkit-transition: all 0.4s; 309 | } 310 | 311 | .cta-button:hover, 312 | .cta-button:active { 313 | cursor: pointer; 314 | color: var(--color-light); 315 | background-color: var(--color-alert); 316 | outline: none; 317 | } 318 | 319 | .cta-button__icon { 320 | width: 3rem; 321 | height: 3rem; 322 | margin-left: 1rem; 323 | fill: var(--color-alert); 324 | transition: all 0.4s; 325 | -webkit-transition: all 0.4s; 326 | } 327 | 328 | .cta-button:hover .cta-button__icon, 329 | .cta-button:active .cta-button__icon { 330 | fill: var(--color-light); 331 | } 332 | 333 | @media only screen and (max-width: 1250px) { 334 | .cta-button-container { 335 | display: none; 336 | } 337 | .cta-button { 338 | display: none; 339 | } 340 | } 341 | 342 | /* - - - - - top - instructions - - - - - */ 343 | .instructions-container { 344 | display: flex; 345 | justify-content: space-between; 346 | width: 70%; 347 | margin: 0 auto; 348 | } 349 | 350 | .instructions { 351 | width: 65%; 352 | margin-left: auto; 353 | display: block; 354 | padding: 3rem; 355 | border: 2px solid var(--color-grey-medium); 356 | } 357 | 358 | .instructions__title { 359 | color: var(--color-dark); 360 | } 361 | 362 | .instructions__subtitle { 363 | margin-top: 2rem; 364 | margin-bottom: 1rem; 365 | } 366 | 367 | .instructions__list { 368 | padding-left: 2rem; 369 | } 370 | 371 | .instructions__text { 372 | margin-bottom: 1rem; 373 | } 374 | 375 | .video-container { 376 | display: inline-flex; 377 | justify-content: center; 378 | align-content: center; 379 | width: 100%; 380 | margin-top: 4%; 381 | padding: 0; 382 | } 383 | 384 | .video { 385 | width: 95%; 386 | max-height: 220px; 387 | padding: 0; 388 | background-color: #151611; 389 | border-radius: 5px; 390 | } 391 | 392 | @media (min-width: 1024px) { 393 | .video { 394 | max-width: 400px; 395 | 396 | max-height: 300px; 397 | } 398 | } 399 | 400 | @media only screen and (max-width: 1250px) { 401 | .instructions-container { 402 | width: 70%; 403 | flex-direction: column; 404 | justify-content: center; 405 | align-items: center; 406 | align-content: center; 407 | } 408 | .instructions { 409 | width: 100%; 410 | margin-right: 0; 411 | margin-top: 4rem; 412 | } 413 | } 414 | 415 | @media only screen and (max-width: 900px) { 416 | .instructions-container { 417 | width: 80%; 418 | } 419 | } 420 | 421 | @media only screen and (max-width: 770px) { 422 | .instructions-container { 423 | width: 100%; 424 | } 425 | 426 | .instructions { 427 | border: none; 428 | border-top: 2px solid var(--color-grey-medium); 429 | border-bottom: 2px solid var(--color-grey-medium); 430 | } 431 | } 432 | 433 | @media only screen and (max-width: 728px) { 434 | .instructions { 435 | border: none; 436 | } 437 | } 438 | 439 | @media only screen and (max-width: 578px) { 440 | .instructions-container { 441 | padding-top: 8rem; 442 | } 443 | } 444 | 445 | @media only screen and (max-width: 480px) { 446 | .instructions-container { 447 | padding-top: 12rem; 448 | } 449 | } 450 | 451 | /* - - - - - credits - - - - - */ 452 | .card { 453 | display: flex; 454 | align-items: center; 455 | justify-content: space-between; 456 | margin-bottom: 2rem; 457 | } 458 | 459 | .card:last-of-type { 460 | margin-bottom: 0; 461 | } 462 | 463 | .card__img { 464 | width: 10rem; 465 | height: 10rem; 466 | overflow: hidden; 467 | display: flex; 468 | justify-content: center; 469 | align-content: center; 470 | } 471 | .avatar { 472 | border-radius: 50%; 473 | border: 2px solid var(--color-dark); 474 | max-width: 100%; 475 | max-height: 100%; 476 | } 477 | 478 | .card__text { 479 | flex: 1; 480 | margin-left: 2.5rem; 481 | } 482 | 483 | .divider { 484 | display: block; 485 | height: 1px; 486 | margin-bottom: 3.5rem; 487 | padding: 0; 488 | border: 0; 489 | border-top: 1px solid var(--color-dark); 490 | } 491 | 492 | @media only screen and (max-width: 578px) { 493 | .card { 494 | flex-direction: column; 495 | } 496 | 497 | .card__img { 498 | margin-bottom: 2rem; 499 | } 500 | 501 | .card__text { 502 | margin-left: 0; 503 | } 504 | 505 | .card__text h3 { 506 | text-align: center; 507 | } 508 | } 509 | 510 | /* - - - - - contact - - - - - */ 511 | .contact-container { 512 | width: 100%; 513 | } 514 | .input-field { 515 | display: flex; 516 | flex-direction: column; 517 | width: 100%; 518 | } 519 | 520 | .input-field label { 521 | margin-bottom: 1rem; 522 | font-size: 1.8rem; 523 | } 524 | 525 | .input-field input { 526 | margin-bottom: 3rem; 527 | padding: 1rem; 528 | font-size: 1.6rem; 529 | font-family: var(--font-default); 530 | outline: none; 531 | border: none; 532 | } 533 | 534 | form textarea { 535 | width: 100%; 536 | margin-bottom: 3rem; 537 | padding: 1rem; 538 | font-size: 1.6rem; 539 | font-family: var(--font-default); 540 | outline: none; 541 | resize: none; 542 | border: none; 543 | } 544 | 545 | .btn-submit { 546 | width: 100%; 547 | padding: 2rem 0; 548 | font-size: 1.8rem; 549 | font-weight: 300; 550 | text-transform: capitalize; 551 | letter-spacing: 1px; 552 | opacity: 1; 553 | color: var(--color-light); 554 | background-color: var(--color-dark); 555 | outline: none; 556 | border: none; 557 | transition: all 0.4; 558 | -webkit-transition: all 0.4; 559 | } 560 | 561 | .btn-submit:hover, 562 | .btn-submit:active { 563 | cursor: pointer; 564 | opacity: 0.8; 565 | } 566 | 567 | /* - - - footer - - - */ 568 | .footer { 569 | display: flex; 570 | flex-direction: column; 571 | justify-content: space-between; 572 | height: 30rem; 573 | padding: 2rem 10rem; 574 | background-color: var(--color-dark); 575 | } 576 | 577 | @media only screen and (max-width: 728px) { 578 | .footer { 579 | padding: 2rem 4rem 3rem 4rem; 580 | } 581 | } 582 | 583 | @media only screen and (max-width: 380px) { 584 | .footer { 585 | height: 35rem; 586 | } 587 | } 588 | 589 | .footer__top-container { 590 | padding: 0 2rem; 591 | display: flex; 592 | justify-content: space-between; 593 | height: 19.5rem; 594 | align-items: center; 595 | } 596 | 597 | @media only screen and (max-width: 380px) { 598 | .footer__top-container { 599 | flex-direction: column-reverse; 600 | align-items: center; 601 | justify-content: center; 602 | height: 22rem; 603 | } 604 | } 605 | 606 | .footer__logo { 607 | height: 10rem; 608 | width: 11.2rem; 609 | } 610 | 611 | .footer__list { 612 | padding: 0; 613 | margin: 0; 614 | list-style: none; 615 | } 616 | 617 | @media only screen and (max-width: 380px) { 618 | .footer__list { 619 | text-align: center; 620 | margin-top: 2.4rem; 621 | } 622 | } 623 | 624 | .footer__list__item { 625 | margin-top: 0.5rem; 626 | } 627 | 628 | .footer__list__item a { 629 | font-size: 1.7rem; 630 | color: var(--color-grey-light); 631 | text-decoration: none; 632 | font-weight: 300; 633 | } 634 | 635 | .footer__list__item a:hover { 636 | color: var(--color-primary-main); 637 | } 638 | 639 | .footer__bottom-container { 640 | display: flex; 641 | justify-content: space-between; 642 | align-items: center; 643 | border-top: 1px solid #d9d900; 644 | padding-top: 1rem; 645 | } 646 | 647 | @media only screen and (max-width: 380px) { 648 | .footer__bottom-container { 649 | flex-direction: column; 650 | align-items: center; 651 | } 652 | } 653 | 654 | .footer--logo-lemon-container:hover .footer--logo-lemon { 655 | fill: var(--color-secondary-main); 656 | } 657 | 658 | .footer--logo-lemon { 659 | position: absolute; 660 | top: 0.8rem; 661 | right: 1.6rem; 662 | fill: var(--color-light); 663 | height: 10rem; 664 | width: 10rem; 665 | } 666 | 667 | .footer__icon-list { 668 | display: flex; 669 | align-items: center; 670 | padding: 0; 671 | margin: 0; 672 | list-style: none; 673 | } 674 | 675 | .footer__icon-list-item { 676 | margin-right: 1.2rem; 677 | } 678 | .footer__icon-list-item:last-of-type { 679 | margin-right: 0; 680 | } 681 | 682 | .footer__icon { 683 | height: 2.4rem; 684 | width: 2.4rem; 685 | fill: var(--color-grey-light); 686 | } 687 | 688 | .footer__icon:hover { 689 | fill: var(--color-primary-main); 690 | } 691 | 692 | .footer__icon-twitter { 693 | height: 3.5rem; 694 | width: 3.5rem; 695 | margin-top: 0.2rem; 696 | margin-right: -0.6rem; 697 | margin-left: -0.6rem; 698 | fill: var(--color-grey-light); 699 | } 700 | 701 | .footer__icon-twitter:hover { 702 | fill: var(--color-primary-main); 703 | } 704 | 705 | .footer__copyright { 706 | color: var(--color-grey-light); 707 | font-weight: 300; 708 | font-size: 1.4rem; 709 | } 710 | 711 | @media only screen and (max-width: 578px) { 712 | .footer__copyright { 713 | margin-top: 0.4rem; 714 | } 715 | } 716 | --------------------------------------------------------------------------------