├── 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 |
4 |
11 |
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 |
handleCreateSession()}
30 | className={createSessionBtn}
31 | >
32 | Create Session
33 |
34 |
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 |
23 |
24 |
25 |
26 |
27 |
49 |
© 2020 Lemoncode
50 |
51 |
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 |
41 | Content
42 |
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 |
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 |
62 | {labelName}
63 | {open ? (
64 |
65 | ) : (
66 |
67 | )}
68 |
69 |
70 |
71 |
72 |
80 | navigator.clipboard.writeText(urlLink)}
84 | >
85 |
86 |
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 |
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 |
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 | code-paster-logo
--------------------------------------------------------------------------------
/front/static/assets/sprite.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | codepaster-logo
4 |
5 |
6 |
7 | lemoncode-logo
8 |
9 |
10 |
11 |
12 |
13 |
14 | github-icon
15 |
16 |
17 |
18 |
19 | website-icon
20 |
21 |
22 |
23 |
27 |
28 | mail-icon
29 |
30 |
31 |
32 |
33 | arrow-icon
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------