├── .github └── ISSUE_TEMPLATE │ └── custom.md ├── .gitignore ├── README.md ├── api ├── .dockerignore ├── .env.dist ├── .gitignore ├── .prettierrc ├── Dockerfile ├── Procfile ├── docker-compose.yml ├── nest-cli.json ├── package.json ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── github-app.strategy.ts │ │ ├── github.strategy.ts │ │ └── jwt.strategy.ts │ ├── config │ │ ├── config.module.ts │ │ └── config.service.ts │ ├── cron │ │ ├── cron.module.ts │ │ ├── cron.service.spec.ts │ │ └── cron.service.ts │ ├── github │ │ ├── github.module.ts │ │ └── github.service.ts │ ├── log │ │ ├── log.entity.ts │ │ ├── log.module.ts │ │ └── log.service.ts │ ├── main.ts │ ├── orm.module.ts │ ├── pull-request │ │ ├── pull-request.entity.ts │ │ ├── pull-request.module.ts │ │ └── pull-request.service.ts │ ├── queue │ │ ├── dependencies.queue.ts │ │ ├── queue.module.ts │ │ ├── worker.module.ts │ │ └── worker.ts │ ├── repository │ │ ├── repository.controller.ts │ │ ├── repository.entity.ts │ │ ├── repository.module.ts │ │ └── repository.service.ts │ ├── users │ │ ├── user.entity.ts │ │ ├── users.controller.ts │ │ ├── users.module.ts │ │ └── users.service.ts │ ├── utils │ │ ├── dependencies.spec.ts │ │ └── dependencies.ts │ └── webhooks │ │ ├── webhooks.controller.ts │ │ ├── webhooks.interceptor.ts │ │ └── webhooks.module.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tmp │ └── .gitkeep ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json └── yarn.lock ├── app ├── .buckconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── __tests__ │ └── App-test.tsx ├── android │ ├── app │ │ ├── _BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── app │ │ │ │ └── ReactNativeFlipper.java │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── app │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── index.js ├── ios │ ├── Podfile │ ├── Podfile.lock │ ├── app-tvOS │ │ └── Info.plist │ ├── app-tvOSTests │ │ └── Info.plist │ ├── app.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── app-tvOS.xcscheme │ │ │ └── app.xcscheme │ ├── app.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── app │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m │ └── appTests │ │ ├── Info.plist │ │ └── appTests.m ├── metro.config.js ├── package.json ├── src │ ├── App.tsx │ ├── components │ │ ├── Header.tsx │ │ ├── Repository │ │ │ ├── FrameworkTag.tsx │ │ │ └── RepoItem.tsx │ │ ├── Row.tsx │ │ └── Ui │ │ │ ├── LoadingIndicator.tsx │ │ │ ├── MainBackground.tsx │ │ │ └── _StatusBar.tsx │ ├── config │ │ ├── .gitignore │ │ ├── index.dev.ts │ │ └── index.ts │ ├── containers │ │ ├── Dashboard.tsx │ │ ├── GithubRedirect.tsx │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ └── Splash.tsx │ ├── contexts │ │ └── AuthContext.tsx │ ├── hooks │ │ └── useRequest.ts │ ├── navigators │ │ └── AppStack.tsx │ ├── services │ │ ├── api.ts │ │ ├── apiAuth.ts │ │ ├── apiGithub.ts │ │ └── apiRepositories.ts │ ├── theme │ │ └── index.tsx │ ├── typings │ │ └── index.d.ts │ └── utils │ │ └── AsyncStorage.ts ├── tsconfig.json └── yarn.lock ├── front ├── .babelrc ├── .env.dist ├── .gitignore ├── .prettierrc ├── config-overrides.js ├── netlify.toml ├── package.json ├── public │ ├── _redirects │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── app.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── dep.png │ ├── dumbbell.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── feat-improvement.png │ ├── feat-pr.png │ ├── feat-repo.png │ ├── hero.svg │ ├── images │ │ └── og-graph-color.png │ ├── index.html │ ├── items.png │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── mstile-150x150.png │ ├── p4.png │ ├── ram.svg │ ├── robots.txt │ ├── site.webmanifest │ └── update.png ├── src │ ├── App.tsx │ ├── api │ │ ├── api.ts │ │ ├── auth.ts │ │ ├── github.ts │ │ ├── installations.ts │ │ ├── repositories.ts │ │ └── user.ts │ ├── components │ │ ├── AppBar.tsx │ │ ├── AppHeader.tsx │ │ ├── Container.tsx │ │ ├── Demo │ │ │ ├── AppHeaderDemo.tsx │ │ │ ├── DemoItem.tsx │ │ │ └── model.ts │ │ ├── DependenciesList │ │ │ ├── DependenciesList.tsx │ │ │ ├── DependencyItem.tsx │ │ │ └── index.ts │ │ ├── DependencyTabs.tsx │ │ ├── FirstAppButton.tsx │ │ ├── Flex │ │ │ ├── Flex.tsx │ │ │ └── index.ts │ │ ├── FormInput │ │ │ ├── FormInput.tsx │ │ │ └── index.ts │ │ ├── FrameworkTag │ │ │ └── FrameworkTag.tsx │ │ ├── Header │ │ │ ├── Header.tsx │ │ │ ├── HeaderLinks.tsx │ │ │ └── index.ts │ │ ├── InstallationRepositories │ │ │ ├── InstallationRepositories.tsx │ │ │ └── index.ts │ │ ├── InstallationRepositoriesList │ │ │ ├── InstallationRepositoriesList.tsx │ │ │ ├── InstallationRepositoriesListItem.tsx │ │ │ └── index.ts │ │ ├── LoadBar.tsx │ │ ├── LoadScore.tsx │ │ ├── OrganizationsMenuList │ │ │ ├── OrganizationMenuItem.tsx │ │ │ ├── OrganizationsMenuList.tsx │ │ │ └── index.ts │ │ ├── PrivateRoute │ │ │ ├── PrivateRoute.tsx │ │ │ └── index.ts │ │ ├── PullRequest │ │ │ └── PullRequestItem.tsx │ │ ├── RepoConfigForm │ │ │ ├── RepoConfigForm.tsx │ │ │ └── index.ts │ │ ├── RepositoriesList │ │ │ ├── RepositoriesList.tsx │ │ │ ├── RepositoryListEmpty.tsx │ │ │ ├── RepositoryListItem.tsx │ │ │ └── index.ts │ │ ├── Screenshot.tsx │ │ └── SelectMenu │ │ │ ├── SelectMenu.tsx │ │ │ └── index.ts │ ├── containers │ │ ├── Dashboard.tsx │ │ ├── DashboardSkeleton.tsx │ │ ├── Demo.tsx │ │ ├── GithubRedirect.tsx │ │ ├── Home.tsx │ │ ├── Layout │ │ │ └── AuthApp.tsx │ │ ├── Repository │ │ │ ├── AddRepo.tsx │ │ │ ├── RepositoryLayout.tsx │ │ │ ├── ViewPullRequest.tsx │ │ │ ├── ViewRepo.tsx │ │ │ └── ViewRepoSkeleton.tsx │ │ ├── Router.tsx │ │ ├── Settings.tsx │ │ └── WaitingBeta.tsx │ ├── contexts │ │ ├── AuthContext.tsx │ │ ├── DependenciesContext.tsx │ │ └── RepositoryContext.tsx │ ├── hooks │ │ ├── useChakraToast.ts │ │ ├── useMessageListener.ts │ │ └── useRequest.ts │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── theme │ │ └── index.ts │ ├── typings │ │ ├── entities.d.ts │ │ └── index.d.ts │ └── utils │ │ └── dependencies.ts ├── tsconfig.json ├── tsconfig.paths.json └── yarn.lock └── reactivated-app.gif /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## 🎯 But 10 | 11 | Description 12 | 13 | ## 👣 Steps 14 | 15 | - todo 1 16 | - todo 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .git -------------------------------------------------------------------------------- /api/.env.dist: -------------------------------------------------------------------------------- 1 | TYPEORM_CONNECTION=mysql 2 | TYPEORM_HOST=localhost 3 | TYPEORM_USERNAME=root 4 | TYPEORM_PASSWORD=secret 5 | TYPEORM_DATABASE=reactivated 6 | TYPEORM_PORT=3306 7 | TYPEORM_SYNCHRONIZE=true 8 | 9 | CLIENT_ID= 10 | CLIENT_SECRET= 11 | CALLBACK_URL=http://localhost:3007/redirect 12 | GITHUB_WEBHOOK_SECRET= 13 | 14 | APP_CLIENT_ID= 15 | APP_CLIENT_SECRET= 16 | APP_CALLBACK_URL=reactivatedapp:/redirect 17 | 18 | REDIS_URL=redis://127.0.0.1 19 | MAX_JOBS_NUMBER=5 20 | WEB_CONCURRENCY=2 21 | MAX_REPOS=5 22 | IS_BETA=false 23 | 24 | SLACK_BETA_URL= -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /tmp/* 5 | !/tmp/.gitkeep 6 | 7 | # Env 8 | .env.dev 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | # OS 19 | .DS_Store 20 | 21 | # Tests 22 | /coverage 23 | /.nyc_output 24 | 25 | # IDEs and editors 26 | /.idea 27 | .project 28 | .classpath 29 | .c9/ 30 | *.launch 31 | .settings/ 32 | *.sublime-workspace 33 | 34 | # IDE - VSCode 35 | .vscode/* 36 | !.vscode/settings.json 37 | !.vscode/tasks.json 38 | !.vscode/launch.json 39 | !.vscode/extensions.json -------------------------------------------------------------------------------- /api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY package.json yarn.lock ./ 5 | COPY . . 6 | RUN yarn 7 | EXPOSE 3000 3306 6379 8 | CMD [ "yarn", "start:dev" ] 9 | -------------------------------------------------------------------------------- /api/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start:prod 2 | worker: npm run start:worker -------------------------------------------------------------------------------- /api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | mysql: 5 | image: mysql:5.7.10 6 | restart: always 7 | environment: 8 | MYSQL_USER: admin 9 | MYSQL_PASSWORD: password 10 | MYSQL_ROOT_PASSWORD: password 11 | MYSQL_DATABASE: reactivated 12 | ports: 13 | - 3306:3306 14 | volumes: 15 | - reactivated-db:/var/lib/mysql 16 | 17 | redis: 18 | image: redis:5-alpine 19 | ports: 20 | - 6379:6379 21 | volumes: 22 | - reactivated-redis:/data 23 | 24 | api: 25 | build: 26 | context: . 27 | dockerfile: Dockerfile 28 | volumes: 29 | - ./:/usr/src/app 30 | - /usr/src/app/node_modules 31 | depends_on: 32 | - mysql 33 | ports: 34 | - 3000:3000 35 | links: 36 | - mysql:mysql 37 | 38 | volumes: 39 | reactivated-db: 40 | reactivated-redis: 41 | -------------------------------------------------------------------------------- /api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "plugins": ["@nestjs/swagger/plugin"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "NODE_ENV=prod yarn prebuild && tsc -p tsconfig.build.json", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "smee": "smee --url https://smee.io/BVk7Sqmgj7fXXcV --path /webhooks/consume --port 3000", 13 | "start:dev": "NODE_ENV=dev nest start --watch", 14 | "start:worker": "node -r ts-node/register src/queue/worker.ts", 15 | "start:debug": "nest start --debug --watch", 16 | "start:prod": "node dist/main", 17 | "lint": "tslint -p tsconfig.json -c tslint.json", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json", 23 | "dev": "ts-node-dev --ignore-watch node_modules ./src/main.ts", 24 | "postinstall": "yarn build" 25 | }, 26 | "dependencies": { 27 | "@nestjs/bull": "^0.1.1", 28 | "@nestjs/common": "^7.1.3", 29 | "@nestjs/core": "^7.1.3", 30 | "@nestjs/jwt": "^7.0.0", 31 | "@nestjs/passport": "^7.0.0", 32 | "@nestjs/platform-express": "^7.1.3", 33 | "@nestjs/schedule": "^0.4.0", 34 | "@nestjs/swagger": "^4.0.0", 35 | "@nestjs/typeorm": "^7.1.0", 36 | "@nestjsx/crud": "^4.6.2", 37 | "@nestjsx/crud-typeorm": "^4.6.2", 38 | "@types/bull": "^3.13.0", 39 | "bull": "^3.12.1", 40 | "class-transformer": "^0.2.3", 41 | "class-validator": "^0.11.0", 42 | "crypto-js": "^3.1.9-1", 43 | "mysql": "^2.17.1", 44 | "passport": "^0.4.0", 45 | "passport-github": "^1.1.0", 46 | "passport-jwt": "^4.0.0", 47 | "passport-local": "^1.0.0", 48 | "pg": "^7.12.1", 49 | "reflect-metadata": "^0.1.13", 50 | "rimraf": "^3.0.0", 51 | "rxjs": "^6.5.3", 52 | "secure-compare": "^3.0.1", 53 | "swagger-ui-express": "^4.1.2", 54 | "throng": "^4.0.0", 55 | "ts-node-dev": "^1.0.0-pre.44", 56 | "typeorm": "^0.2.20" 57 | }, 58 | "devDependencies": { 59 | "@nestjs/cli": "^6.9.0", 60 | "@nestjs/schematics": "^6.7.0", 61 | "@nestjs/testing": "^6.7.1", 62 | "@types/express": "^4.17.1", 63 | "@types/jest": "^24.0.18", 64 | "@types/node": "^12.7.5", 65 | "@types/supertest": "^2.0.8", 66 | "jest": "^24.9.0", 67 | "prettier": "^1.18.2", 68 | "supertest": "^4.0.2", 69 | "ts-jest": "^24.1.0", 70 | "ts-loader": "^6.1.1", 71 | "ts-node": "^8.4.1", 72 | "tsconfig-paths": "^3.9.0", 73 | "tslint": "^5.20.0", 74 | "typescript": "^3.6.3" 75 | }, 76 | "jest": { 77 | "moduleFileExtensions": [ 78 | "js", 79 | "json", 80 | "ts" 81 | ], 82 | "rootDir": "src", 83 | "testRegex": ".spec.ts$", 84 | "transform": { 85 | "^.+\\.(t|j)s$": "ts-jest" 86 | }, 87 | "coverageDirectory": "./coverage", 88 | "testEnvironment": "node" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /api/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /api/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards, Request } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AuthService } from './auth/auth.service'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | import { User } from './users/user.entity'; 6 | 7 | @Controller() 8 | export class AppController { 9 | constructor(private readonly authService: AuthService) {} 10 | 11 | @ApiTags('auth') 12 | @UseGuards(AuthGuard('github')) 13 | @Get('/auth/github') 14 | async login(@Request() req) { 15 | return {}; 16 | } 17 | 18 | @ApiTags('auth') 19 | @UseGuards(AuthGuard('github-app')) 20 | @Get('app/auth/github') 21 | async appLogin(@Request() req) { 22 | return {}; 23 | } 24 | 25 | @ApiTags('auth') 26 | @UseGuards(AuthGuard('github')) 27 | @Get('/auth/github/callback') 28 | async redirect(@Request() req) { 29 | const { 30 | githubToken, 31 | username, 32 | githubId, 33 | id, 34 | validated, 35 | avatarUrl, 36 | }: User = req.user; 37 | const jwt = this.authService.createToken({ 38 | githubToken: githubToken, 39 | userName: username, 40 | githubId: githubId, 41 | userId: id, 42 | validated, 43 | avatarUrl, 44 | }); 45 | 46 | return { token: jwt }; 47 | } 48 | 49 | @ApiTags('auth') 50 | @UseGuards(AuthGuard('github-app')) 51 | @Get('app/auth/github/callback') 52 | async appRedirect(@Request() req) { 53 | const { githubToken, username, githubId, id } = req.user; 54 | const jwt = this.authService.createToken({ 55 | githubToken: githubToken, 56 | userName: username, 57 | githubId: githubId, 58 | userId: id, 59 | }); 60 | 61 | return { token: jwt }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScheduleModule } from '@nestjs/schedule'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { ConfigModule } from './config/config.module'; 7 | import { ConfigService } from './config/config.service'; 8 | import { CronModule } from './cron/cron.module'; 9 | import { GithubModule } from './github/github.module'; 10 | import { OrmModule } from './orm.module'; 11 | import { PullRequestModule } from './pull-request/pull-request.module'; 12 | import { RepositoryModule } from './repository/repository.module'; 13 | import { UsersModule } from './users/users.module'; 14 | import { WebhooksModule } from './webhooks/webhooks.module'; 15 | import { BullModule } from '@nestjs/bull'; 16 | import { LogModule } from './log/log.module'; 17 | @Module({ 18 | imports: [ 19 | UsersModule, 20 | RepositoryModule, 21 | ScheduleModule.forRoot(), 22 | 23 | BullModule.registerQueueAsync({ 24 | name: 'dependencies', 25 | imports: [ConfigModule], 26 | inject: [ConfigService], 27 | useFactory: (configService: ConfigService) => ({ 28 | redis: configService.get('REDIS_URL'), 29 | processors: [], 30 | }), 31 | }), 32 | 33 | OrmModule, 34 | ConfigModule, 35 | AuthModule, 36 | WebhooksModule, 37 | GithubModule, 38 | CronModule, 39 | PullRequestModule, 40 | LogModule, 41 | ], 42 | controllers: [AppController], 43 | providers: [AppService], 44 | }) 45 | export class AppModule {} 46 | -------------------------------------------------------------------------------- /api/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService {} 5 | -------------------------------------------------------------------------------- /api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, HttpModule } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { GitHubStrategy } from './github.strategy'; 4 | import { ConfigModule } from '../config/config.module'; 5 | import { UsersModule } from '../users/users.module'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { ConfigService } from '../config/config.service'; 8 | import { JwtStrategy } from './jwt.strategy'; 9 | import { GitHubAppStrategy } from './github-app.strategy'; 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule, 14 | UsersModule, 15 | JwtModule.registerAsync({ 16 | imports: [ConfigModule], 17 | useFactory: async (configService: ConfigService) => ({ 18 | secret: configService.get('CLIENT_SECRET'), 19 | signOptions: { expiresIn: '3600s' }, 20 | }), 21 | inject: [ConfigService], 22 | }), 23 | ], 24 | providers: [AuthService, GitHubStrategy, GitHubAppStrategy, JwtStrategy], 25 | exports: [AuthService], 26 | }) 27 | export class AuthModule {} 28 | -------------------------------------------------------------------------------- /api/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | 4 | @Injectable() 5 | export class AuthService { 6 | constructor(private readonly jwtService: JwtService) {} 7 | 8 | createToken(user: any) { 9 | return this.jwtService.sign(user); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api/src/auth/github-app.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-github'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '../config/config.service'; 5 | import { UsersService } from '../users/users.service'; 6 | import { User } from '../users/user.entity'; 7 | 8 | @Injectable() 9 | export class GitHubAppStrategy extends PassportStrategy( 10 | Strategy, 11 | 'github-app', 12 | ) { 13 | constructor(config: ConfigService, userService: UsersService) { 14 | super( 15 | { 16 | clientID: config.get('APP_CLIENT_ID'), 17 | clientSecret: config.get('APP_CLIENT_SECRET'), 18 | callbackURL: config.get('APP_CALLBACK_URL'), 19 | }, 20 | 21 | async (accessToken, tokenSecret, profile, done) => { 22 | let user = await userService.githubAuth(accessToken, profile); 23 | return done(null, user); 24 | }, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/src/auth/github.strategy.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy } from 'passport-github'; 4 | import { ConfigService } from '../config/config.service'; 5 | import { UsersService } from '../users/users.service'; 6 | import { User } from '../users/user.entity'; 7 | 8 | @Injectable() 9 | export class GitHubStrategy extends PassportStrategy(Strategy) { 10 | constructor(private config: ConfigService, userService: UsersService) { 11 | super( 12 | { 13 | clientID: config.get('CLIENT_ID'), 14 | clientSecret: config.get('CLIENT_SECRET'), 15 | callbackURL: config.get('CALLBACK_URL'), 16 | }, 17 | 18 | async (accessToken, tokenSecret, profile, done) => { 19 | let user = await userService.githubAuth(accessToken, profile); 20 | return done(null, user); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { ConfigService } from '../config/config.service'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor(private config: ConfigService) { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | ignoreExpiration: false, 12 | secretOrKey: config.get('CLIENT_SECRET'), 13 | }); 14 | } 15 | 16 | async validate(payload: any) { 17 | if (this.config.get('IS_BETA') === 'true' && payload.validated === false) { 18 | throw new UnauthorizedException('Your account will soon be validated.'); 19 | } 20 | 21 | return { 22 | username: payload.userName, 23 | githubId: payload.githubId, 24 | githubToken: payload.githubToken, 25 | avatarUrl: payload.avatarUrl, 26 | id: payload.userId, 27 | validated: payload.validated, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from './config.service'; 4 | 5 | @Module({ 6 | providers: [ 7 | { 8 | provide: ConfigService, 9 | useValue: new ConfigService( 10 | path.resolve(`./.env.${process.env.NODE_ENV || 'dev'}`), 11 | ), 12 | }, 13 | ], 14 | exports: [ConfigService], 15 | }) 16 | export class ConfigModule {} 17 | -------------------------------------------------------------------------------- /api/src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as fs from 'fs'; 3 | 4 | export class ConfigService { 5 | private readonly envConfig: { [key: string]: string }; 6 | 7 | constructor(filePath: string) { 8 | if (fs.existsSync(filePath)) { 9 | this.envConfig = dotenv.parse(fs.readFileSync(filePath)); 10 | } else { 11 | this.envConfig = process.env; 12 | } 13 | } 14 | 15 | get(key: string): string { 16 | return this.envConfig[key]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/src/cron/cron.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { QueueModule } from '../queue/queue.module'; 3 | import { RepositoryModule } from '../repository/repository.module'; 4 | import { CronService } from './cron.service'; 5 | 6 | @Module({ 7 | imports: [QueueModule, RepositoryModule], 8 | providers: [CronService], 9 | }) 10 | export class CronModule {} 11 | -------------------------------------------------------------------------------- /api/src/cron/cron.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CronService } from './cron.service'; 3 | 4 | describe('CronService', () => { 5 | let service: CronService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CronService], 10 | }).compile(); 11 | 12 | service = module.get(CronService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /api/src/cron/cron.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectQueue } from '@nestjs/bull'; 2 | import { Injectable, Logger } from '@nestjs/common'; 3 | import { Cron, CronExpression } from '@nestjs/schedule'; 4 | import { Queue } from 'bull'; 5 | import { RepositoryService } from '../repository/repository.service'; 6 | 7 | @Injectable() 8 | export class CronService { 9 | private readonly logger = new Logger(CronService.name); 10 | 11 | constructor( 12 | @InjectQueue('dependencies') private readonly queue: Queue, 13 | private readonly repositoryService: RepositoryService, 14 | ) {} 15 | 16 | @Cron(CronExpression.EVERY_4_HOURS) 17 | async refreshAllRepositories() { 18 | const repos = await this.repositoryService.getAllRepos(); 19 | for (const repo of repos) { 20 | for (const user of repo.users) { 21 | if (user.githubToken) { 22 | this.queue.add('compute_yarn_dependencies', { 23 | repositoryFullName: repo.fullName, 24 | path: repo.path, 25 | branch: repo.branch, 26 | githubToken: user.githubToken, 27 | repositoryId: repo.githubId, 28 | hasYarnLock: repo.hasYarnLock, 29 | }); 30 | break; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api/src/github/github.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, HttpModule } from '@nestjs/common'; 2 | import { GithubService } from './github.service'; 3 | 4 | @Module({ 5 | imports: [HttpModule], 6 | exports: [GithubService], 7 | providers: [GithubService], 8 | }) 9 | export class GithubModule {} 10 | -------------------------------------------------------------------------------- /api/src/log/log.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; 3 | import { PullRequest } from '../pull-request/pull-request.entity'; 4 | 5 | @Entity() 6 | export class Log { 7 | @ApiProperty({ 8 | description: 'Id of the object', 9 | readOnly: true, 10 | }) 11 | @PrimaryGeneratedColumn() 12 | id?: number; 13 | 14 | @ApiProperty() 15 | @Column({ length: 500, nullable: true }) 16 | stackTrace?: string; 17 | 18 | @ApiProperty() 19 | @Column() 20 | name: string; 21 | 22 | @ApiProperty() 23 | @Column() 24 | failedReason: string; 25 | 26 | @ApiProperty() 27 | @Column({ type: 'json', nullable: true }) 28 | data?: object; 29 | 30 | @OneToOne( 31 | type => PullRequest, 32 | pullRequest => pullRequest.log, 33 | { onDelete: 'CASCADE' }, 34 | ) 35 | pullRequest: PullRequest; 36 | } 37 | -------------------------------------------------------------------------------- /api/src/log/log.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { Log } from './log.entity'; 3 | import { LogService } from './log.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { PullRequestModule } from '../pull-request/pull-request.module'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Log]), PullRequestModule], 9 | providers: [LogService], 10 | exports: [LogService], 11 | }) 12 | export class LogModule {} 13 | -------------------------------------------------------------------------------- /api/src/log/log.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@nestjsx/crud-typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { Log } from './log.entity'; 6 | import { PullRequest } from '../pull-request/pull-request.entity'; 7 | 8 | @Injectable() 9 | export class LogService extends TypeOrmCrudService { 10 | constructor( 11 | @InjectRepository(Log) 12 | private readonly repository: Repository, 13 | @InjectRepository(PullRequest) 14 | private readonly pullRequestRepository: Repository, 15 | ) { 16 | super(repository); 17 | } 18 | 19 | async savePullRequestLog(logDto: Partial, branchName: string) { 20 | const pullRequest = await this.pullRequestRepository.findOneOrFail({ 21 | where: { 22 | branchName, 23 | }, 24 | }); 25 | return await this.repository.save({ ...logDto, pullRequest }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { NestExpressApplication } from '@nestjs/platform-express'; 3 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.enableCors(); 9 | 10 | const options = new DocumentBuilder() 11 | .setTitle('Reactivated App') 12 | .setDescription('Reactivated App description') 13 | .setVersion('1.0') 14 | .addTag('reactivated app') 15 | .build(); 16 | if (process.env.NODE_ENV === 'dev') { 17 | const document = SwaggerModule.createDocument(app, options); 18 | SwaggerModule.setup('swagger', app, document); 19 | } 20 | 21 | await app.listen(process.env.PORT || 3000); 22 | } 23 | bootstrap(); 24 | -------------------------------------------------------------------------------- /api/src/orm.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module, OnModuleInit } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ConfigModule } from './config/config.module'; 4 | import { ConfigService } from './config/config.service'; 5 | 6 | @Module({ 7 | imports: [ 8 | TypeOrmModule.forRootAsync({ 9 | imports: [ConfigModule], 10 | inject: [ConfigService], 11 | useFactory: async (configService: ConfigService) => ({ 12 | type: configService.get('TYPEORM_CONNECTION') as 'postgres', 13 | host: configService.get('TYPEORM_HOST'), 14 | port: Number(configService.get('TYPEORM_PORT')), 15 | username: configService.get('TYPEORM_USERNAME'), 16 | password: configService.get('TYPEORM_PASSWORD'), 17 | database: configService.get('TYPEORM_DATABASE'), 18 | entities: [__dirname + '/**/*.entity{.ts,.js}'], 19 | synchronize: Boolean(configService.get('TYPEORM_SYNCHRONIZE')), 20 | // logging: process.env.NODE_ENV === 'dev', 21 | }), 22 | }), 23 | ], 24 | controllers: [], 25 | providers: [], 26 | }) 27 | export class OrmModule {} 28 | -------------------------------------------------------------------------------- /api/src/pull-request/pull-request.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | Column, 4 | Entity, 5 | JoinColumn, 6 | ManyToOne, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | } from 'typeorm'; 10 | import { Log } from '../log/log.entity'; 11 | import { Repository } from '../repository/repository.entity'; 12 | 13 | export type Status = 'pending' | 'done' | 'merged' | 'closed' | 'error'; 14 | 15 | @Entity() 16 | export class PullRequest { 17 | @PrimaryGeneratedColumn() 18 | @ApiProperty({ 19 | description: 'Id of the object', 20 | readOnly: true, 21 | }) 22 | id: number; 23 | 24 | @ApiProperty() 25 | @Column() 26 | status: Status; 27 | 28 | @ApiProperty() 29 | @ManyToOne( 30 | () => Repository, 31 | repository => repository.pullRequests, 32 | { 33 | onDelete: 'CASCADE', 34 | }, 35 | ) 36 | @JoinColumn({ name: 'repositoryId' }) 37 | repository: Repository; 38 | 39 | @ApiProperty() 40 | @Column({ unique: true }) 41 | branchName: string; 42 | 43 | @ApiProperty() 44 | @Column({ nullable: true }) 45 | url: string; 46 | 47 | @OneToOne( 48 | type => Log, 49 | log => log.pullRequest, 50 | { onUpdate: 'CASCADE' }, 51 | ) 52 | @JoinColumn() 53 | log: Log; 54 | } 55 | -------------------------------------------------------------------------------- /api/src/pull-request/pull-request.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { PullRequest } from './pull-request.entity'; 4 | import { PullRequestService } from './pull-request.service'; 5 | import { RepositoryModule } from '../repository/repository.module'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forFeature([PullRequest]), 10 | forwardRef(() => RepositoryModule), 11 | ], 12 | providers: [PullRequestService], 13 | exports: [PullRequestService, TypeOrmModule], 14 | controllers: [], 15 | }) 16 | export class PullRequestModule {} 17 | -------------------------------------------------------------------------------- /api/src/pull-request/pull-request.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@nestjsx/crud-typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { PullRequest } from './pull-request.entity'; 6 | import { RepositoryService } from '../repository/repository.service'; 7 | 8 | @Injectable() 9 | export class PullRequestService extends TypeOrmCrudService { 10 | constructor( 11 | @InjectRepository(PullRequest) 12 | private readonly repository: Repository, 13 | private readonly repoService: RepositoryService, 14 | ) { 15 | super(repository); 16 | } 17 | 18 | async createPullRequest(pullRequest: PullRequest): Promise { 19 | return this.repository.save(pullRequest); 20 | } 21 | 22 | async updatePullRequest( 23 | branchName: string, 24 | data: Partial>, 25 | ) { 26 | const pullRequest = await this.repository.findOneOrFail({ 27 | where: { 28 | branchName, 29 | }, 30 | }); 31 | return await this.repository.save({ ...pullRequest, ...data }); 32 | } 33 | 34 | async getPullRequestsFromRepository(repositoryId, limit?) { 35 | const repo = await this.repoService.findRepo({ id: repositoryId }); 36 | 37 | if (!repo) { 38 | throw new NotFoundException('Repository not found'); 39 | } 40 | 41 | return await this.repository.find({ 42 | where: { repository: { id: repo.id } }, 43 | relations: ['repository', 'log'], 44 | order: { id: 'DESC' }, 45 | take: limit, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /api/src/queue/queue.module.ts: -------------------------------------------------------------------------------- 1 | import { BullModule, BullModuleOptions } from '@nestjs/bull'; 2 | import { forwardRef, Module } from '@nestjs/common'; 3 | import { ConfigModule } from '../config/config.module'; 4 | import { ConfigService } from '../config/config.service'; 5 | import { GithubModule } from '../github/github.module'; 6 | import { OrmModule } from '../orm.module'; 7 | import { RepositoryModule } from '../repository/repository.module'; 8 | 9 | const redisOptions = (configService: ConfigService) => { 10 | const config: BullModuleOptions = { 11 | redis: configService.get('REDIS_URL'), 12 | settings: { 13 | lockDuration: 30000 * 10, 14 | lockRenewTime: 5000, 15 | }, 16 | limiter: { 17 | duration: 1000, 18 | max: Number(configService.get('MAX_JOBS_NUMBER')), 19 | }, 20 | }; 21 | 22 | return config; 23 | }; 24 | 25 | const BullQueueModule = BullModule.registerQueueAsync({ 26 | name: 'dependencies', 27 | imports: [ConfigModule], 28 | inject: [ConfigService], 29 | useFactory: (configService: ConfigService) => redisOptions(configService), 30 | }); 31 | 32 | @Module({ 33 | imports: [ 34 | BullQueueModule, 35 | GithubModule, 36 | forwardRef(() => RepositoryModule), 37 | OrmModule, 38 | ], 39 | exports: [BullQueueModule], 40 | }) 41 | export class QueueModule {} 42 | -------------------------------------------------------------------------------- /api/src/queue/worker.module.ts: -------------------------------------------------------------------------------- 1 | import { BullModule, BullModuleOptions } from '@nestjs/bull'; 2 | import { Module, OnModuleInit } from '@nestjs/common'; 3 | import { DoneCallback, Job } from 'bull'; 4 | import { ConfigModule } from '../config/config.module'; 5 | import { ConfigService } from '../config/config.service'; 6 | import { GithubModule } from '../github/github.module'; 7 | import { LogModule } from '../log/log.module'; 8 | import { OrmModule } from '../orm.module'; 9 | import { PullRequestModule } from '../pull-request/pull-request.module'; 10 | import { RepositoryModule } from '../repository/repository.module'; 11 | import { DependenciesQueue } from './dependencies.queue'; 12 | 13 | const redisOptions = (configService: ConfigService) => { 14 | const config: BullModuleOptions = { 15 | redis: configService.get('REDIS_URL'), 16 | settings: { 17 | lockDuration: 30000 * 10, 18 | maxStalledCount: 0, 19 | }, 20 | limiter: { 21 | duration: 1000, 22 | max: Number(configService.get('MAX_JOBS_NUMBER')), 23 | }, 24 | processors: [ 25 | (job: Job, done: DoneCallback) => { 26 | done(null, job.data); 27 | }, 28 | ], 29 | }; 30 | 31 | return config; 32 | }; 33 | 34 | const BullQueueModule = BullModule.registerQueueAsync({ 35 | name: 'dependencies', 36 | imports: [ConfigModule], 37 | inject: [ConfigService], 38 | useFactory: (configService: ConfigService) => redisOptions(configService), 39 | }); 40 | 41 | @Module({ 42 | imports: [ 43 | BullQueueModule, 44 | GithubModule, 45 | RepositoryModule, 46 | PullRequestModule, 47 | OrmModule, 48 | LogModule, 49 | ], 50 | exports: [BullQueueModule], 51 | providers: [DependenciesQueue], 52 | }) 53 | export class WorkerModule implements OnModuleInit { 54 | onModuleInit() { 55 | console.log('WORKER: ', process.pid); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /api/src/queue/worker.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import * as dotenv from 'dotenv'; 4 | import * as throng from 'throng'; 5 | import { WorkerModule } from './worker.module'; 6 | dotenv.config({ path: `./.env.${process.env.NODE_ENV || 'dev'}` }); 7 | 8 | const WORKERS = Number(process.env.WEB_CONCURRENCY); 9 | 10 | async function bootstrap() { 11 | const worker = await NestFactory.create(WorkerModule); 12 | Logger.log('Worker ' + process.pid); 13 | worker.init(); 14 | } 15 | 16 | throng({ 17 | workers: WORKERS, 18 | lifetime: Infinity, // Respawn worker if it dies 19 | start: bootstrap, 20 | }); 21 | -------------------------------------------------------------------------------- /api/src/repository/repository.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, HttpModule, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ConfigModule } from '../config/config.module'; 4 | import { GithubModule } from '../github/github.module'; 5 | import { PullRequestModule } from '../pull-request/pull-request.module'; 6 | import { QueueModule } from '../queue/queue.module'; 7 | import { UsersModule } from '../users/users.module'; 8 | import { RepositoryController } from './repository.controller'; 9 | import { Repository } from './repository.entity'; 10 | import { RepositoryService } from './repository.service'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([Repository]), 15 | HttpModule, 16 | GithubModule, 17 | forwardRef(() => UsersModule), 18 | forwardRef(() => QueueModule), 19 | forwardRef(() => PullRequestModule), 20 | ConfigModule, 21 | ], 22 | exports: [RepositoryService], 23 | providers: [RepositoryService], 24 | controllers: [RepositoryController], 25 | }) 26 | export class RepositoryModule {} 27 | -------------------------------------------------------------------------------- /api/src/repository/repository.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { TypeOrmCrudService } from '@nestjsx/crud-typeorm'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from './repository.entity'; 5 | import { 6 | Repository as RepositoryContent, 7 | DeleteResult, 8 | RemoveOptions, 9 | } from 'typeorm'; 10 | import { User } from '../users/user.entity'; 11 | 12 | @Injectable() 13 | export class RepositoryService extends TypeOrmCrudService { 14 | constructor( 15 | @InjectRepository(Repository) 16 | private repositoryContent: RepositoryContent, 17 | ) { 18 | super(repositoryContent); 19 | } 20 | 21 | async getAllRepos() { 22 | return this.repositoryContent.find({ relations: ['users'] }); 23 | } 24 | 25 | async removeRepos(repos: Repository[], options?: RemoveOptions) { 26 | return this.repositoryContent.remove(repos, options); 27 | } 28 | 29 | async findRepos(criteria: Partial) { 30 | return this.repositoryContent.find({ 31 | where: criteria, 32 | relations: ['users'], 33 | }); 34 | } 35 | 36 | async findRepo(criteria: Partial) { 37 | return this.repositoryContent.findOne({ 38 | where: criteria, 39 | relations: ['users'], 40 | }); 41 | } 42 | 43 | async addRepo(repo: Repository): Promise { 44 | return this.repositoryContent.save(repo); 45 | } 46 | 47 | async updateRepo(repoId: string, repo: Repository) { 48 | const repository = await this.findRepo({ id: parseInt(repoId, 10) }); 49 | let users = repository.users; 50 | 51 | /* 52 | * Search if the users passed in repo exists in the repository found in db 53 | * If it doesnt exist, adds the user to the repository 54 | */ 55 | if ( 56 | repo.users && 57 | users && 58 | users.some(repoUser => repo.users.some(user => user.id === repoUser.id)) 59 | ) { 60 | users = [...repository.users, ...(repo.users || [])]; 61 | } 62 | 63 | return this.repositoryContent.save({ 64 | ...repository, 65 | ...repo, 66 | users, 67 | }); 68 | } 69 | 70 | async deleteRepo(params, userId: number) { 71 | const repos = await this.find({ 72 | where: params, 73 | relations: ['users'], 74 | }); 75 | 76 | return Promise.all( 77 | repos.map( 78 | (repo): Promise => { 79 | const users = repo.users; 80 | 81 | if (users.some(user => user.id === userId)) { 82 | if (users.length === 1) { 83 | return this.repositoryContent.delete(repo.id); 84 | } else { 85 | return this.repositoryContent.save({ 86 | ...repo, 87 | users: users.filter(user => user.id !== userId), 88 | }); 89 | } 90 | } 91 | 92 | return Promise.resolve(); 93 | }, 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /api/src/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | @Entity() 5 | export class User { 6 | @ApiProperty({ 7 | description: 'Id of the object', 8 | readOnly: true, 9 | }) 10 | @PrimaryGeneratedColumn() 11 | id?: number; 12 | 13 | @ApiProperty() 14 | @Column({ length: 25 }) 15 | username: string; 16 | 17 | @ApiProperty({ readOnly: true }) 18 | @Column() 19 | githubId: string; 20 | 21 | @ApiProperty() 22 | @Column() 23 | githubToken: string; 24 | 25 | @ApiProperty() 26 | @Column() 27 | avatarUrl: string; 28 | 29 | @ApiProperty() 30 | @Column({ default: false }) 31 | validated: boolean; 32 | } 33 | -------------------------------------------------------------------------------- /api/src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | NotFoundException, 4 | Param, 5 | Post, 6 | Req, 7 | UnauthorizedException, 8 | UseGuards, 9 | } from '@nestjs/common'; 10 | import { AuthGuard } from '@nestjs/passport'; 11 | import { Repository } from '../repository/repository.entity'; 12 | import { RepositoryService } from '../repository/repository.service'; 13 | import { UsersService } from './users.service'; 14 | 15 | @Controller('users') 16 | export class UsersController { 17 | constructor( 18 | public service: UsersService, 19 | private readonly repositoryService: RepositoryService, 20 | ) {} 21 | 22 | @Post(':id/delete-account') 23 | @UseGuards(AuthGuard('jwt')) 24 | async deleteUser(@Param('id') userId: string, @Req() req) { 25 | const user = await this.service.findOne(userId); 26 | if (!user) { 27 | throw new NotFoundException('User not found'); 28 | } 29 | if (req.user.id !== user.id) { 30 | throw new UnauthorizedException(); 31 | } 32 | 33 | const repos: Repository[] = await this.repositoryService.getAllRepos(); 34 | const reposOfUser = repos.filter((repo: Repository) => { 35 | if (repo.users.length === 1 && repo.users[0].id === user.id) { 36 | return true; 37 | } 38 | return false; 39 | }); 40 | 41 | await this.repositoryService.removeRepos(reposOfUser); 42 | return await this.service.deleteUser(user); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /api/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, HttpModule, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ConfigModule } from '../config/config.module'; 4 | import { RepositoryModule } from '../repository/repository.module'; 5 | import { User } from './user.entity'; 6 | import { UsersController } from './users.controller'; 7 | import { UsersService } from './users.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forFeature([User]), 12 | HttpModule, 13 | ConfigModule, 14 | forwardRef(() => RepositoryModule), 15 | ], 16 | exports: [UsersService], 17 | providers: [UsersService], 18 | controllers: [UsersController], 19 | }) 20 | export class UsersModule {} 21 | -------------------------------------------------------------------------------- /api/src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { TypeOrmCrudService } from '@nestjsx/crud-typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { ConfigService } from '../config/config.service'; 6 | import { User } from './user.entity'; 7 | 8 | @Injectable() 9 | export class UsersService extends TypeOrmCrudService { 10 | constructor( 11 | @InjectRepository(User) private usersRepository: Repository, 12 | 13 | private readonly config: ConfigService, 14 | private readonly httpService: HttpService, 15 | ) { 16 | super(usersRepository); 17 | } 18 | 19 | async getAllUsers(): Promise { 20 | return await this.usersRepository.find(); 21 | } 22 | 23 | async getUser(username: string): Promise { 24 | const users = await this.usersRepository.find({ 25 | where: [{ username }], 26 | }); 27 | return users[0]; 28 | } 29 | 30 | async getById(id: User['id']) { 31 | return this.usersRepository.findOne({ 32 | where: { id }, 33 | }); 34 | } 35 | 36 | async updateUser(user: User) { 37 | this.usersRepository.save(user); 38 | } 39 | 40 | async createUser(user: User): Promise { 41 | return await this.usersRepository.save(user); 42 | } 43 | 44 | async deleteUser(user: User) { 45 | this.usersRepository.delete(user); 46 | } 47 | 48 | async githubAuth(accessToken, profile): Promise { 49 | let user = await this.getUser(profile.username); 50 | 51 | if (!user) { 52 | let newUser: User = { 53 | username: profile.username, 54 | githubId: profile.id, 55 | githubToken: accessToken, 56 | validated: this.config.get('IS_BETA') === 'true' ? false : true, 57 | avatarUrl: profile._json.avatar_url, 58 | }; 59 | user = await this.createUser(newUser); 60 | 61 | const text = `New user to validate registered : ${profile.username}`; 62 | await this.httpService 63 | .post( 64 | this.config.get('SLACK_BETA_URL'), 65 | { text }, 66 | { 67 | headers: { 68 | 'Content-type': 'application/json', 69 | }, 70 | }, 71 | ) 72 | .toPromise(); 73 | } else { 74 | await this.updateUser({ 75 | ...user, 76 | githubToken: accessToken, 77 | }); 78 | } 79 | 80 | return user; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /api/src/webhooks/webhooks.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Observable, of } from 'rxjs'; 8 | import { ConfigService } from '../config/config.service'; 9 | 10 | @Injectable() 11 | export class WebhookInterceptor implements NestInterceptor { 12 | constructor(protected configService: ConfigService) {} 13 | 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | const cryptoJS = require('crypto-js'); 16 | const compare = require('secure-compare'); 17 | 18 | const request = context.switchToHttp().getRequest(); 19 | 20 | const body: any = request.body; 21 | const xHubSignature = request.headers['x-hub-signature']; 22 | 23 | const signature: string = 24 | 'sha1=' + 25 | cryptoJS 26 | .HmacSHA1( 27 | JSON.stringify(body), 28 | this.configService.get('GITHUB_WEBHOOK_SECRET'), 29 | ) 30 | .toString(); 31 | 32 | if (compare(signature, xHubSignature)) { 33 | return next.handle(); 34 | } else { 35 | return of([]); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /api/src/webhooks/webhooks.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WebhooksController } from './webhooks.controller'; 3 | import { UsersModule } from '../users/users.module'; 4 | import { RepositoryModule } from '../repository/repository.module'; 5 | import { ConfigModule } from '../config/config.module'; 6 | import { PullRequestModule } from '../pull-request/pull-request.module'; 7 | import { QueueModule } from '../queue/queue.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | RepositoryModule, 12 | UsersModule, 13 | ConfigModule, 14 | PullRequestModule, 15 | QueueModule, 16 | ], 17 | controllers: [WebhooksController], 18 | providers: [], 19 | }) 20 | export class WebhooksModule {} 21 | -------------------------------------------------------------------------------- /api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/app.module'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/api/tmp/.gitkeep -------------------------------------------------------------------------------- /api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./src", 12 | "incremental": true 13 | }, 14 | "include": ["./src"], 15 | "exclude": ["node_modules", "dist", "tmp"] 16 | } 17 | -------------------------------------------------------------------------------- /api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /app/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | rules: { 'react-native/no-inline-styles': 0 }, 7 | }; 8 | -------------------------------------------------------------------------------- /app/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # Visual Studio Code 33 | # 34 | .vscode/ 35 | 36 | # node.js 37 | # 38 | node_modules/ 39 | npm-debug.log 40 | yarn-error.log 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | !debug.keystore 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | # For more information about the recommended setup visit: 53 | # https://docs.fastlane.tools/best-practices/source-control/ 54 | 55 | */fastlane/report.xml 56 | */fastlane/Preview.html 57 | */fastlane/screenshots 58 | 59 | # Bundle artifact 60 | *.jsbundle 61 | 62 | # CocoaPods 63 | /ios/Pods/ 64 | -------------------------------------------------------------------------------- /app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | jsxBracketSameLine: false, 5 | printWidth: 120, 6 | semi: true, 7 | singleQuote: true, 8 | trailingComma: 'all', 9 | }; 10 | -------------------------------------------------------------------------------- /app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app/__tests__/App-test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /app/android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.app", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.app", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /app/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /app/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/debug.keystore -------------------------------------------------------------------------------- /app/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/android/app/src/debug/java/com/app/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.app; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.app; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "app"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/app/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.app; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.reactnativecommunity.webview.RNCWebViewPackage; 8 | import com.facebook.react.ReactInstanceManager; 9 | import com.facebook.react.ReactNativeHost; 10 | import com.facebook.react.ReactPackage; 11 | import com.facebook.soloader.SoLoader; 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = 18 | new ReactNativeHost(this) { 19 | @Override 20 | public boolean getUseDeveloperSupport() { 21 | return BuildConfig.DEBUG; 22 | } 23 | 24 | @Override 25 | protected List getPackages() { 26 | @SuppressWarnings("UnnecessaryLocalVariable") 27 | List packages = new PackageList(this).getPackages(); 28 | // Packages that cannot be autolinked yet can be added manually here, for example: 29 | // packages.add(new MyReactNativePackage()); 30 | return packages; 31 | } 32 | 33 | @Override 34 | protected String getJSMainModuleName() { 35 | return "index"; 36 | } 37 | }; 38 | 39 | @Override 40 | public ReactNativeHost getReactNativeHost() { 41 | return mReactNativeHost; 42 | } 43 | 44 | @Override 45 | public void onCreate() { 46 | super.onCreate(); 47 | SoLoader.init(this, /* native exopackage */ false); 48 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 49 | } 50 | 51 | /** 52 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 53 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 54 | * 55 | * @param context 56 | * @param reactInstanceManager 57 | */ 58 | private static void initializeFlipper( 59 | Context context, ReactInstanceManager reactInstanceManager) { 60 | if (BuildConfig.DEBUG) { 61 | try { 62 | /* 63 | We use reflection here to pick up the class that initializes Flipper, 64 | since Flipper library is not available in release mode 65 | */ 66 | Class aClass = Class.forName("com.app.ReactNativeFlipper"); 67 | aClass 68 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 69 | .invoke(null, context, reactInstanceManager); 70 | } catch (ClassNotFoundException e) { 71 | e.printStackTrace(); 72 | } catch (NoSuchMethodException e) { 73 | e.printStackTrace(); 74 | } catch (IllegalAccessException e) { 75 | e.printStackTrace(); 76 | } catch (InvocationTargetException e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | app 3 | 4 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "29.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 29 8 | targetSdkVersion = 28 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.5.2") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://www.jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.33.1 29 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/app/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /app/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'app' 2 | 3 | include ':react-native-webview' 4 | project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android') 5 | 6 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 7 | include ':app' 8 | -------------------------------------------------------------------------------- /app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "displayName": "app" 4 | } 5 | -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { AppRegistry } from 'react-native'; 6 | import App from './src/App'; 7 | import { name as appName } from './app.json'; 8 | import 'react-native-gesture-handler'; 9 | 10 | AppRegistry.registerComponent(appName, () => App); 11 | -------------------------------------------------------------------------------- /app/ios/app-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/ios/app-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/ios/app.xcodeproj/xcshareddata/xcschemes/app-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/ios/app.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/ios/app.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/ios/app/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /app/ios/app/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | 6 | #import 7 | #import 8 | #import 9 | 10 | #if DEBUG 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | 18 | static void InitializeFlipper(UIApplication *application) { 19 | FlipperClient *client = [FlipperClient sharedClient]; 20 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 21 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 22 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 23 | [client addPlugin:[FlipperKitReactPlugin new]]; 24 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 25 | [client start]; 26 | } 27 | #endif 28 | 29 | @implementation AppDelegate 30 | 31 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 32 | { 33 | #if DEBUG 34 | InitializeFlipper(application); 35 | #endif 36 | 37 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 38 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 39 | moduleName:@"app" 40 | initialProperties:nil]; 41 | 42 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 43 | 44 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 45 | UIViewController *rootViewController = [UIViewController new]; 46 | rootViewController.view = rootView; 47 | self.window.rootViewController = rootViewController; 48 | [self.window makeKeyAndVisible]; 49 | return YES; 50 | } 51 | 52 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 53 | { 54 | #if DEBUG 55 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 56 | #else 57 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 58 | #endif 59 | } 60 | 61 | - (BOOL)application:(UIApplication *)application 62 | openURL:(NSURL *)url 63 | options:(NSDictionary *)options 64 | { 65 | return [RCTLinkingManager application:application openURL:url options:options]; 66 | } 67 | @end 68 | -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "size": "29x29", 6 | "scale": "2x" 7 | }, 8 | { 9 | "idiom": "iphone", 10 | "size": "29x29", 11 | "scale": "3x" 12 | }, 13 | { 14 | "idiom": "iphone", 15 | "size": "40x40", 16 | "scale": "2x" 17 | }, 18 | { 19 | "idiom": "iphone", 20 | "size": "40x40", 21 | "scale": "3x" 22 | }, 23 | { 24 | "idiom": "iphone", 25 | "size": "60x60", 26 | "scale": "2x" 27 | }, 28 | { 29 | "idiom": "iphone", 30 | "size": "60x60", 31 | "scale": "3x" 32 | } 33 | ], 34 | "info": { 35 | "version": 1, 36 | "author": "xcode" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/ios/app/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/ios/app/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | app 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLName 29 | reactivatedapp 30 | CFBundleURLSchemes 31 | 32 | reactivatedapp 33 | 34 | 35 | 36 | CFBundleVersion 37 | 1 38 | LSApplicationCategoryType 39 | 40 | LSRequiresIPhoneOS 41 | 42 | NSAppTransportSecurity 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | NSExceptionDomains 47 | 48 | localhost 49 | 50 | NSExceptionAllowsInsecureHTTPLoads 51 | 52 | 53 | 54 | 55 | NSLocationWhenInUseUsageDescription 56 | 57 | UIAppFonts 58 | 59 | AntDesign.ttf 60 | Entypo.ttf 61 | EvilIcons.ttf 62 | Feather.ttf 63 | FontAwesome.ttf 64 | FontAwesome5_Brands.ttf 65 | FontAwesome5_Regular.ttf 66 | FontAwesome5_Solid.ttf 67 | Foundation.ttf 68 | Ionicons.ttf 69 | MaterialIcons.ttf 70 | MaterialCommunityIcons.ttf 71 | SimpleLineIcons.ttf 72 | Octicons.ttf 73 | Zocial.ttf 74 | 75 | UILaunchStoryboardName 76 | LaunchScreen 77 | UIRequiredDeviceCapabilities 78 | 79 | armv7 80 | 81 | UISupportedInterfaceOrientations 82 | 83 | UIInterfaceOrientationPortrait 84 | UIInterfaceOrientationLandscapeLeft 85 | UIInterfaceOrientationLandscapeRight 86 | 87 | UIViewControllerBasedStatusBarAppearance 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/ios/app/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/ios/appTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/ios/appTests/appTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface appTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation appTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /app/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "test": "jest", 10 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 11 | "postinstall": "rndebugger-open" 12 | }, 13 | "dependencies": { 14 | "@react-native-community/async-storage": "^1.11.0", 15 | "@react-native-community/masked-view": "^0.1.10", 16 | "@react-navigation/native": "^5.5.0", 17 | "@react-navigation/stack": "^5.4.1", 18 | "axios": "^0.19.2", 19 | "color": "^3.1.2", 20 | "jwt-decode": "^2.2.0", 21 | "react": "16.11.0", 22 | "react-native": "0.62.2", 23 | "react-native-animatable": "^1.3.3", 24 | "react-native-gesture-handler": "^1.6.1", 25 | "react-native-magnus": "^1.0.38", 26 | "react-native-modal": "^11.5.6", 27 | "react-native-progress": "^4.1.2", 28 | "react-native-reanimated": "^1.9.0", 29 | "react-native-safe-area-context": "^3.0.2", 30 | "react-native-screens": "^2.8.0", 31 | "react-native-vector-icons": "^6.6.0", 32 | "react-native-webview": "^10.2.3", 33 | "swr": "^0.2.2" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.6.2", 37 | "@babel/runtime": "^7.6.2", 38 | "@react-native-community/eslint-config": "^1.0.0", 39 | "@types/jest": "^24.0.24", 40 | "@types/jwt-decode": "^2.2.1", 41 | "@types/react-native": "^0.62.0", 42 | "@types/react-test-renderer": "16.9.2", 43 | "@typescript-eslint/eslint-plugin": "^2.27.0", 44 | "@typescript-eslint/parser": "^2.27.0", 45 | "babel-jest": "^24.9.0", 46 | "eslint": "^6.5.1", 47 | "jest": "^24.9.0", 48 | "metro-react-native-babel-preset": "^0.58.0", 49 | "prettier": "^2.0.4", 50 | "react-native-debugger-open": "^0.3.24", 51 | "react-test-renderer": "16.11.0", 52 | "typescript": "^3.8.3" 53 | }, 54 | "jest": { 55 | "preset": "react-native", 56 | "moduleFileExtensions": [ 57 | "ts", 58 | "tsx", 59 | "js", 60 | "jsx", 61 | "json", 62 | "node" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { SafeAreaView, StatusBar } from 'react-native'; 4 | import 'react-native-gesture-handler'; 5 | import { ThemeProvider } from 'react-native-magnus'; 6 | import { AuthProvider } from './contexts/AuthContext'; 7 | import AppStack from './navigators/AppStack'; 8 | import { theme } from './theme'; 9 | 10 | const App = () => { 11 | const linking = { 12 | prefixes: ['reactivatedapp://'], 13 | }; 14 | return ( 15 | <> 16 | 17 | {/* <_StatusBar /> */} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /app/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Div, Icon, Text } from 'react-native-magnus'; 3 | 4 | type HeaderProps = {}; 5 | 6 | const Header: React.FC = () => { 7 | return ( 8 |

9 | 10 | 11 | React 12 | 13 | 14 | ivated 15 | 16 |
17 | 18 | app 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default Header; 25 | -------------------------------------------------------------------------------- /app/src/components/Repository/FrameworkTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View, StyleSheet } from 'react-native'; 3 | import { Tag } from 'react-native-magnus'; 4 | 5 | type FrameworkTagProps = { 6 | framework: FrameworkTag; 7 | }; 8 | 9 | const FrameworkTag: React.FC = ({ framework }) => { 10 | const getFrameworkColor = (framework: FrameworkTag) => { 11 | switch (framework) { 12 | case 'react': 13 | case 'react native': 14 | return 'blue'; 15 | 16 | case 'vue': 17 | return 'green'; 18 | 19 | case 'angular': 20 | case 'nest.js': 21 | return 'red'; 22 | 23 | default: 24 | return 'black'; 25 | } 26 | }; 27 | 28 | const color = getFrameworkColor(framework); 29 | 30 | return ( 31 | 32 | {framework} 33 | 34 | ); 35 | }; 36 | 37 | export default FrameworkTag; 38 | -------------------------------------------------------------------------------- /app/src/components/Repository/RepoItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, Div, Image, Tag } from 'react-native-magnus'; 3 | import Row from '../Row'; 4 | import * as Progress from 'react-native-progress'; 5 | import FrameworkTag from './FrameworkTag'; 6 | import { View } from 'react-native-animatable'; 7 | import { StyleSheet } from 'react-native'; 8 | import { TouchableOpacity } from 'react-native-gesture-handler'; 9 | 10 | type RepoItemProps = { 11 | repo: Repository; 12 | }; 13 | 14 | const RepoItem: React.FC = ({ repo }) => { 15 | const getHealthBarColor = (score: number) => { 16 | let color = 'green'; 17 | if (score < 25) { 18 | color = 'red'; 19 | } else if (score < 75) { 20 | color = 'orange'; 21 | } 22 | return color; 23 | }; 24 | 25 | return ( 26 |
27 | 28 | 29 | 38 | 39 |
40 | 41 | {repo.name} 42 | 43 | 44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | const styles = StyleSheet.create({ 62 | arrow: { 63 | width: 0, 64 | height: 0, 65 | backgroundColor: 'transparent', 66 | borderStyle: 'solid', 67 | borderLeftWidth: 7, 68 | borderRightWidth: 7, 69 | borderBottomWidth: 14, 70 | borderLeftColor: 'transparent', 71 | borderRightColor: 'transparent', 72 | borderBottomColor: 'black', 73 | transform: [{ rotate: '90deg' }], 74 | }, 75 | }); 76 | 77 | export default RepoItem; 78 | -------------------------------------------------------------------------------- /app/src/components/Row.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Div } from 'react-native-magnus'; 3 | 4 | type RowProps = { 5 | children: React.ReactNode; 6 | }; 7 | 8 | const Row: React.FC = ({ children }) => { 9 | return
{children}
; 10 | }; 11 | 12 | export default Row; 13 | -------------------------------------------------------------------------------- /app/src/components/Ui/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator } from 'react-native'; 3 | import { Overlay, Text } from 'react-native-magnus'; 4 | 5 | type LoadingIndicatorProps = { 6 | loading: boolean; 7 | }; 8 | 9 | const LoadingIndicator: React.FC = ({ loading }) => { 10 | return ( 11 | 12 | 13 | Loading... 14 | 15 | ); 16 | }; 17 | 18 | export default LoadingIndicator; 19 | -------------------------------------------------------------------------------- /app/src/components/Ui/MainBackground.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Div } from 'react-native-magnus'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | 5 | type MainBackgroundProps = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | const MainBackground: React.FC = ({ children }) => { 10 | return ( 11 | 12 |
13 | {children} 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default MainBackground; 20 | -------------------------------------------------------------------------------- /app/src/components/Ui/_StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, StatusBar, StyleSheet } from 'react-native'; 3 | import { View } from 'react-native-animatable'; 4 | import { SafeAreaView } from 'react-native-safe-area-context'; 5 | 6 | const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 40 : StatusBar.currentHeight; 7 | 8 | const _StatusBar: React.FC = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | const styles = StyleSheet.create({ 17 | statusBar: { 18 | height: STATUSBAR_HEIGHT, 19 | backgroundColor: '#24294e', 20 | }, 21 | }); 22 | 23 | export default _StatusBar; 24 | -------------------------------------------------------------------------------- /app/src/config/.gitignore: -------------------------------------------------------------------------------- 1 | index.ts -------------------------------------------------------------------------------- /app/src/config/index.dev.ts: -------------------------------------------------------------------------------- 1 | export const Config = { 2 | API_URL: 'http://192.168.0.25:3000', 3 | }; 4 | -------------------------------------------------------------------------------- /app/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const Config = { 2 | API_URL: 'http://192.168.0.25:3000', 3 | }; 4 | -------------------------------------------------------------------------------- /app/src/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList } from 'react-native'; 3 | import { Avatar, Button, Div, Icon, Text } from 'react-native-magnus'; 4 | import Header from '../components/Header'; 5 | import RepoItem from '../components/Repository/RepoItem'; 6 | import MainBackground from '../components/Ui/MainBackground'; 7 | import { useAxiosRequest } from '../hooks/useRequest'; 8 | import { getRepositories } from '../services/apiRepositories'; 9 | import { SafeAreaView } from 'react-native-safe-area-context'; 10 | 11 | type DashboardProps = { 12 | children: React.ReactNode; 13 | }; 14 | 15 | const Dashboard: React.FC = ({ children }) => { 16 | const { data: repositories } = useAxiosRequest('/repositories', { fetcher: getRepositories }); 17 | 18 | if (!repositories) { 19 | return null; 20 | } 21 | 22 | return ( 23 | 24 |
25 |
26 | My Reactivated Apps 27 | {repositories.length > 0 && ( 28 | 29 | {repositories.length.toString()} 30 | 31 | )} 32 |
33 | 34 |
35 | {repositories.length > 0 ? ( 36 | } 39 | contentContainerStyle={{ flex: 1 }} 40 | /> 41 | ) : ( 42 | 43 | You have no reactivated app yet! Start now by adding yout from a GitHub repository 44 | 45 | )} 46 |
47 |
48 | 62 |
63 | 64 | ); 65 | }; 66 | 67 | export default Dashboard; 68 | -------------------------------------------------------------------------------- /app/src/containers/GithubRedirect.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationProp, RouteProp } from '@react-navigation/native'; 2 | import React from 'react'; 3 | import { useAuthContext } from '../contexts/AuthContext'; 4 | import { AppStackParamList } from '../navigators/AppStack'; 5 | import apiAuth from '../services/apiAuth'; 6 | import { storeData } from '../utils/AsyncStorage'; 7 | import LoadingIndicator from '../components/Ui/LoadingIndicator'; 8 | 9 | type GithubRedirectProps = { 10 | route: RouteProp; 11 | navigation: NavigationProp; 12 | }; 13 | 14 | const GithubRedirect: React.FC = ({ route, navigation }) => { 15 | const { getCurrentToken: getToken, token } = useAuthContext(); 16 | 17 | React.useEffect(() => { 18 | async function connectGithub() { 19 | await getToken(); 20 | 21 | if (!token) { 22 | const { data } = await apiAuth.getGithubCallback(route.params.code); 23 | await storeData('token', data.token); 24 | } 25 | navigation.reset({ index: 0, routes: [{ name: 'Dashboard' }] }); 26 | } 27 | 28 | connectGithub(); 29 | }, []); 30 | 31 | return ; 32 | }; 33 | 34 | export default GithubRedirect; 35 | -------------------------------------------------------------------------------- /app/src/containers/Home.tsx: -------------------------------------------------------------------------------- 1 | import { StackNavigationProp } from '@react-navigation/stack'; 2 | import React from 'react'; 3 | import { Button, Div, Icon, Text } from 'react-native-magnus'; 4 | import Header from '../components/Header'; 5 | import MainBackground from '../components/Ui/MainBackground'; 6 | import { AppStackParamList } from '../navigators/AppStack'; 7 | 8 | type HomeProps = { 9 | navigation: StackNavigationProp; 10 | }; 11 | 12 | const Home: React.FC = ({ navigation }) => { 13 | const onSignInGithub = () => { 14 | navigation.navigate('Login'); 15 | }; 16 | 17 | return ( 18 | 19 |
20 |
21 | 22 | 23 | Keep your JS app up to date 24 | 25 |
26 | 40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Home; 47 | -------------------------------------------------------------------------------- /app/src/containers/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WebView } from 'react-native-webview'; 3 | import { Config } from '../config'; 4 | 5 | type LoginProps = {}; 6 | 7 | // Github login Webview 8 | const Login: React.FC = () => { 9 | return ; 10 | }; 11 | export default Login; 12 | -------------------------------------------------------------------------------- /app/src/containers/Splash.tsx: -------------------------------------------------------------------------------- 1 | import { StackNavigationProp } from '@react-navigation/stack'; 2 | import React from 'react'; 3 | import { Div, Text } from 'react-native-magnus'; 4 | import Row from '../components/Row'; 5 | import MainBackground from '../components/Ui/MainBackground'; 6 | import { AppStackParamList } from '../navigators/AppStack'; 7 | import { getData, storeData } from '../utils/AsyncStorage'; 8 | 9 | type SplashProps = { 10 | navigation: StackNavigationProp; 11 | }; 12 | 13 | const Splash: React.FC = ({ navigation }) => { 14 | React.useEffect(() => { 15 | async function startup() { 16 | // await storeData('token', ''); 17 | const token = await getData('token'); 18 | 19 | setTimeout(() => { 20 | if (token) { 21 | navigation.reset({ 22 | index: 0, 23 | routes: [{ name: 'Dashboard' }], 24 | }); 25 | } else { 26 | navigation.reset({ 27 | index: 0, 28 | routes: [{ name: 'Home' }], 29 | }); 30 | } 31 | }, 2000); 32 | } 33 | 34 | startup(); 35 | }, []); 36 | 37 | return ( 38 | 39 | 40 | 41 | React 42 | 43 | 44 | ivated 45 | 46 | 47 | 48 |
49 |
50 | 51 | app 52 | 53 |
54 | 55 | ); 56 | }; 57 | 58 | export default Splash; 59 | -------------------------------------------------------------------------------- /app/src/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import jwt_decode from 'jwt-decode'; 2 | import React, { useContext, useMemo } from 'react'; 3 | import { getData, storeData } from '../utils/AsyncStorage'; 4 | 5 | interface AuthContextInterface { 6 | jwTokenData: JwTokenData | null; 7 | token: string | null; 8 | getCurrentToken: () => Promise; 9 | } 10 | 11 | const AuthContext = React.createContext({ 12 | jwTokenData: null, 13 | token: null, 14 | getCurrentToken: async () => {}, 15 | }); 16 | 17 | interface Props { 18 | children: React.ReactNode; 19 | } 20 | 21 | function AuthProvider(props: Props) { 22 | const [token, setToken] = React.useState(''); 23 | const getCurrentToken = async () => { 24 | setToken((await getData('token')) || ''); 25 | }; 26 | const jwTokenData = useMemo(() => (token ? jwt_decode(token) : null), [token]); 27 | 28 | return ; 29 | } 30 | 31 | function useAuthContext() { 32 | const context = useContext(AuthContext); 33 | return context; 34 | } 35 | 36 | export { AuthProvider, useAuthContext }; 37 | -------------------------------------------------------------------------------- /app/src/hooks/useRequest.ts: -------------------------------------------------------------------------------- 1 | import useSWR, { ConfigInterface, keyInterface } from 'swr'; 2 | import { AxiosResponse } from 'axios'; 3 | 4 | const useRequest = (key: keyInterface, config: ConfigInterface) => { 5 | return useSWR(key, config); 6 | }; 7 | 8 | const useAxiosRequest = (key: keyInterface, config: ConfigInterface) => { 9 | const { data: response, ...rest } = useRequest>(key, config); 10 | const data = response?.data; 11 | 12 | return { data, ...rest }; 13 | }; 14 | 15 | export { useRequest, useAxiosRequest }; 16 | -------------------------------------------------------------------------------- /app/src/navigators/AppStack.tsx: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import React from 'react'; 3 | import Home from '../containers/Home'; 4 | import Splash from '../containers/Splash'; 5 | import Login from '../containers/Login'; 6 | import Dashboard from '../containers/Dashboard'; 7 | import GithubRedirect from '../containers/GithubRedirect'; 8 | 9 | export type AppStackParamList = { 10 | Splash: undefined; 11 | Home: undefined; 12 | Login: undefined; 13 | Dashboard: undefined; 14 | redirect: { code: string }; 15 | }; 16 | 17 | const Stack = createStackNavigator(); 18 | 19 | const AppStack = () => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default AppStack; 32 | -------------------------------------------------------------------------------- /app/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Config } from '../config'; 3 | import { getData, storeData } from '../utils/AsyncStorage'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | 6 | const apiClient = axios.create({ 7 | baseURL: Config.API_URL, 8 | }); 9 | 10 | apiClient.interceptors.request.use( 11 | async (config) => { 12 | config.headers.Authorization = `Bearer ${await getData('token')}`; 13 | return config; 14 | }, 15 | (error) => Promise.reject(error), 16 | ); 17 | 18 | apiClient.interceptors.response.use(undefined, (error) => { 19 | if (error.response.status === 401) { 20 | console.warn('error.response.status', error.response.data); 21 | const navigation = useNavigation(); 22 | storeData('token', ''); 23 | navigation.navigate('Home'); 24 | } 25 | return Promise.reject(error); 26 | }); 27 | 28 | export default apiClient; 29 | -------------------------------------------------------------------------------- /app/src/services/apiAuth.ts: -------------------------------------------------------------------------------- 1 | import apiClient from './api'; 2 | 3 | const getGithubCallback = (code: string) => { 4 | return apiClient.get<{ token: string }>(`app/auth/github/callback?code=${code}`); 5 | }; 6 | 7 | export default { 8 | getGithubCallback, 9 | }; 10 | -------------------------------------------------------------------------------- /app/src/services/apiGithub.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import jwt_decode from 'jwt-decode'; 3 | import { getData } from '../utils/AsyncStorage'; 4 | 5 | const githubClient = axios.create({ 6 | baseURL: 'https://api.github.com', 7 | }); 8 | 9 | githubClient.interceptors.request.use( 10 | async (config) => { 11 | const code = jwt_decode((await getData('token')) || ''); 12 | console.warn('code', code); 13 | if (code) { 14 | const githubToken = code.githubToken; 15 | config.headers.Authorization = `token ${githubToken}`; 16 | config.headers.Accept = 'application/vnd.github.machine-man-preview+json'; 17 | } 18 | return config; 19 | }, 20 | (error) => Promise.reject(error), 21 | ); 22 | 23 | export default githubClient; 24 | -------------------------------------------------------------------------------- /app/src/services/apiRepositories.ts: -------------------------------------------------------------------------------- 1 | import githubClient from './apiGithub'; 2 | import apiClient from './api'; 3 | 4 | export const getRepositories = () => { 5 | return apiClient.get(`/repositories`, { 6 | params: { 7 | filter: 'isConfigured||eq||true', 8 | }, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /app/src/theme/index.tsx: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | colors: { 3 | mainBg: '#24294e', 4 | 5 | // brand50: 'rgba(71,253,167,0.1)', 6 | brand100: '#affed8', 7 | brand200: '#51fdaa', 8 | brand300: '#43ee9c', 9 | brand400: '#3edd90', 10 | brand500: '#47fda7', 11 | brand600: '#33b576', 12 | brand700: '#2c9b65', 13 | brand800: '#227b50', 14 | brand900: '#14482f', 15 | 16 | // secondary50: '#8f92a7', 17 | secondary100: '#7a7d95', 18 | secondary200: '#646883', 19 | secondary300: '#4f5371', 20 | secondary400: '#393e60', 21 | secondary500: '#393e60', 22 | secondary600: '#24294e', 23 | secondary700: '#1d2146', 24 | secondary800: '#191d42', 25 | secondary900: '#16193e', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /app/src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | interface JwTokenData { 2 | githubToken: string; 3 | githubId: string; 4 | userName: string; 5 | userId: User['id']; 6 | } 7 | 8 | interface User { 9 | id: number; 10 | username: string; 11 | githubId: string; 12 | githubToken: string; 13 | } 14 | 15 | interface Repository { 16 | id: number; 17 | name: string; 18 | fullName: string; 19 | githubId: string; 20 | installationId: string; 21 | author: string; 22 | repoImg: string; 23 | createdAt: string; // Date 24 | dependenciesUpdatedAt: string; // Date 25 | repoUrl: string; 26 | user: User; 27 | score: number; 28 | packageJson: PackageJson; 29 | dependencies: { 30 | deps: (Dependency | PrefixedDependency)[]; 31 | } | null; 32 | branch: string; 33 | path?: string; 34 | framework: FrameworkTag; 35 | } 36 | 37 | type Dependency = [string, string, string, string, DependencyType, string]; 38 | type PrefixedDependency = { [prefix: string]: Dependency[] }; 39 | 40 | type DependencyType = 'dependencies' | 'devDependencies'; 41 | 42 | type PackageJson = { 43 | dependencies: object; 44 | devDependencies: object; 45 | } | null; 46 | 47 | type FrameworkTag = 'react' | 'react native' | 'vue' | 'angular' | 'next.js' | 'nest.js' | 'express'; 48 | 49 | interface GithubAccount { 50 | login: string; 51 | avatarUrl: string; 52 | id: string; 53 | } 54 | 55 | interface GithubInstallationRepository { 56 | id: number; 57 | name: string; 58 | fullName: string; 59 | private: boolean; 60 | } 61 | 62 | interface GithubInstallation { 63 | id: number; 64 | owner: GithubAccount; 65 | repositories: GithubInstallationRepository[]; 66 | } 67 | 68 | interface GithubBranch { 69 | name: string; 70 | } 71 | -------------------------------------------------------------------------------- /app/src/utils/AsyncStorage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | 3 | export const getData = async (key: string) => { 4 | try { 5 | const value = await AsyncStorage.getItem(key); 6 | if (value !== null) { 7 | return value; 8 | } else { 9 | return null; 10 | } 11 | } catch (e) { 12 | console.error('getData -> e', e); 13 | } 14 | }; 15 | 16 | export const storeData = async (key: string, data: object | string) => { 17 | try { 18 | if (typeof data === 'object') { 19 | const jsonData = JSON.stringify(data); 20 | return await AsyncStorage.setItem(key, jsonData); 21 | } 22 | return await AsyncStorage.setItem(key, data); 23 | } catch (e) { 24 | console.error('storeData -> e', e); 25 | } 26 | }; 27 | 28 | export const APP_OPENED_TIMES_KEY = 'appOpenedTimes'; 29 | -------------------------------------------------------------------------------- /front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-optional-chaining"] 3 | } 4 | -------------------------------------------------------------------------------- /front/.env.dist: -------------------------------------------------------------------------------- 1 | REACT_APP_API_HOST=http://localhost:3000 2 | REACT_APP_GITHUB_APP_NAME=dev-reactivated-app 3 | REACT_APP_MAX_REPOS=5 4 | REACT_APP_IS_BETA=true -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /front/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "always", 4 | "semi": false, 5 | "bracketSpacing": true, 6 | "trailingComma": "all" 7 | } -------------------------------------------------------------------------------- /front/config-overrides.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { addWebpackAlias, override, useBabelRc } = require('customize-cra') 3 | const { addReactRefresh } = require('customize-cra-react-refresh') 4 | 5 | module.exports = override( 6 | addWebpackAlias({ 7 | '@api': path.resolve('src', 'api'), 8 | '@assets': path.resolve('src', 'assets'), 9 | '@components': path.resolve('src', 'components'), 10 | '@containers': path.resolve('src', 'containers'), 11 | '@contexts': path.resolve('src', 'contexts'), 12 | '@hooks': path.resolve('src', 'hooks'), 13 | '@utils': path.resolve('src', 'utils'), 14 | }), 15 | addReactRefresh({ disableRefreshCheck: true }), 16 | useBabelRc(), 17 | ) 18 | -------------------------------------------------------------------------------- /front/netlify.toml: -------------------------------------------------------------------------------- 1 | [context."staging".environment] 2 | REACT_APP_API_HOST="https://reactivated-app-staging.herokuapp.com" 3 | REACT_APP_GITHUB_APP_NAME="staging-reactivated-app" 4 | 5 | [context."master".environment] 6 | REACT_APP_API_HOST="https://reactivated-app.herokuapp.com" 7 | REACT_APP_GITHUB_APP_NAME="reactivated-app" 8 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/core": "^0.8.0", 7 | "@emotion/core": "^10.0.35", 8 | "@emotion/styled": "^10.0.27", 9 | "@rehooks/local-storage": "^2.4.0", 10 | "axios": "^0.20.0", 11 | "date-fns": "^2.16.1", 12 | "emotion-theming": "^10.0.27", 13 | "framer-motion": "^2.6.5", 14 | "jwt-decode": "^2.2.0", 15 | "node-sass": "^4.14.1", 16 | "query-string": "^6.13.1", 17 | "react": "^16.13.1", 18 | "react-countup": "^4.3.3", 19 | "react-dom": "^16.13.1", 20 | "react-icons": "^3.11.0", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "^3.4.3", 23 | "react-stickynode": "^3.0.3", 24 | "semver": "^7.3.2", 25 | "swr": "^0.3.2" 26 | }, 27 | "scripts": { 28 | "start": "PORT=3007 react-app-rewired start", 29 | "build": "react-app-rewired build", 30 | "test": "react-app-rewired test", 31 | "eject": "react-scripts eject", 32 | "typescheck": "tsc --noEmit --jsx react --skipLibCheck", 33 | "get-types": "swagger-typescript-api -p http://localhost:3000/swagger-json -o ./src/typings -n entities2.d.ts --no-client && prettier --write ./src/typings/entities2.d.ts" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 52 | "@types/axios": "^0.14.0", 53 | "@types/jest": "^26.0.12", 54 | "@types/jwt-decode": "^2.2.1", 55 | "@types/node": "^14.6.2", 56 | "@types/react": "^16.9.49", 57 | "@types/react-dom": "^16.9.8", 58 | "@types/react-router": "^5.1.8", 59 | "@types/react-router-dom": "^5.1.5", 60 | "@types/react-stickynode": "^3.0.0", 61 | "@types/semver": "^7.3.3", 62 | "customize-cra": "^1.0.0", 63 | "customize-cra-react-refresh": "^1.1.0", 64 | "husky": "^4.2.5", 65 | "lint-staged": "^10.2.13", 66 | "prettier": "^2.1.1", 67 | "pretty-quick": "^3.0.0", 68 | "react-app-rewired": "^2.1.6", 69 | "swagger-typescript-api": "^1.12.0", 70 | "typescript": "^4.0.2" 71 | }, 72 | "husky": { 73 | "hooks": { 74 | "pre-commit": "lint-staged && yarn typescheck && pretty-quick --staged" 75 | } 76 | }, 77 | "lint-staged": { 78 | "src/**/*.{js,jsx,ts,tsx,json,md}": [ 79 | "eslint src/**/*.{ts,tsx,js} --fix", 80 | "git add" 81 | ], 82 | "src/**/*.{css,scss}": [ 83 | "prettier --write", 84 | "git add" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /front/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /front/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /front/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /front/public/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/app.png -------------------------------------------------------------------------------- /front/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/apple-touch-icon.png -------------------------------------------------------------------------------- /front/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /front/public/dep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/dep.png -------------------------------------------------------------------------------- /front/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/favicon-16x16.png -------------------------------------------------------------------------------- /front/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/favicon-32x32.png -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/favicon.ico -------------------------------------------------------------------------------- /front/public/feat-improvement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/feat-improvement.png -------------------------------------------------------------------------------- /front/public/feat-pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/feat-pr.png -------------------------------------------------------------------------------- /front/public/feat-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/feat-repo.png -------------------------------------------------------------------------------- /front/public/images/og-graph-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/images/og-graph-color.png -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Reactivated.app - Keep your JS app up to date 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | 57 | 58 | 59 | 63 | 64 | 65 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /front/public/items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/items.png -------------------------------------------------------------------------------- /front/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/logo192.png -------------------------------------------------------------------------------- /front/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/logo512.png -------------------------------------------------------------------------------- /front/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /front/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/mstile-150x150.png -------------------------------------------------------------------------------- /front/public/p4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/p4.png -------------------------------------------------------------------------------- /front/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /front/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reactivated.app", 3 | "short_name": "React JSX visual editor for Chakra UI", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /front/public/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/front/public/update.png -------------------------------------------------------------------------------- /front/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react' 2 | import AuthApp from '@containers/Layout/AuthApp' 3 | import { useAuth } from '@contexts/AuthContext' 4 | import { BrowserRouter, Route, Switch } from 'react-router-dom' 5 | import Demo from './containers/Demo' 6 | 7 | const GithubRedirect = lazy(() => import('./containers/GithubRedirect')) 8 | const Home = lazy(() => import('./containers/Home')) 9 | 10 | function App() { 11 | const { token } = useAuth() 12 | 13 | return ( 14 | }> 15 | 16 | 17 | 18 | 19 | { 22 | if (token) { 23 | return 24 | } 25 | 26 | return 27 | }} 28 | /> 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /front/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const apiClient = axios.create({ 4 | baseURL: process.env.REACT_APP_API_HOST, 5 | }) 6 | 7 | apiClient.interceptors.request.use( 8 | (config) => { 9 | config.headers.Authorization = `Bearer ${localStorage.getItem('token')}` 10 | return config 11 | }, 12 | (error) => Promise.reject(error), 13 | ) 14 | 15 | apiClient.interceptors.response.use(undefined, (error) => { 16 | if (error.response.status === 401) { 17 | window.localStorage.removeItem('token') 18 | window.location.assign('/') 19 | } 20 | return Promise.reject(error) 21 | }) 22 | 23 | export default apiClient 24 | -------------------------------------------------------------------------------- /front/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import API from './api' 2 | 3 | const getGithubCallback = (code: string) => { 4 | return API.get<{ token: string }>(`/auth/github/callback?code=${code}`) 5 | } 6 | 7 | export default { 8 | getGithubCallback, 9 | } 10 | -------------------------------------------------------------------------------- /front/src/api/github.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import jwt_decode from 'jwt-decode' 3 | 4 | const githubClient = axios.create({ 5 | baseURL: 'https://api.github.com', 6 | }) 7 | 8 | githubClient.interceptors.request.use( 9 | (config) => { 10 | const code = jwt_decode(localStorage.getItem('token')!) 11 | const githubToken = code.githubToken 12 | config.headers.Authorization = `token ${githubToken}` 13 | config.headers.Accept = 'application/vnd.github.machine-man-preview+json' 14 | return config 15 | }, 16 | (error) => Promise.reject(error), 17 | ) 18 | 19 | export default githubClient 20 | -------------------------------------------------------------------------------- /front/src/api/installations.ts: -------------------------------------------------------------------------------- 1 | import GithubAPI from './github' 2 | 3 | type Installations = { 4 | installations: Array<{ 5 | id: number 6 | account: { 7 | login: string 8 | avatar_url: string 9 | id: string 10 | } 11 | }> 12 | } 13 | 14 | type InstallationRepositories = { 15 | repositories: Array<{ 16 | id: number 17 | name: string 18 | full_name: string 19 | private: boolean 20 | }> 21 | } 22 | 23 | export const getUserInstallations = async (): Promise => { 24 | const timestamp = Math.round(+new Date() / 1000) 25 | const { data: installationsData } = await GithubAPI.get( 26 | `/user/installations?t=${timestamp}`, 27 | ) 28 | 29 | const installations = installationsData.installations 30 | 31 | const repositories = await Promise.all( 32 | installations.map((installation) => { 33 | return GithubAPI.get( 34 | `/user/installations/${installation.id}/repositories?t=${timestamp}`, 35 | ).then((res) => res.data) 36 | }), 37 | ) 38 | 39 | return installations.map((installation, idx) => { 40 | return { 41 | id: installation.id, 42 | owner: { 43 | avatarUrl: installation.account.avatar_url, 44 | login: installation.account.login, 45 | id: installation.account.id, 46 | }, 47 | repositories: repositories[idx].repositories.map< 48 | GithubInstallationRepository 49 | >((repository) => { 50 | return { 51 | id: repository.id, 52 | fullName: repository.full_name, 53 | name: repository.name, 54 | private: repository.private, 55 | } 56 | }), 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /front/src/api/repositories.ts: -------------------------------------------------------------------------------- 1 | import API from './api' 2 | import GithubAPI from './github' 3 | import { AxiosRequestConfig } from 'axios' 4 | import { Repository } from '../typings/entities' 5 | 6 | export const getRepositories = () => { 7 | return API.get(`/repositories`, { 8 | params: { 9 | sort: ['score,ASC'], 10 | }, 11 | }) 12 | } 13 | 14 | export const getRepository = (repositoryId: string) => { 15 | return API.get(`/repositories/${repositoryId}`, { 16 | params: { join: 'pullRequests' }, 17 | }) 18 | } 19 | 20 | export const getRepositoryBranches = (fullName: string) => { 21 | return GithubAPI.get(`/repos/${fullName}/branches`, { 22 | params: { per_page: 100 }, 23 | }) 24 | } 25 | 26 | interface ConfigureRepoParams { 27 | id: Repository['id'] 28 | data: Pick 29 | } 30 | 31 | export const configureRepository = (params: ConfigureRepoParams) => { 32 | return API.put( 33 | `/repositories/${params.id}/configure`, 34 | params.data, 35 | ) 36 | } 37 | 38 | interface SetupRepoParams { 39 | data: Pick 40 | } 41 | 42 | export const setupRepository = (params: SetupRepoParams) => { 43 | return API.post(`/repositories/setup`, params.data) 44 | } 45 | 46 | export const findRepositoriesByName = (name: string) => { 47 | return API.get(`/repositories`, { 48 | params: { 49 | filter: `fullName||eq||${name}`, 50 | }, 51 | }) 52 | } 53 | 54 | export const recomputeDeps = (repoId: Repository['id']) => { 55 | return API.get(`/repositories/${repoId}/compute_deps`) 56 | } 57 | 58 | export const syncRepository = (fullName: string) => { 59 | return API.get(`/repositories/sync/${fullName}`) 60 | } 61 | 62 | export const createUpgradePR = ( 63 | fullName: string, 64 | data: { updatedDependencies: string[]; repoId: number }, 65 | ) => { 66 | return API.post(`repositories/${fullName}/pulls`, data) 67 | } 68 | 69 | export const getPullRequests = ( 70 | repoId: string, 71 | config?: AxiosRequestConfig, 72 | ) => { 73 | return API.get(`repositories/${repoId}/pull-requests`, config) 74 | } 75 | 76 | export const deleteRepository = (repoId: number) => { 77 | return API.delete(`repositories/${repoId.toString()}`) 78 | } 79 | -------------------------------------------------------------------------------- /front/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import API from './api' 2 | 3 | export const deleteAccount = (userId: number) => { 4 | return API.post(`/users/${userId}/delete-account`) 5 | } 6 | -------------------------------------------------------------------------------- /front/src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Stack, Box, Badge, BoxProps } from '@chakra-ui/core' 3 | import { motion } from 'framer-motion' 4 | import { useHistory } from 'react-router' 5 | import { useRepository } from '@contexts/RepositoryContext' 6 | import { useCallback } from 'react' 7 | 8 | interface IProps { 9 | outdatedCount?: number 10 | pullRequestCount: number 11 | repositoryId: number 12 | activeTabName: 'dependencies' | 'pr' 13 | } 14 | 15 | interface ITabProps { 16 | isActive?: boolean 17 | } 18 | 19 | export const Tab: React.FC = ({ isActive, ...rest }) => ( 20 | 32 | ) 33 | 34 | const MotionBadge = motion.custom(Badge) 35 | 36 | const AppBar = React.forwardRef( 37 | ({ outdatedCount, pullRequestCount, repositoryId, activeTabName }, ref) => { 38 | const history = useHistory() 39 | const { prCount } = useRepository() 40 | const [isAnimated, setIsAnimated] = useState(false) 41 | 42 | useEffect(() => { 43 | if (prCount > 0) { 44 | setIsAnimated(true) 45 | setTimeout(() => { 46 | setIsAnimated(false) 47 | }, 1200) 48 | } 49 | }, [prCount]) 50 | 51 | const goToViewRepo = useCallback(() => { 52 | history.push(`/repo/${repositoryId}`) 53 | }, [history, repositoryId]) 54 | const isDependenciesActive = React.useMemo( 55 | () => activeTabName === 'dependencies', 56 | [activeTabName], 57 | ) 58 | 59 | const isPullRequestActive = React.useMemo(() => activeTabName === 'pr', [ 60 | activeTabName, 61 | ]) 62 | const goToPullRequest = useCallback(() => { 63 | history.push(`/repo/${repositoryId}/pull-requests`) 64 | }, [history, repositoryId]) 65 | 66 | return ( 67 | 68 | 69 | {outdatedCount ? ( 70 | <> 71 | Outdated Dependencies{' '} 72 | 73 | {outdatedCount} 74 | 75 | 76 | ) : ( 77 | <>Dependencies Outdated 78 | )} 79 | 80 | 81 | Pull Requests{' '} 82 | 91 | {pullRequestCount + prCount} 92 | 93 | 94 | 95 | ) 96 | }, 97 | ) 98 | 99 | export default React.memo(AppBar) 100 | -------------------------------------------------------------------------------- /front/src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box, BoxProps } from '@chakra-ui/core' 3 | 4 | const Container = React.forwardRef( 5 | ({ children, ...rest }, ref) => ( 6 | 17 | ), 18 | ) 19 | 20 | export default Container 21 | -------------------------------------------------------------------------------- /front/src/components/Demo/DemoItem.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, IconButton, Image, Text } from '@chakra-ui/core' 2 | import React from 'react' 3 | import { Column, Row } from '../Flex' 4 | import FrameworkTag from '../FrameworkTag/FrameworkTag' 5 | import LoadBar from '../LoadBar' 6 | import LoadScore from '../LoadScore' 7 | import { Link } from 'react-router-dom' 8 | import { motion } from 'framer-motion' 9 | import { reactRepo } from './model' 10 | 11 | const DemoItem = React.forwardRef((props, ref) => { 12 | const repository = reactRepo 13 | 14 | return ( 15 | 16 | 28 | {repository.score > 0 && } 29 | 30 | 36 | 37 | {repository.name} 43 | 44 | 45 | {repository.name} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | }) 65 | 66 | const DemoItemMotion = motion.custom(DemoItem) 67 | 68 | export default DemoItemMotion 69 | -------------------------------------------------------------------------------- /front/src/components/DependenciesList/DependenciesList.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from '@chakra-ui/core' 2 | import React, { useRef } from 'react' 3 | import DependencyItem from './DependencyItem' 4 | 5 | interface IProps { 6 | dependencies: Dependency[] 7 | selectedDependencies: { 8 | [key: string]: 'stable' | 'latest' 9 | } 10 | onDependencySelected: ( 11 | checked: boolean, 12 | name: string, 13 | type: 'stable' | 'latest', 14 | ) => void 15 | } 16 | 17 | const DependenciesList: React.FC = ({ 18 | dependencies, 19 | selectedDependencies, 20 | onDependencySelected, 21 | }) => { 22 | const lastPrefixRef = useRef() 23 | const onSelect = React.useCallback( 24 | (checked: boolean, name: string, type: 'stable' | 'latest'): void => { 25 | onDependencySelected(checked, name, type) 26 | }, 27 | [onDependencySelected], 28 | ) 29 | const isStableChecked = React.useCallback( 30 | (depName: string) => { 31 | return selectedDependencies[depName] === 'stable' 32 | }, 33 | [selectedDependencies], 34 | ) 35 | const isLatestChecked = React.useCallback( 36 | (depName: string) => { 37 | return selectedDependencies[depName] === 'latest' 38 | }, 39 | [selectedDependencies], 40 | ) 41 | 42 | return ( 43 | 44 | 45 | {dependencies.map((dep) => { 46 | const displayPrefix = 47 | lastPrefixRef.current !== dep.prefix && dep.prefix 48 | if (dep.prefix) { 49 | lastPrefixRef.current = dep.prefix 50 | } 51 | 52 | return ( 53 | 54 | {displayPrefix && ( 55 | 56 | {dep.prefix} 57 | 58 | )} 59 | 65 | 66 | ) 67 | })} 68 | 69 | 70 | ) 71 | } 72 | 73 | export default DependenciesList 74 | -------------------------------------------------------------------------------- /front/src/components/DependenciesList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './DependenciesList' 2 | -------------------------------------------------------------------------------- /front/src/components/DependencyTabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tabs, TabPanels, TabPanel } from '@chakra-ui/core' 3 | import { DependenciesTypeTabs } from '@containers/Repository/ViewRepo' 4 | import DependenciesList from '@components/DependenciesList' 5 | 6 | interface IProps { 7 | dependencies: Dependency[] 8 | devDependencies: Dependency[] 9 | onDependencySelected: ( 10 | checked: boolean, 11 | name: string, 12 | type: 'stable' | 'latest', 13 | ) => void 14 | selectedDependencies: { 15 | [key: string]: 'stable' | 'latest' 16 | } 17 | } 18 | 19 | const DependencyTabs: React.FC = ({ 20 | dependencies, 21 | devDependencies, 22 | onDependencySelected, 23 | selectedDependencies, 24 | }) => { 25 | return ( 26 | 32 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | export default DependencyTabs 58 | -------------------------------------------------------------------------------- /front/src/components/FirstAppButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Button, Text, Image, Flex } from '@chakra-ui/core' 3 | import { FaPlug } from 'react-icons/fa' 4 | import { Link } from 'react-router-dom' 5 | import { motion } from 'framer-motion' 6 | 7 | const MotionImage = motion.custom(Image) 8 | 9 | const FirstAppButton = () => { 10 | const [isHover, setHovered] = useState(false) 11 | 12 | return ( 13 | <> 14 | 21 | 31 | 32 | 39 | No Reactivated app? 40 | 41 | 42 | You have no reactivated app yet! Start now by adding your first app 43 | from a GitHub repository 44 | 45 | 46 | 47 | 63 | 64 | 65 | 66 | ) 67 | } 68 | 69 | export default FirstAppButton 70 | -------------------------------------------------------------------------------- /front/src/components/Flex/Flex.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex, FlexProps } from '@chakra-ui/core' 3 | 4 | interface Props extends FlexProps {} 5 | 6 | export const Row = (props: Props) => 7 | 8 | export const Column = (props: Props) => 9 | -------------------------------------------------------------------------------- /front/src/components/Flex/index.ts: -------------------------------------------------------------------------------- 1 | export { Column, Row } from './Flex' 2 | -------------------------------------------------------------------------------- /front/src/components/FormInput/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import { 3 | FormControl, 4 | FormLabel, 5 | FormHelperText, 6 | FormControlProps, 7 | } from '@chakra-ui/core' 8 | 9 | interface Props extends FormControlProps { 10 | label?: string 11 | id?: string 12 | children: React.ReactNode 13 | helperText?: React.ReactNode 14 | } 15 | 16 | const FormInput: React.FC = memo( 17 | ({ label, id, children, helperText, ...props }) => { 18 | return ( 19 | 20 | {!!label && {label}} 21 | {children} 22 | {!!helperText && {helperText}} 23 | 24 | ) 25 | }, 26 | ) 27 | 28 | export default FormInput 29 | -------------------------------------------------------------------------------- /front/src/components/FormInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FormInput' 2 | -------------------------------------------------------------------------------- /front/src/components/FrameworkTag/FrameworkTag.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from '@chakra-ui/core' 2 | import React from 'react' 3 | import { Repository } from '../../typings/entities' 4 | 5 | interface FrameworkTagProps extends Pick {} 6 | 7 | const FrameworkTag: React.FC = ({ framework }) => { 8 | const getFrameworkColor = (framework: FrameworkTag) => { 9 | switch (framework) { 10 | case 'react': 11 | case 'react native': 12 | return '#61dafb' 13 | 14 | case 'vue': 15 | return '#4fc08d' 16 | 17 | case 'angular': 18 | case 'nest.js': 19 | return 'red.500' 20 | 21 | default: 22 | return '#000' 23 | } 24 | } 25 | 26 | return ( 27 | 28 | 35 | {framework} 36 | 37 | ) 38 | } 39 | 40 | export default FrameworkTag 41 | -------------------------------------------------------------------------------- /front/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Link, Stack, Text } from '@chakra-ui/core' 2 | import React from 'react' 3 | import { FaPlug } from 'react-icons/fa' 4 | 5 | const Header: React.FC = ({ children }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | Reactivated 14 | 15 | 16 | ⬤ 17 | 18 | 19 | app 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | ) 27 | } 28 | 29 | export default Header 30 | -------------------------------------------------------------------------------- /front/src/components/Header/HeaderLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from '@chakra-ui/core' 3 | import { FaGithub } from 'react-icons/fa' 4 | 5 | const HeaderLinks: React.FC<{ loading: boolean }> = ({ loading }) => { 6 | return ( 7 | <> 8 | 20 | 21 | 33 | 34 | ) 35 | } 36 | 37 | export default HeaderLinks 38 | -------------------------------------------------------------------------------- /front/src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Header' 2 | -------------------------------------------------------------------------------- /front/src/components/InstallationRepositories/InstallationRepositories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react' 2 | import { Row, Column } from '@components/Flex' 3 | import OrganizationsMenuList from '@components/OrganizationsMenuList' 4 | import { Box } from '@chakra-ui/core' 5 | import InstallationRepositoriesList from '@components/InstallationRepositoriesList' 6 | 7 | interface Props { 8 | installations: GithubInstallation[] 9 | onSelectRepo: (fullName: string) => void 10 | } 11 | 12 | const InstallationRepositories: React.FC = ({ 13 | installations, 14 | onSelectRepo, 15 | }) => { 16 | const owners = useMemo(() => { 17 | return installations.map((installation) => installation.owner) 18 | }, [installations]) 19 | const [selectedOwner, setSelectedOwner] = useState(owners[0]) 20 | 21 | const onSelectOwner = (owner: GithubAccount) => { 22 | if (owner.id !== selectedOwner.id) { 23 | setSelectedOwner(owner) 24 | } 25 | } 26 | 27 | const repos = useMemo(() => { 28 | const installation = installations.find( 29 | (install) => install.owner.id === selectedOwner.id, 30 | ) 31 | 32 | return installation?.repositories || [] 33 | }, [installations, selectedOwner]) 34 | 35 | const onSelectRepository = (id: GithubInstallationRepository['id']) => { 36 | const repo = repos.find((repo) => repo.id === id) 37 | 38 | if (repo) { 39 | onSelectRepo(repo.fullName) 40 | } 41 | } 42 | 43 | return ( 44 | 45 | 46 | owner.id !== selectedOwner.id)} 48 | selectedOrg={selectedOwner} 49 | onSelect={onSelectOwner} 50 | /> 51 | 52 | 53 | 57 | 58 | 59 | ) 60 | } 61 | 62 | export default InstallationRepositories 63 | -------------------------------------------------------------------------------- /front/src/components/InstallationRepositories/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './InstallationRepositories' 2 | -------------------------------------------------------------------------------- /front/src/components/InstallationRepositoriesList/InstallationRepositoriesList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { List } from '@chakra-ui/core' 3 | import InstallationRepositoriesListItem from './InstallationRepositoriesListItem' 4 | 5 | interface Props { 6 | repositories: GithubInstallationRepository[] 7 | onSelect: (id: GithubInstallationRepository['id']) => void 8 | } 9 | 10 | const InstallationRepositoriesList: React.FC = ({ 11 | repositories, 12 | onSelect, 13 | }) => { 14 | return ( 15 | 16 | {repositories.map((repo) => ( 17 | 22 | ))} 23 | 24 | ) 25 | } 26 | 27 | export default InstallationRepositoriesList 28 | -------------------------------------------------------------------------------- /front/src/components/InstallationRepositoriesList/InstallationRepositoriesListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import { Row } from '@components/Flex' 3 | import { Box, Text, Icon, ListItem, Spinner } from '@chakra-ui/core' 4 | import { FaGithub } from 'react-icons/fa' 5 | 6 | interface Props extends GithubInstallationRepository { 7 | onSelect: (id: GithubInstallationRepository['id']) => void 8 | } 9 | 10 | const InstallationRepositoriesListItem: React.FC = memo( 11 | ({ fullName, private: isPrivate, id, onSelect, ...props }) => { 12 | const [loading, setLoading] = React.useState(false) 13 | 14 | const onPress = () => { 15 | setLoading(true) 16 | onSelect(id) 17 | } 18 | 19 | return ( 20 | 27 | 28 | 29 | {loading ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 | 35 | {fullName} 36 | 37 | 38 | 39 | {isPrivate && ( 40 | 41 | 42 | 43 | Private 44 | 45 | 46 | )} 47 | 48 | 49 | 50 | 51 | ) 52 | }, 53 | ) 54 | 55 | export default InstallationRepositoriesListItem 56 | -------------------------------------------------------------------------------- /front/src/components/InstallationRepositoriesList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './InstallationRepositoriesList' 2 | -------------------------------------------------------------------------------- /front/src/components/LoadBar.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/core' 2 | import { motion } from 'framer-motion' 3 | import React from 'react' 4 | import { Repository } from '../typings/entities' 5 | 6 | interface Props extends Pick {} 7 | 8 | const MotionBox = motion.custom(Box) 9 | const LoadBar: React.FC = ({ score }) => { 10 | return ( 11 | 20 | 31 | 43 | 44 | ) 45 | } 46 | 47 | export default LoadBar 48 | -------------------------------------------------------------------------------- /front/src/components/LoadScore.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, Text } from '@chakra-ui/core' 2 | import React from 'react' 3 | import CountUp from 'react-countup' 4 | import { useRepository } from '../contexts/RepositoryContext' 5 | import { Repository } from '../typings/entities' 6 | 7 | interface Props extends Pick { 8 | isSmall?: boolean 9 | } 10 | 11 | const LoadScore = React.forwardRef( 12 | ({ score, isSmall = false }, ref) => { 13 | const { repository, score: scoreCtxt } = useRepository() 14 | return ( 15 | 16 | 17 | 18 | {repository && scoreCtxt >= repository.score ? ( 19 | 25 | ) : ( 26 | score 27 | )} 28 | 29 | 30 | % 31 | 32 | 33 | 34 | Reactivated 35 | 36 | 37 | ) 38 | }, 39 | ) 40 | 41 | export default LoadScore 42 | -------------------------------------------------------------------------------- /front/src/components/OrganizationsMenuList/OrganizationMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import { MenuItem, Image, Text, MenuItemProps } from '@chakra-ui/core' 3 | 4 | interface Props extends GithubAccount, Omit { 5 | onPress?: (account: GithubAccount) => void 6 | as?: React.ComponentType 7 | } 8 | 9 | const OrganizationMenuItem: React.FC = memo( 10 | ({ onPress, id, login, avatarUrl, as: Comp = MenuItem, ...props }) => { 11 | const onSelect = () => { 12 | if (onPress) { 13 | onPress({ id, login, avatarUrl }) 14 | } 15 | } 16 | 17 | return ( 18 | 19 | {login} 20 | 21 | {login} 22 | 23 | 24 | ) 25 | }, 26 | ) 27 | 28 | export default OrganizationMenuItem 29 | -------------------------------------------------------------------------------- /front/src/components/OrganizationsMenuList/OrganizationsMenuList.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import SelectMenu from '@components/SelectMenu' 3 | import OrganizationMenuItem from './OrganizationMenuItem' 4 | import { Row } from '@components/Flex' 5 | 6 | interface Props { 7 | orgs: GithubAccount[] 8 | onSelect: (account: GithubAccount) => void 9 | selectedOrg: GithubAccount 10 | } 11 | 12 | const OrganizationsMenuList: React.FC = memo( 13 | ({ orgs, onSelect, selectedOrg }) => { 14 | const keyExtractor = (item: GithubAccount) => { 15 | return item.id.toString() 16 | } 17 | 18 | const renderItem = (item: GithubAccount) => { 19 | return 20 | } 21 | 22 | const renderSelected = () => { 23 | return ( 24 | 30 | ) 31 | } 32 | 33 | return ( 34 | 35 | items={orgs} 36 | keyExtractor={keyExtractor} 37 | renderMenuItem={renderItem} 38 | renderSelected={renderSelected} 39 | /> 40 | ) 41 | }, 42 | ) 43 | 44 | export default OrganizationsMenuList 45 | -------------------------------------------------------------------------------- /front/src/components/OrganizationsMenuList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './OrganizationsMenuList' 2 | -------------------------------------------------------------------------------- /front/src/components/PrivateRoute/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { RouteProps, Route, Redirect } from 'react-router' 3 | import { useAuth } from '@contexts/AuthContext' 4 | 5 | interface Props extends RouteProps {} 6 | 7 | const PrivateRoute = (props: Props) => { 8 | const { token } = useAuth() 9 | 10 | if (token) { 11 | return 12 | } 13 | 14 | return 15 | } 16 | 17 | export default PrivateRoute 18 | -------------------------------------------------------------------------------- /front/src/components/PrivateRoute/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PrivateRoute' 2 | -------------------------------------------------------------------------------- /front/src/components/PullRequest/PullRequestItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Box, 4 | Stack, 5 | Tag, 6 | Flex, 7 | Text, 8 | Link, 9 | Spinner, 10 | Tooltip, 11 | } from '@chakra-ui/core' 12 | import { PullRequest } from '@typings/entities' 13 | 14 | type PullRequestItemProps = { 15 | pullRequest: PullRequest 16 | } 17 | 18 | const PullRequestItem: React.FC = ({ pullRequest }) => { 19 | const getColorFromStatus = (status: string) => { 20 | switch (status) { 21 | case 'done': 22 | return 'green.300' 23 | case 'pending': 24 | return 'yellow.300' 25 | case 'closed': 26 | case 'error': 27 | return 'red.300' 28 | case 'merged': 29 | return 'brand.500' 30 | default: 31 | return 'black' 32 | } 33 | } 34 | 35 | return ( 36 | 46 | 47 | 48 | {pullRequest.log ? ( 49 | 56 | 61 | 62 | ) : ( 63 | 68 | )} 69 | 70 | {pullRequest.status === 'pending' ? ( 71 | <> 72 | {pullRequest.status} 73 | 74 | ) : ( 75 | pullRequest.status 76 | )} 77 | 78 | 79 | 80 | {pullRequest.url ? ( 81 | 82 | {pullRequest.branchName} 83 | 84 | ) : ( 85 | {pullRequest.branchName} 86 | )} 87 | 88 | 89 | ) 90 | } 91 | 92 | export default PullRequestItem 93 | -------------------------------------------------------------------------------- /front/src/components/RepoConfigForm/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RepoConfigForm' 2 | -------------------------------------------------------------------------------- /front/src/components/RepositoriesList/RepositoriesList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex } from '@chakra-ui/core' 3 | import { Link } from 'react-router-dom' 4 | import RepositoryListItem from './RepositoryListItem' 5 | import { Repository } from '../../typings/entities' 6 | import RepositoryListEmpty from './RepositoryListEmpty' 7 | import { getMaxRepositories } from '@containers/Dashboard' 8 | import { motion } from 'framer-motion' 9 | 10 | interface Props { 11 | repositories: Repository[] 12 | } 13 | 14 | const MotionRepositoryListItem = motion.custom(RepositoryListItem) 15 | const MotionRepositoryListEmpty = motion.custom(RepositoryListEmpty) 16 | 17 | const RepositoriesList = ({ repositories }: Props) => { 18 | const emptyBlockCount = getMaxRepositories() - repositories.length 19 | 20 | return ( 21 | 22 | {repositories.map((repository) => { 23 | if (repository.isConfigured) { 24 | return ( 25 | 26 | 30 | 31 | ) 32 | } else { 33 | return ( 34 | 35 | ) 36 | } 37 | })} 38 | {[...new Array(emptyBlockCount < 0 ? 0 : emptyBlockCount)].map( 39 | (_, idx) => ( 40 | 44 | ), 45 | )} 46 | 47 | ) 48 | } 49 | 50 | export default RepositoriesList 51 | -------------------------------------------------------------------------------- /front/src/components/RepositoriesList/RepositoryListEmpty.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex, Icon, Box, FlexProps } from '@chakra-ui/core' 3 | import { Link } from 'react-router-dom' 4 | 5 | const RepositoryListEmpty = React.forwardRef((props, ref) => ( 6 | 7 | 23 | 24 | Add new app 25 | 26 | 27 | 28 | )) 29 | 30 | export default RepositoryListEmpty 31 | -------------------------------------------------------------------------------- /front/src/components/RepositoriesList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RepositoriesList' 2 | -------------------------------------------------------------------------------- /front/src/components/Screenshot.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React from 'react' 3 | import { Box, Image } from '@chakra-ui/core' 4 | 5 | const Screenshot = () => { 6 | return ( 7 | 8 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Reactivated.app UI 30 | 31 | 32 | ) 33 | } 34 | 35 | export default Screenshot 36 | -------------------------------------------------------------------------------- /front/src/components/SelectMenu/SelectMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Menu, 4 | MenuButton, 5 | Button, 6 | MenuList, 7 | MenuButtonProps, 8 | } from '@chakra-ui/core' 9 | 10 | interface Props extends MenuButtonProps { 11 | renderMenuItem: (item: T, index: number) => React.ReactElement 12 | renderSelected: () => React.ReactElement 13 | items: T[] 14 | keyExtractor: (item: T, index: number) => string 15 | } 16 | 17 | const SelectMenu = ({ 18 | renderMenuItem, 19 | renderSelected, 20 | items, 21 | keyExtractor, 22 | ...props 23 | }: Props) => { 24 | return ( 25 | 26 | 33 | {renderSelected()} 34 | 35 | 36 | {items.map((item, index) => 37 | React.cloneElement(renderMenuItem(item, index), { 38 | key: keyExtractor(item, index), 39 | }), 40 | )} 41 | 42 | 43 | ) 44 | } 45 | 46 | export default SelectMenu 47 | -------------------------------------------------------------------------------- /front/src/components/SelectMenu/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './SelectMenu' 2 | -------------------------------------------------------------------------------- /front/src/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import * as RepositoriesAPI from '@api/repositories' 2 | import { Badge, Box, Flex, Image, Text } from '@chakra-ui/core' 3 | import Container from '@components/Container' 4 | import FirstAppButton from '@components/FirstAppButton' 5 | import RepositoriesList from '@components/RepositoriesList' 6 | import { useAxiosRequest } from '@hooks/useRequest' 7 | import React from 'react' 8 | import { Repository } from '../typings/entities' 9 | import DashboardSkeleton from './DashboardSkeleton' 10 | 11 | export const getMaxRepositories = () => { 12 | return parseInt(process.env.REACT_APP_MAX_REPOS || '5', 10) 13 | } 14 | 15 | const Home = () => { 16 | let { data: repositories } = useAxiosRequest('/repositories', { 17 | fetcher: RepositoriesAPI.getRepositories, 18 | refreshInterval: 6000, 19 | }) 20 | 21 | if (!repositories) { 22 | return 23 | } 24 | 25 | repositories = repositories.sort((repo1, repo2) => { 26 | if (!repo1.isConfigured) { 27 | return 1 28 | } else { 29 | if (repo1.isConfigured && repo2.isConfigured) { 30 | return repo1.score - repo2.score 31 | } 32 | return -1 33 | } 34 | }) 35 | 36 | return ( 37 | <> 38 | 39 | 40 | 41 | My Reactivated apps{' '} 42 | 43 | {repositories.length}/{getMaxRepositories()} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {repositories.length === 0 ? ( 56 | 57 | 58 | 59 | ) : ( 60 | 61 | gym 62 | 69 | You have {repositories.length} Reactivated app 70 | {repositories.length > 1 ? 's' : ''} 71 | 72 | 73 | Dependencies of your Reactivated apps are automatically 74 | refreshed every 4 hours by our system. 75 | 76 | 77 | )} 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | export default Home 85 | -------------------------------------------------------------------------------- /front/src/containers/DashboardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Badge, 3 | Box, 4 | Button, 5 | Flex, 6 | IconButton, 7 | Image, 8 | Skeleton, 9 | Stack, 10 | Text, 11 | } from '@chakra-ui/core' 12 | import Container from '@components/Container' 13 | import React from 'react' 14 | import { Link } from 'react-router-dom' 15 | import { Row } from '../components/Flex' 16 | 17 | const DashboardSkeleton = () => { 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | My Reactivated apps{' '} 25 | 26 | {3} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {Array.from(Array(5).keys()).map((_, i) => ( 48 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | reactivated-app 72 | 73 | 74 | 75 | 76 | react 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 88 % 85 | 86 | 87 | 88 | 94 | 95 | 96 | 97 | 98 | ))} 99 | 100 | 101 | ) 102 | } 103 | 104 | export default DashboardSkeleton 105 | -------------------------------------------------------------------------------- /front/src/containers/GithubRedirect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useLocation, Redirect } from 'react-router' 3 | import { useAuth } from '@contexts/AuthContext' 4 | import AuthAPI from '@api/auth' 5 | import qs from 'query-string' 6 | import Home from '@containers/Home' 7 | 8 | const GithubRedirect = () => { 9 | const { token, setToken } = useAuth() 10 | 11 | const { search } = useLocation() 12 | 13 | const authUser = async (code: string) => { 14 | const { data } = await AuthAPI.getGithubCallback(code) 15 | setToken(data.token) 16 | } 17 | 18 | useEffect(() => { 19 | if (!token) { 20 | const parsed = qs.parse(search) 21 | authUser(parsed.code as string) 22 | } else { 23 | if (window.opener) { 24 | window.opener.postMessage('installation-made') 25 | window.close() 26 | } 27 | } 28 | // eslint-disable-next-line react-hooks/exhaustive-deps 29 | }, []) 30 | 31 | if (!token) { 32 | return 33 | } 34 | 35 | if (window.opener) { 36 | return null 37 | } 38 | 39 | return 40 | } 41 | 42 | export default GithubRedirect 43 | -------------------------------------------------------------------------------- /front/src/containers/Layout/AuthApp.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Box, 4 | Flex, 5 | Icon, 6 | Menu, 7 | MenuButton, 8 | MenuItem, 9 | MenuList, 10 | Button, 11 | Skeleton, 12 | MenuDivider, 13 | } from '@chakra-ui/core' 14 | import Header from '@components/Header' 15 | import Router from '@containers/Router' 16 | import { useAuth } from '@contexts/AuthContext' 17 | import { Global } from '@emotion/core' 18 | import { deleteFromStorage } from '@rehooks/local-storage' 19 | import React from 'react' 20 | import WaitingBeta from '../WaitingBeta' 21 | import { Link } from 'react-router-dom' 22 | import { FiLogOut } from 'react-icons/fi' 23 | 24 | const AuthApp = () => { 25 | const logOut = () => { 26 | deleteFromStorage('token') 27 | } 28 | 29 | const { jwTokenData } = useAuth() 30 | 31 | return ( 32 | <> 33 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | {jwTokenData?.avatarUrl ? ( 48 | 54 | ) : ( 55 | 56 | 57 | 58 | )} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 |
79 |
80 |
81 | 82 | 89 | 96 | {process.env.REACT_APP_IS_BETA === 'true' && 97 | jwTokenData?.validated === false ? ( 98 | 99 | ) : ( 100 | 101 | )} 102 | 103 | 104 | ) 105 | } 106 | 107 | export default AuthApp 108 | -------------------------------------------------------------------------------- /front/src/containers/Repository/ViewPullRequest.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from '@chakra-ui/core' 2 | import React from 'react' 3 | import { useRouteMatch } from 'react-router-dom' 4 | import PullRequestItem from '../../components/PullRequest/PullRequestItem' 5 | import { useAxiosRequest } from '../../hooks/useRequest' 6 | import { getPullRequests } from '../../api/repositories' 7 | import { PullRequest } from '@typings/entities' 8 | import { useRepository } from '../../contexts/RepositoryContext' 9 | 10 | const ViewPullRequest = () => { 11 | const { 12 | params: { id }, 13 | } = useRouteMatch<{ id: string }>() 14 | const { data } = useAxiosRequest( 15 | `repositories/${id}/pull-requests`, 16 | { 17 | fetcher: () => getPullRequests(id), 18 | refreshInterval: 3000, 19 | }, 20 | ) 21 | 22 | const { repository, updateScore } = useRepository() 23 | 24 | React.useEffect(() => { 25 | updateScore(repository?.score || 0) 26 | }, [repository, updateScore]) 27 | 28 | return ( 29 | <> 30 | 37 | 38 | 🚧 39 | {' '} 40 | PR creation is experimental. It only supports Yarn packager (yarn.lock 41 | file will be created even if there is a package-lock.json). 42 | 43 | {data && data.length > 0 ? ( 44 | <> 45 | 46 | {data.map((pullRequest) => ( 47 | 48 | ))} 49 | 50 | 51 | ) : ( 52 | 58 | 59 | No pull requests 60 | 61 | 62 | )} 63 | 64 | ) 65 | } 66 | 67 | export default ViewPullRequest 68 | -------------------------------------------------------------------------------- /front/src/containers/Repository/ViewRepoSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Skeleton, Stack, Box, Heading, Text, Code } from '@chakra-ui/core' 3 | import Container from '@components/Container' 4 | import FrameworkTag from '@components/FrameworkTag/FrameworkTag' 5 | 6 | const ViewRepoSkeleton = () => { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | 15 | repo name 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 19 days ago 26 | 27 | 28 | 29 | 30 | 38 | link repo 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Outdated Dependencies 50 | 51 | 52 | 53 | Refreshed 1day ago 54 | 55 | 56 | 57 | Tabs 58 | 59 | 60 | 61 | 62 | {[...new Array(5)].map((_, i) => ( 63 | 64 | Dep 65 | 66 | ))} 67 | 68 | 69 | 70 | ) 71 | } 72 | 73 | export default ViewRepoSkeleton 74 | -------------------------------------------------------------------------------- /front/src/containers/Router.tsx: -------------------------------------------------------------------------------- 1 | import PrivateRoute from '@components/PrivateRoute' 2 | import { RepositoryProvider } from '@contexts/RepositoryContext' 3 | import React from 'react' 4 | import { Route, Switch } from 'react-router-dom' 5 | import Dashboard from './Dashboard' 6 | import AddRepo from './Repository/AddRepo' 7 | import RepositoryLayout from './Repository/RepositoryLayout' 8 | import ViewPullRequest from './Repository/ViewPullRequest' 9 | import ViewRepo from './Repository/ViewRepo' 10 | import Settings from './Settings' 11 | 12 | function Router() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default Router 37 | -------------------------------------------------------------------------------- /front/src/containers/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Popover, 4 | PopoverArrow, 5 | PopoverBody, 6 | PopoverCloseButton, 7 | PopoverContent, 8 | PopoverHeader, 9 | PopoverTrigger, 10 | Text, 11 | useToast, 12 | Box, 13 | } from '@chakra-ui/core' 14 | import React from 'react' 15 | import { FaTrash } from 'react-icons/fa' 16 | import { useHistory } from 'react-router' 17 | import { deleteAccount } from '../api/user' 18 | import Container from '../components/Container' 19 | import { Row } from '../components/Flex' 20 | import { useAuth } from '../contexts/AuthContext' 21 | import { deleteFromStorage } from '@rehooks/local-storage' 22 | 23 | const Settings = () => { 24 | const { jwTokenData } = useAuth() 25 | const history = useHistory() 26 | const toast = useToast() 27 | 28 | const onDeleteAccount = async () => { 29 | try { 30 | await deleteAccount(jwTokenData?.userId) 31 | deleteFromStorage('token') 32 | history.push('/') 33 | 34 | return toast({ 35 | position: 'top-right', 36 | title: 'Account deleted.', 37 | description: 'Your account has been correctly deleted', 38 | status: 'success', 39 | duration: 4000, 40 | isClosable: true, 41 | }) 42 | } catch (error) { 43 | return toast({ 44 | position: 'top-right', 45 | title: 'An error occured.', 46 | description: 'Unable to delete account', 47 | status: 'error', 48 | duration: 4000, 49 | isClosable: true, 50 | }) 51 | } 52 | } 53 | 54 | return ( 55 | <> 56 | 57 | Settings 58 | 59 | 60 | 61 | 62 | 63 | There's not much here... but you can delete your account. 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | Confirm deletion 76 | 77 | 78 | Are you sure to delete your account? 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ) 91 | } 92 | 93 | export default Settings 94 | -------------------------------------------------------------------------------- /front/src/containers/WaitingBeta.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from '@chakra-ui/core' 2 | import React from 'react' 3 | import Container from '../components/Container' 4 | 5 | type Props = {} 6 | 7 | const WaitingBeta: React.FC = () => { 8 | return ( 9 | 10 | We will soon validate your account... 11 | 12 | ) 13 | } 14 | 15 | export default WaitingBeta 16 | -------------------------------------------------------------------------------- /front/src/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from 'react' 2 | import { useLocalStorage, writeStorage } from '@rehooks/local-storage' 3 | import jwt_decode from 'jwt-decode' 4 | 5 | interface AuthContextInterface { 6 | jwTokenData: JwTokenData | null 7 | setToken: (value: string | null) => void 8 | token: string | null 9 | } 10 | 11 | const AuthContext = React.createContext({ 12 | setToken: () => null, 13 | jwTokenData: null, 14 | token: null, 15 | }) 16 | 17 | interface Props { 18 | children: React.ReactNode 19 | } 20 | 21 | function AuthProvider(props: Props) { 22 | const [token] = useLocalStorage('token', null) 23 | const jwTokenData = useMemo( 24 | () => (token ? jwt_decode(token) : null), 25 | [token], 26 | ) 27 | const setToken = (value: string | null) => { 28 | writeStorage('token', value) 29 | } 30 | return ( 31 | 32 | ) 33 | } 34 | 35 | function useAuth() { 36 | const context = useContext(AuthContext) 37 | return context 38 | } 39 | 40 | export { AuthProvider, useAuth } 41 | -------------------------------------------------------------------------------- /front/src/contexts/DependenciesContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react' 2 | 3 | type TDep = { [key: string]: 'stable' | 'latest' } 4 | 5 | interface DependenciesContextInterface { 6 | setDependencies: (item: TDep) => void 7 | dependencies: TDep | null 8 | } 9 | 10 | const DependenciesContext = React.createContext({ 11 | setDependencies: () => null, 12 | dependencies: null, 13 | }) 14 | 15 | interface Props { 16 | children: React.ReactNode 17 | } 18 | 19 | function DependenciesProvider(props: Props) { 20 | const [dependencies, setDependencies] = useState({}) 21 | 22 | return ( 23 | 27 | ) 28 | } 29 | 30 | function useDependencies() { 31 | const context = useContext(DependenciesContext) 32 | return context 33 | } 34 | 35 | export { DependenciesProvider, useDependencies } 36 | -------------------------------------------------------------------------------- /front/src/contexts/RepositoryContext.tsx: -------------------------------------------------------------------------------- 1 | import { Repository } from '@typings/entities' 2 | import React, { useContext, useState } from 'react' 3 | interface RepositoryContextInterface { 4 | setRepository: (repository: Repository | undefined) => void 5 | repository: Repository | undefined 6 | increasePRCount: () => void 7 | prCount: number 8 | updateScore: (newScore: number) => void 9 | outdatedCount: number 10 | score: number 11 | } 12 | 13 | const RepositoryContext = React.createContext({ 14 | setRepository: () => null, 15 | repository: undefined, 16 | prCount: 0, 17 | increasePRCount: () => null, 18 | updateScore: () => null, 19 | outdatedCount: 0, 20 | score: 0, 21 | }) 22 | 23 | interface Props { 24 | children: React.ReactNode 25 | } 26 | 27 | export function RepositoryProvider(props: Props) { 28 | const [repository, setRepository] = useState() 29 | const [prCount, setCreatedCount] = useState(0) 30 | const [score, setScore] = React.useState(0) 31 | const [outdatedCount, setOutdatedCount] = React.useState(0) 32 | 33 | React.useEffect(() => { 34 | setCreatedCount(0) 35 | if (repository) { 36 | setOutdatedCount( 37 | repository.dependencies!.deps.reduce( 38 | (outdatedCount, dep: object | string[], i) => { 39 | if (Array.isArray(dep)) { 40 | return outdatedCount + 1 41 | } 42 | 43 | return outdatedCount + Object.values(dep)[0].length 44 | }, 45 | 0, 46 | ), 47 | ) 48 | } 49 | }, [repository]) 50 | 51 | const increasePRCount = () => { 52 | setCreatedCount((prevCount) => prevCount + 1) 53 | } 54 | const updateScore = (newScore: number) => { 55 | setScore(newScore) 56 | } 57 | 58 | return ( 59 | 71 | ) 72 | } 73 | 74 | export function useRepository() { 75 | const context = useContext(RepositoryContext) 76 | return context 77 | } 78 | -------------------------------------------------------------------------------- /front/src/hooks/useChakraToast.ts: -------------------------------------------------------------------------------- 1 | import { useToastOptions, useToast } from '@chakra-ui/core' 2 | import { useCallback } from 'react' 3 | 4 | const defaultConfig: useToastOptions = { 5 | duration: 5000, 6 | isClosable: true, 7 | position: 'top', 8 | } 9 | 10 | const useChakraToast = () => { 11 | const showToast = useToast() 12 | 13 | return useCallback( 14 | (options: useToastOptions) => { 15 | showToast({ 16 | ...defaultConfig, 17 | ...options, 18 | }) 19 | }, 20 | [showToast], 21 | ) 22 | } 23 | 24 | export default useChakraToast 25 | -------------------------------------------------------------------------------- /front/src/hooks/useMessageListener.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | const useMessageListener = (listener: (e: MessageEvent) => void) => { 4 | useEffect(() => { 5 | window.addEventListener('message', listener) 6 | 7 | return () => { 8 | window.removeEventListener('message', listener) 9 | } 10 | }, [listener]) 11 | } 12 | 13 | export default useMessageListener 14 | -------------------------------------------------------------------------------- /front/src/hooks/useRequest.ts: -------------------------------------------------------------------------------- 1 | import useSWR, { ConfigInterface, keyInterface } from 'swr' 2 | import { AxiosResponse } from 'axios' 3 | 4 | const useRequest = ( 5 | key: keyInterface, 6 | config: ConfigInterface, 7 | ) => { 8 | return useSWR(key, config) 9 | } 10 | 11 | const useAxiosRequest = ( 12 | key: keyInterface, 13 | config: ConfigInterface, 14 | ) => { 15 | const { data: response, ...rest } = useRequest>(key, config) 16 | const data = response?.data 17 | 18 | return { data, ...rest } 19 | } 20 | 21 | export { useRequest, useAxiosRequest } 22 | -------------------------------------------------------------------------------- /front/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | --main-color: #3c9cea; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /front/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { AuthProvider } from '@contexts/AuthContext' 4 | import { ThemeProvider, CSSReset } from '@chakra-ui/core' 5 | import App from './App' 6 | import theme from './theme' 7 | import './index.css' 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ) 18 | -------------------------------------------------------------------------------- /front/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'react-gauge-chart' 3 | -------------------------------------------------------------------------------- /front/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { theme } from '@chakra-ui/core' 2 | export default { 3 | ...theme, 4 | colors: { 5 | ...theme.colors, 6 | brand: { 7 | 50: 'rgba(71,253,167,0.1)', 8 | 100: '#affed8', 9 | 200: '#51fdaa', 10 | 300: '#43ee9c', 11 | 400: '#3edd90', 12 | 500: '#47fda7', 13 | 600: '#33b576', 14 | 700: '#2c9b65', 15 | 800: '#227b50', 16 | 900: '#14482f', 17 | }, 18 | secondary: { 19 | 50: '#8f92a7', 20 | 100: '#7a7d95', 21 | 200: '#646883', 22 | 300: '#4f5371', 23 | 400: '#393e60', 24 | 500: '#393e60', 25 | 600: '#24294e', 26 | 700: '#1d2146', 27 | 800: '#191d42', 28 | 900: '#16193e', 29 | }, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /front/src/typings/entities.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | 4 | /* 5 | * --------------------------------------------------------------- 6 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 7 | * ## ## 8 | * ## AUTHOR: acacode ## 9 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 10 | * --------------------------------------------------------------- 11 | */ 12 | 13 | export interface User { 14 | /** Id of the object */ 15 | id: number 16 | username: string 17 | githubId: string 18 | githubToken: string 19 | } 20 | 21 | export interface GeneratedUserBulkDto { 22 | bulk: User[] 23 | } 24 | 25 | export interface Date {} 26 | 27 | export interface Repository { 28 | /** Id of the object */ 29 | id: number 30 | 31 | /** Name of the repo */ 32 | name: string 33 | fullName: string 34 | githubId: string 35 | installationId: string 36 | 37 | /** Author of the repo */ 38 | author: string 39 | 40 | /** Image of the repo */ 41 | repoImg: string 42 | 43 | /** Create date of the repo */ 44 | createdAt: string 45 | dependenciesUpdatedAt: string 46 | 47 | /** URL of the repo */ 48 | repoUrl: string 49 | users: User[] 50 | 51 | /** Package.json file of the repo */ 52 | packageJson: PackageJson 53 | 54 | /** Outdated dependencies of the repo */ 55 | dependencies: { 56 | deps: (DependencyArray | PrefixedDependency)[] 57 | } | null 58 | branch: string 59 | path: string 60 | isConfigured: boolean 61 | score: number 62 | framework: FrameworkTag 63 | hasYarnLock: boolean 64 | pullRequests: PullRequest[] 65 | 66 | crawlError: string 67 | } 68 | 69 | export interface Log { 70 | /** Id of the object */ 71 | id: number 72 | stackTrace: string 73 | name: string 74 | failedReason: string 75 | data: object 76 | } 77 | 78 | export interface PullRequest { 79 | /** Id of the object */ 80 | id: number 81 | status: string 82 | repository: Repository 83 | branchName: string 84 | url: string 85 | log: Log 86 | } 87 | 88 | export interface GeneratedRepositoryBulkDto { 89 | bulk: Repository[] 90 | } 91 | -------------------------------------------------------------------------------- /front/src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | interface JwTokenData { 2 | githubToken: string 3 | githubId: string 4 | userName: string 5 | userId: User['id'] 6 | validated: boolean 7 | avatarUrl: string 8 | } 9 | 10 | type DependencyArray = [string, string, string, string, DependencyType, string] 11 | type Dependency = { 12 | name: string 13 | current: string 14 | wanted: string 15 | latest: string 16 | type: DependencyType 17 | url: string 18 | prefix?: string 19 | } 20 | 21 | type PrefixedDependency = { [prefix: string]: DependencyArray[] } 22 | 23 | type DependencyType = 'dependencies' | 'devDependencies' 24 | 25 | type PackageJson = { 26 | dependencies: object 27 | devDependencies: object 28 | } | null 29 | 30 | type FrameworkTag = 31 | | 'react' 32 | | 'react native' 33 | | 'vue' 34 | | 'angular' 35 | | 'next.js' 36 | | 'nest.js' 37 | | 'express' 38 | 39 | interface GithubAccount { 40 | login: string 41 | avatarUrl: string 42 | id: string 43 | } 44 | 45 | interface GithubInstallationRepository { 46 | id: number 47 | name: string 48 | fullName: string 49 | private: boolean 50 | } 51 | 52 | interface GithubInstallation { 53 | id: number 54 | owner: GithubAccount 55 | repositories: GithubInstallationRepository[] 56 | } 57 | 58 | interface GithubBranch { 59 | name: string 60 | } 61 | 62 | type Status = 'pending' | 'done' | 'merged' | 'closed' 63 | -------------------------------------------------------------------------------- /front/src/utils/dependencies.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from '@typings/entities' 2 | 3 | export const getDependenciesCount = (packageJson: PackageJson | undefined) => { 4 | let totalDependencies = 0 5 | if (packageJson?.dependencies) { 6 | totalDependencies += Object.keys(packageJson.dependencies).length 7 | } 8 | if (packageJson?.devDependencies) { 9 | totalDependencies += Object.keys(packageJson.devDependencies).length 10 | } 11 | return totalDependencies 12 | } 13 | 14 | export const refinedDependency = (dependency: DependencyArray): Dependency => ({ 15 | name: dependency[0], 16 | current: dependency[1], 17 | wanted: dependency[2], 18 | latest: dependency[3], 19 | type: dependency[4], 20 | url: dependency[5], 21 | }) 22 | 23 | export const getNewScore = ( 24 | nbSelectedDependencies: number, 25 | nbOutdatedDependencies: number, 26 | packageJson: PackageJson | undefined, 27 | ) => { 28 | let totalDependencies = 0 29 | if (packageJson) { 30 | if (packageJson.dependencies) { 31 | totalDependencies += Object.keys(packageJson.dependencies).length 32 | } 33 | if (packageJson.devDependencies) { 34 | totalDependencies += Object.keys(packageJson.devDependencies).length 35 | } 36 | } else { 37 | return 0 38 | } 39 | 40 | const newScore = Math.round( 41 | 100 - 42 | ((nbOutdatedDependencies - nbSelectedDependencies) / totalDependencies) * 43 | 100, 44 | ) 45 | return newScore 46 | } 47 | 48 | export const sortDependencies = (repository: Repository) => { 49 | let dependencies: Dependency[] = [] 50 | let devDependencies: Dependency[] = [] 51 | 52 | repository?.dependencies?.deps.forEach( 53 | (dep: DependencyArray | PrefixedDependency) => { 54 | if (Array.isArray(dep)) { 55 | const depObject = refinedDependency(dep) 56 | depObject.type === 'dependencies' 57 | ? dependencies.push(depObject) 58 | : devDependencies.push(depObject) 59 | } else { 60 | const prefix = Object.keys(dep)[0] 61 | const type = dep[prefix][0][4] 62 | dependencies = [ 63 | ...dependencies, 64 | ...dep[prefix] 65 | .filter((dep) => dep[4] === type) 66 | .map((dep) => ({ ...refinedDependency(dep), prefix })), 67 | ] 68 | } 69 | }, 70 | ) 71 | 72 | return { dependencies, devDependencies } 73 | } 74 | -------------------------------------------------------------------------------- /front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /front/tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /reactivated-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/premieroctet/reactivated-app/a338bc37c79f2b03afac97407b9785ce73b74c90/reactivated-app.gif --------------------------------------------------------------------------------