diff --git a/apps/model-ad/app/project.json b/apps/model-ad/app/project.json index e150b7461a..70b57bf375 100644 --- a/apps/model-ad/app/project.json +++ b/apps/model-ad/app/project.json @@ -34,6 +34,9 @@ "output": "assets" } ], + "stylePreprocessorOptions": { + "includePaths": ["libs"] + }, "styles": [ "apps/model-ad/app/src/styles.scss", "node_modules/primeicons/primeicons.css", @@ -116,7 +119,10 @@ "outputPath": "dist/apps/model-ad/app/browser/server", "main": "apps/model-ad/app/server.ts", "tsConfig": "apps/model-ad/app/tsconfig.server.json", - "inlineStyleLanguage": "scss" + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["libs"] + } }, "configurations": { "production": { diff --git a/apps/openchallenges/app/.eslintrc.json b/apps/openchallenges/app/.eslintrc.json index 9d63a89a78..984ee36364 100644 --- a/apps/openchallenges/app/.eslintrc.json +++ b/apps/openchallenges/app/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["plugin:playwright/recommended", "../../../.eslintrc.json"], + "extends": ["../../../.eslintrc.json", "plugin:playwright/recommended"], "ignorePatterns": ["!**/*"], "overrides": [ { @@ -10,7 +10,7 @@ "error", { "type": "attribute", - "prefix": "openchallenges", + "prefix": "app", "style": "camelCase" } ], @@ -18,7 +18,7 @@ "error", { "type": "element", - "prefix": "openchallenges", + "prefix": "app", "style": "kebab-case" } ] diff --git a/apps/openchallenges/app/Dockerfile b/apps/openchallenges/app/Dockerfile index c27f548043..54e5a62481 100644 --- a/apps/openchallenges/app/Dockerfile +++ b/apps/openchallenges/app/Dockerfile @@ -1,6 +1,6 @@ FROM node:20.7.0-alpine -ENV APP_DIR=/app +ENV APP_DIR=/opt/app RUN apk add --no-cache curl envsubst jq su-exec @@ -10,9 +10,8 @@ COPY apps/openchallenges/app/docker-entrypoint.d /docker-entrypoint.d RUN chmod +x docker-entrypoint.sh /docker-entrypoint.d/* WORKDIR ${APP_DIR} -COPY dist/apps/openchallenges/app/browser/server ${APP_DIR} -# The path of the destination folder must be the same as the path specified in server.ts. -COPY dist/apps/openchallenges/app/browser/browser ${APP_DIR}/dist/apps/openchallenges/app/browser/browser +COPY dist/apps/openchallenges/app/server ./server +COPY dist/apps/openchallenges/app/browser ./browser HEALTHCHECK --interval=2s --timeout=3s --retries=20 --start-period=5s \ CMD curl --fail --silent "localhost:4200/health" | jq '.status' | grep UP || exit 1 @@ -21,4 +20,4 @@ EXPOSE 4200 ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["node", "main.js"] +CMD ["node", "server/server.mjs"] diff --git a/apps/openchallenges/app/README.md b/apps/openchallenges/app/README.md deleted file mode 100644 index 9583c7bc02..0000000000 --- a/apps/openchallenges/app/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# OpenChallenges App - -## Configuration - -### Dev Server - -- Server host: The server port is defined by the property `options.host` in `project.json` for the - task `serve`. -- Server port: The server port is defined by the property `options.port` in `project.json` for the - task `serve`. -- Proxy config: The configuration used by the web app to proxy requests to API servers is defined in - `src/proxy.conf.json`. This file is referenced in `project.json`. CORS is enabled on the OC API - gateway so there is no need to proxy the requests sent to it. -- App config: The configuration used by the web app is defined in `src/config/config.json` - -### Containerized Server - -- Server host: TODO -- Server port: The server listen to the port defined in `docker/nginx/templates/http.conf.template`. - The port exposed by the container is defined in `Dockerfile` and - `{workspaceRoot}/docker/openchallenges/services/app.yml`. -- Proxy config: The configuration used by the Nginx to proxy requests to API servers can be - specified in `docker/nginx/templates/http.conf.template`. CORS is enabled on the OC API gateway so - there is no need to proxy the requests sent to it. -- App config: The configuration used by the web app is defined by the file - `src/config/config.json.template` and the environment variables specified to the container. diff --git a/apps/openchallenges/app/docker-entrypoint.d/10-envsubst-on-app-config-template.sh b/apps/openchallenges/app/docker-entrypoint.d/10-envsubst-on-app-config-template.sh index dfef93928a..eb9777341b 100644 --- a/apps/openchallenges/app/docker-entrypoint.d/10-envsubst-on-app-config-template.sh +++ b/apps/openchallenges/app/docker-entrypoint.d/10-envsubst-on-app-config-template.sh @@ -1,4 +1,5 @@ #!/usr/bin/env sh -cd "${APP_DIR}/dist/apps/openchallenges/app/browser/browser/config" +# Generate 'config.json' from 'config.json.template' using environment variables. +cd "${APP_DIR}/browser/config" envsubst < config.json.template > config.json diff --git a/apps/openchallenges/app/jest.config.ts b/apps/openchallenges/app/jest.config.ts index 34108a0a47..cf0c7d8678 100644 --- a/apps/openchallenges/app/jest.config.ts +++ b/apps/openchallenges/app/jest.config.ts @@ -1,9 +1,7 @@ -/* eslint-disable */ export default { - displayName: 'openchallenges', + displayName: 'openchallenges-app', preset: '../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], - globals: {}, coverageDirectory: '../../../coverage/apps/openchallenges/app', transform: { '^.+\\.(ts|mjs|js|html)$': [ diff --git a/apps/openchallenges/app/project.json b/apps/openchallenges/app/project.json index defd557ed2..3640fc8aba 100644 --- a/apps/openchallenges/app/project.json +++ b/apps/openchallenges/app/project.json @@ -2,92 +2,81 @@ "name": "openchallenges-app", "$schema": "../../../node_modules/nx/schemas/project-schema.json", "projectType": "application", + "prefix": "app", "sourceRoot": "apps/openchallenges/app/src", - "prefix": "openchallenges", + "tags": ["type:app", "scope:client", "language:typescript"], "targets": { - "create-config": { - "executor": "nx:run-commands", - "options": { - "command": "cp -n .env.example .env", - "cwd": "{projectRoot}" - } - }, - "sonar": { - "executor": "nx:run-commands", - "options": { - "command": "bash $WORKSPACE_DIR/tools/sonar-scanner.sh --project-key {projectName} --project-dir .", - "cwd": "{projectRoot}" - } - }, "build": { - "executor": "@angular-devkit/build-angular:browser", + "executor": "@nx/angular:application", "outputs": ["{options.outputPath}"], "options": { - "outputPath": "dist/apps/openchallenges/app/browser/browser", + "outputPath": "dist/apps/openchallenges/app", "index": "apps/openchallenges/app/src/index.html", - "main": "apps/openchallenges/app/src/main.ts", + "browser": "apps/openchallenges/app/src/main.ts", "polyfills": ["zone.js"], "tsConfig": "apps/openchallenges/app/tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ - "apps/openchallenges/app/src/assets", "apps/openchallenges/app/src/config", "apps/openchallenges/app/src/humans.txt", "apps/openchallenges/app/src/robots.txt", - "apps/openchallenges/app/src/sitemap.xml", { - "input": "libs/shared/typescript/assets/src/assets", + "input": "libs/shared/typescript/assets/src", "glob": "**/*", - "output": "assets" - }, - { - "input": "libs/openchallenges/assets/src/assets", - "glob": "**/*", - "output": "openchallenges-assets" + "output": "assets/shared" }, { "input": "libs/openchallenges/assets/src", - "glob": "favicon.ico", - "output": "" + "glob": "**/*", + "output": "assets/openchallenges" } ], + "stylePreprocessorOptions": { + "includePaths": ["libs"] + }, "styles": [ "apps/openchallenges/app/src/styles.scss", "node_modules/primeicons/primeicons.css", "node_modules/primeng/resources/themes/lara-light-blue/theme.css", "node_modules/primeng/resources/primeng.min.css" ], - "scripts": [] + "scripts": [], + "server": "apps/openchallenges/app/src/main.server.ts", + "prerender": false, + "ssr": { + "entry": "apps/openchallenges/app/server.ts" + } }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "1mb", - "maximumError": "2mb" + "maximumWarning": "500kb", + "maximumError": "1.5mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "10kb" + "maximumError": "8kb" } ], "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { - "executor": "@angular-devkit/build-angular:dev-server", + "executor": "@nx/angular:dev-server", + "options": { + "host": "127.0.0.1", + "port": 4200 + }, "configurations": { "production": { "buildTarget": "openchallenges-app:build:production" @@ -96,22 +85,10 @@ "buildTarget": "openchallenges-app:build:development" } }, - "defaultConfiguration": "development", - "options": { - "host": "127.0.0.1", - "port": 4200, - "proxyConfig": "apps/openchallenges/app/src/proxy.conf.json" - } - }, - "serve-detach": { - "executor": "nx:run-commands", - "options": { - "command": "docker/openchallenges/serve-detach.sh openchallenges-app" - }, - "dependsOn": [] + "defaultConfiguration": "development" }, "extract-i18n": { - "executor": "@angular-devkit/build-angular:extract-i18n", + "executor": "@nx/angular:extract-i18n", "options": { "buildTarget": "openchallenges-app:build" } @@ -119,85 +96,27 @@ "lint": { "executor": "@nx/eslint:lint" }, - "lint-fix": { - "executor": "@nx/eslint:lint", - "options": { - "fix": true - } - }, "test": { "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/apps/openchallenges/app"], + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { "jestConfig": "apps/openchallenges/app/jest.config.ts" } }, - "scan-image": { - "executor": "nx:run-commands", + "serve-static": { + "executor": "@nx/web:file-server", "options": { - "command": "trivy image ghcr.io/sage-bionetworks/openchallenges-app:local --quiet", - "color": true + "buildTarget": "openchallenges-app:build", + "staticFilePath": "dist/apps/openchallenges/app/browser", + "spa": true } }, - "server": { - "dependsOn": ["build"], - "executor": "@angular-devkit/build-angular:server", - "options": { - "outputPath": "dist/apps/openchallenges/app/browser/server", - "main": "apps/openchallenges/app/server.ts", - "tsConfig": "apps/openchallenges/app/tsconfig.server.json", - "inlineStyleLanguage": "scss" - }, - "configurations": { - "production": { - "outputHashing": "media" - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "sourceMap": true, - "extractLicenses": false, - "vendorChunk": true - } - }, - "defaultConfiguration": "production" - }, - "serve-ssr": { - "executor": "@angular-devkit/build-angular:ssr-dev-server", - "configurations": { - "development": { - "browserTarget": "openchallenges-app:build:development", - "serverTarget": "openchallenges-app:server:development" - }, - "production": { - "browserTarget": "openchallenges-app:build:production", - "serverTarget": "openchallenges-app:server:production" - } - }, - "defaultConfiguration": "development" - }, - "prerender": { - "executor": "@angular-devkit/build-angular:prerender", - "options": { - "routes": ["/"] - }, - "configurations": { - "development": { - "browserTarget": "openchallenges-app:build:development", - "serverTarget": "openchallenges-app:server:development" - }, - "production": { - "browserTarget": "openchallenges-app:build:production", - "serverTarget": "openchallenges-app:server:production" - } - }, - "defaultConfiguration": "production" - }, - "build-sitemap": { + "serve-detach": { "executor": "nx:run-commands", "options": { - "command": "node tools/generate-sitemap.js http://localhost:4200 apps/openchallenges/app/src/sitemap.xml" - } + "command": "docker/openchallenges/serve-detach.sh {projectName}" + }, + "dependsOn": [] }, "e2e": { "executor": "@nx/playwright:playwright", @@ -207,7 +126,6 @@ } } }, - "tags": ["type:app", "scope:client", "language:typescript"], "implicitDependencies": [ "openchallenges-styles", "openchallenges-themes", diff --git a/apps/openchallenges/app/server.ts b/apps/openchallenges/app/server.ts index 998a3bc842..a7a96c6764 100644 --- a/apps/openchallenges/app/server.ts +++ b/apps/openchallenges/app/server.ts @@ -1,43 +1,38 @@ -import 'zone.js/node'; - import { APP_BASE_HREF } from '@angular/common'; import { CommonEngine } from '@angular/ssr'; -import * as express from 'express'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; +import express from 'express'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; import bootstrap from './src/main.server'; -const PORT = process.env['PORT'] || '4200'; -console.log(`server.ts: ${PORT}`); - // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { const server = express(); - const distFolder = join(process.cwd(), 'dist/apps/openchallenges/app/browser/browser'); - const indexHtml = existsSync(join(distFolder, 'index.original.html')) - ? join(distFolder, 'index.original.html') - : join(distFolder, 'index.html'); + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); const commonEngine = new CommonEngine(); server.set('view engine', 'html'); - server.set('views', distFolder); + server.set('views', browserDistFolder); // Example Express Rest API endpoints // server.get('/api/**', (req, res) => { }); // Serve static files from /browser server.get( - '*.*', - express.static(distFolder, { + '**', + express.static(browserDistFolder, { maxAge: '1y', + index: 'index.html', }), ); - // Health endpoint used by the container + // Health endpoint used by the Docker container server.get('/health', (_req, res) => res.status(200).json({ status: 'UP' })); // All regular routes use the Angular engine - server.get('*', (req, res, next) => { + server.get('**', (req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req; commonEngine @@ -45,22 +40,8 @@ export function app(): express.Express { bootstrap, documentFilePath: indexHtml, url: `${protocol}://${headers.host}${originalUrl}`, - publicPath: distFolder, - providers: [ - { provide: APP_BASE_HREF, useValue: baseUrl }, - // The base URL enables the app to load the app config file during server-side rendering. - { - provide: 'APP_BASE_URL', - // the format of ${host} is `host:port` - useFactory: () => `${protocol}://${headers.host}`, - deps: [], - }, - { - provide: 'APP_PORT', - useValue: PORT, - deps: [], - }, - ], + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], }) .then((html) => res.send(html)) .catch((err) => next(err)); @@ -70,22 +51,13 @@ export function app(): express.Express { } function run(): void { + const port = process.env['PORT'] || '4200'; + // Start up the Node server const server = app(); - server.listen(PORT, () => { - console.log(`Node Express server listening on http://localhost:${PORT}`); + server.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); }); } -// Webpack will replace 'require' with '__webpack_require__' -// '__non_webpack_require__' is a proxy to Node 'require' -// The below code is to ensure that the server is run only when not requiring the bundle. -/* eslint-disable camelcase,no-undef */ -declare const __non_webpack_require__: NodeRequire; -const mainModule = __non_webpack_require__.main; -const moduleFilename = (mainModule && mainModule.filename) || ''; -if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { - run(); -} - -export default bootstrap; +run(); diff --git a/apps/openchallenges/app/src/_app-theme.scss b/apps/openchallenges/app/src/_app-theme.scss index ea984bacc1..b15b9205e5 100644 --- a/apps/openchallenges/app/src/_app-theme.scss +++ b/apps/openchallenges/app/src/_app-theme.scss @@ -1,15 +1,13 @@ @use 'sass:map'; @use '@angular/material' as mat; -@use 'libs/openchallenges/themes/src/fonts' as fonts; -@use 'libs/openchallenges/themes/src/palettes' as palettes; -@use 'libs/openchallenges/themes/src/index' as openchallenges; - +@use 'openchallenges/themes/src/fonts' as fonts; +@use 'openchallenges/themes/src/palettes' as palettes; +@use 'openchallenges/themes/src/index' as openchallenges; @include mat.typography-hierarchy(fonts.$lato); -@include mat.core(); +@include mat.core; $primary: mat.m2-define-palette(palettes.$dark-blue-palette, 600); $accent: mat.m2-define-palette(palettes.$accent-purple-palette, 400); - $theme: mat.m2-define-light-theme( ( color: ( @@ -44,5 +42,5 @@ $theme: map.deep-merge( :root { --color-btn-primary: #39bde7; --color-btn-disabled: #ebebe4; - --color-btn-shadow: rgba(196, 196, 196, 1); + --color-btn-shadow: rgb(196 196 196 / 100%); } diff --git a/apps/openchallenges/app/src/app/app.component.html b/apps/openchallenges/app/src/app/app.component.html index 968b56dcd4..a69881c488 100644 --- a/apps/openchallenges/app/src/app/app.component.html +++ b/apps/openchallenges/app/src/app/app.component.html @@ -1,4 +1,4 @@ - + { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RouterTestingModule], - declarations: [AppComponent, NxWelcomeComponent], + imports: [AppComponent, NxWelcomeComponent, RouterModule.forRoot([])], }).compileComponents(); }); - it('should create the app', () => { + it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Welcome openchallenges-app'); }); - it(`should have as title 'openchallenges'`, () => { + it(`should have as title 'openchallenges-app'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app.title).toEqual('openchallenges'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain( - 'Welcome openchallenges' - ); + expect(app.title).toEqual('openchallenges-app'); }); }); diff --git a/apps/openchallenges/app/src/app/app.component.ts b/apps/openchallenges/app/src/app/app.component.ts index 15f50a8b5b..28f74b9a5d 100644 --- a/apps/openchallenges/app/src/app/app.component.ts +++ b/apps/openchallenges/app/src/app/app.component.ts @@ -17,7 +17,7 @@ import { ConfigService } from '@sagebionetworks/openchallenges/config'; import { NgIf } from '@angular/common'; @Component({ - selector: 'openchallenges-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, diff --git a/apps/openchallenges/app/src/app/app.config.server.ts b/apps/openchallenges/app/src/app/app.config.server.ts index 33d83d276a..9e697f6f1e 100644 --- a/apps/openchallenges/app/src/app/app.config.server.ts +++ b/apps/openchallenges/app/src/app/app.config.server.ts @@ -1,17 +1,18 @@ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; -import { appConfig, APP_BASE_URL_PROVIDER_INDEX } from './app.config'; -import { provideClientHydration } from '@angular/platform-browser'; +import { appConfig } from './app.config'; const serverConfig: ApplicationConfig = { - providers: [provideServerRendering(), provideClientHydration()], + providers: [ + provideServerRendering(), + // This provider enables the config service to locate the config file during SSR. + // Originally added to server.ts (used for production with the Express server), + // it was moved here to ensure availability in both production and development environments. + { + provide: 'APP_PORT', + useValue: process.env['PORT'] || '4200', + }, + ], }; -// The file server.ts defines a provider that specifies 'APP_BASE_URL' based on the request protocol -// and host. If this provider could be defined in serverConfig above, there would be no need to -// manually remove the provider that specifies 'APP_BASE_URL' from appConfig used for client-side -// rendering. Also removing based on an index should be avoided: I would have preferred to remove it -// based on a property value but couldn't. -appConfig.providers.splice(APP_BASE_URL_PROVIDER_INDEX, 1); - export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/apps/openchallenges/app/src/app/app.config.ts b/apps/openchallenges/app/src/app/app.config.ts index e5bcc558e7..43b862581d 100644 --- a/apps/openchallenges/app/src/app/app.config.ts +++ b/apps/openchallenges/app/src/app/app.config.ts @@ -1,28 +1,24 @@ -import { ApplicationConfig, APP_INITIALIZER, APP_ID } from '@angular/core'; +import { + ApplicationConfig, + provideZoneChangeDetection, + APP_INITIALIZER, + APP_ID, +} from '@angular/core'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling, } from '@angular/router'; -import { withInterceptorsFromDi, provideHttpClient } from '@angular/common/http'; -import { provideAnimations } from '@angular/platform-browser/animations'; -import { BASE_PATH as API_CLIENT_BASE_PATH } from '@sagebionetworks/openchallenges/api-client-angular'; +import { appRoutes } from './app.routes'; +import { provideClientHydration } from '@angular/platform-browser'; import { configFactory, ConfigService } from '@sagebionetworks/openchallenges/config'; - -import { routes } from './app.routes'; - -// This index is used to remove the corresponding provider in app.config.server.ts. -export const APP_BASE_URL_PROVIDER_INDEX = 1; +import { BASE_PATH as API_CLIENT_BASE_PATH } from '@sagebionetworks/openchallenges/api-client-angular'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { provideHttpClient, withFetch } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ { provide: APP_ID, useValue: 'openchallenges-app' }, - { - // This provider must be specified at the index defined by APP_BASE_URL_PROVIDER_INDEX. - provide: 'APP_BASE_URL', - useFactory: () => '.', - deps: [], - }, { provide: APP_INITIALIZER, useFactory: configFactory, @@ -38,9 +34,13 @@ export const appConfig: ApplicationConfig = { deps: [ConfigService], }, provideAnimations(), - provideHttpClient(withInterceptorsFromDi()), + // The HTTP client is injected to enable the ConfigService to retrieve the configuration file + // via HTTP when server-side rendering (SSR) is used. + provideHttpClient(withFetch()), + provideClientHydration(), + provideZoneChangeDetection({ eventCoalescing: true }), provideRouter( - routes, + appRoutes, withEnabledBlockingInitialNavigation(), withInMemoryScrolling({ scrollPositionRestoration: 'enabled', diff --git a/apps/openchallenges/app/src/app/app.routes.ts b/apps/openchallenges/app/src/app/app.routes.ts index 973436a0a5..9b8570b1bc 100644 --- a/apps/openchallenges/app/src/app/app.routes.ts +++ b/apps/openchallenges/app/src/app/app.routes.ts @@ -1,6 +1,6 @@ import { Routes } from '@angular/router'; -export const routes: Routes = [ +export const appRoutes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', diff --git a/apps/openchallenges/app/src/app/google-tag-manager/google-tag-manager.component.ts b/apps/openchallenges/app/src/app/google-tag-manager/google-tag-manager.component.ts index 5dedf67c5e..5a9a594e17 100644 --- a/apps/openchallenges/app/src/app/google-tag-manager/google-tag-manager.component.ts +++ b/apps/openchallenges/app/src/app/google-tag-manager/google-tag-manager.component.ts @@ -5,7 +5,7 @@ import { ConfigService } from '@sagebionetworks/openchallenges/config'; import { googleTagManagerIdProvider } from './google-tag-manager-id.provider'; @Component({ - selector: 'openchallenges-google-tag-manager', + selector: 'app-google-tag-manager', template: '', standalone: true, providers: [ diff --git a/apps/openchallenges/app/src/assets/silent-check-sso.html b/apps/openchallenges/app/src/assets/silent-check-sso.html deleted file mode 100644 index b3bd540ded..0000000000 --- a/apps/openchallenges/app/src/assets/silent-check-sso.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/apps/openchallenges/app/src/environments/environment.prod.ts b/apps/openchallenges/app/src/environments/environment.prod.ts deleted file mode 100644 index 81afb5af8b..0000000000 --- a/apps/openchallenges/app/src/environments/environment.prod.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const environment = { - production: true, - apiUrl: 'http://localhost:4200/api', - appVersion: '0.0.1', -}; diff --git a/apps/openchallenges/app/src/environments/environment.ts b/apps/openchallenges/app/src/environments/environment.ts deleted file mode 100644 index 1bfbbf2d22..0000000000 --- a/apps/openchallenges/app/src/environments/environment.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false, - apiUrl: 'http://localhost:4200/api', - appVersion: '0.0.1', -}; - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/apps/openchallenges/app/src/index.html b/apps/openchallenges/app/src/index.html index e132b0928b..1cc385fc94 100644 --- a/apps/openchallenges/app/src/index.html +++ b/apps/openchallenges/app/src/index.html @@ -5,10 +5,9 @@ OpenChallenges - - + - + diff --git a/apps/openchallenges/app/src/proxy.conf.json b/apps/openchallenges/app/src/proxy.conf.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/apps/openchallenges/app/src/proxy.conf.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/openchallenges/app/src/robots.txt b/apps/openchallenges/app/src/robots.txt index 4c73a82512..26fa50257a 100644 --- a/apps/openchallenges/app/src/robots.txt +++ b/apps/openchallenges/app/src/robots.txt @@ -1,5 +1,3 @@ User-agent: * Disallow: /api/ -Disallow: /login/ - -Sitemap: http://www.openchallenges.org/sitemap.xml \ No newline at end of file +Disallow: /login/ \ No newline at end of file diff --git a/apps/openchallenges/app/src/styles.scss b/apps/openchallenges/app/src/styles.scss index 18f9a8bfc4..693114ffcf 100644 --- a/apps/openchallenges/app/src/styles.scss +++ b/apps/openchallenges/app/src/styles.scss @@ -1,3 +1,3 @@ -@use 'libs/openchallenges/styles/src/index'; - -@use 'app-theme'; +/* You can add global styles to this file, and also import other style files */ +@use 'openchallenges/styles/src/index'; +@use './app-theme'; diff --git a/apps/openchallenges/app/src/test-setup.ts b/apps/openchallenges/app/src/test-setup.ts index 1100b3e8a6..ab1eeeb335 100644 --- a/apps/openchallenges/app/src/test-setup.ts +++ b/apps/openchallenges/app/src/test-setup.ts @@ -1 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; import 'jest-preset-angular/setup-jest'; diff --git a/apps/openchallenges/app/tsconfig.app.json b/apps/openchallenges/app/tsconfig.app.json index 58220429a4..2afe01ea4a 100644 --- a/apps/openchallenges/app/tsconfig.app.json +++ b/apps/openchallenges/app/tsconfig.app.json @@ -2,9 +2,9 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../dist/out-tsc", - "types": [] + "types": ["node"] }, - "files": ["src/main.ts"], + "files": ["src/main.ts", "src/main.server.ts", "server.ts"], "include": ["src/**/*.d.ts"], "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/apps/openchallenges/app/tsconfig.editor.json b/apps/openchallenges/app/tsconfig.editor.json index 8ae117d962..a8ac182c08 100644 --- a/apps/openchallenges/app/tsconfig.editor.json +++ b/apps/openchallenges/app/tsconfig.editor.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.json", "include": ["src/**/*.ts"], - "compilerOptions": { - "types": ["jest", "node"] - } + "compilerOptions": {}, + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/apps/openchallenges/app/tsconfig.json b/apps/openchallenges/app/tsconfig.json index e85865cf5b..ddb3050218 100644 --- a/apps/openchallenges/app/tsconfig.json +++ b/apps/openchallenges/app/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2022", - "useDefineForClassFields": false, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, @@ -13,13 +13,13 @@ "include": [], "references": [ { - "path": "./tsconfig.app.json" + "path": "./tsconfig.editor.json" }, { - "path": "./tsconfig.spec.json" + "path": "./tsconfig.app.json" }, { - "path": "./tsconfig.editor.json" + "path": "./tsconfig.spec.json" } ], "extends": "../../../tsconfig.base.json", diff --git a/apps/openchallenges/app/tsconfig.server.json b/apps/openchallenges/app/tsconfig.server.json deleted file mode 100644 index a1f97f4a54..0000000000 --- a/apps/openchallenges/app/tsconfig.server.json +++ /dev/null @@ -1,10 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.app.json", - "compilerOptions": { - "outDir": "../../out-tsc/server", - "target": "ES2022", - "types": ["node"] - }, - "files": ["src/main.server.ts", "server.ts"] -} diff --git a/apps/sandbox/angular-app/.eslintrc.json b/apps/sandbox/angular-app/.eslintrc.json index 7d1084846e..36040daaee 100644 --- a/apps/sandbox/angular-app/.eslintrc.json +++ b/apps/sandbox/angular-app/.eslintrc.json @@ -26,7 +26,7 @@ }, { "files": ["*.html"], - "extends": ["plugin:@nx/angular-template", "plugin:tailwindcss/recommended"], + "extends": ["plugin:@nx/angular-template"], "rules": {} } ] diff --git a/apps/sandbox/angular-app/project.json b/apps/sandbox/angular-app/project.json index c8abb526fa..bd366b65bc 100644 --- a/apps/sandbox/angular-app/project.json +++ b/apps/sandbox/angular-app/project.json @@ -7,7 +7,7 @@ "tags": [], "targets": { "build": { - "executor": "@angular-devkit/build-angular:application", + "executor": "@nx/angular:application", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/apps/sandbox/angular-app", @@ -22,6 +22,9 @@ "input": "apps/sandbox/angular-app/public" } ], + "stylePreprocessorOptions": { + "includePaths": ["libs/openchallenges/styles/src"] + }, "styles": ["apps/sandbox/angular-app/src/styles.scss"], "scripts": [], "server": "apps/sandbox/angular-app/src/main.server.ts", @@ -55,7 +58,7 @@ "defaultConfiguration": "production" }, "serve": { - "executor": "@angular-devkit/build-angular:dev-server", + "executor": "@nx/angular:dev-server", "configurations": { "production": { "buildTarget": "sandbox-angular-app:build:production" @@ -67,7 +70,7 @@ "defaultConfiguration": "development" }, "extract-i18n": { - "executor": "@angular-devkit/build-angular:extract-i18n", + "executor": "@nx/angular:extract-i18n", "options": { "buildTarget": "sandbox-angular-app:build" } diff --git a/apps/sandbox/angular-app/src/styles.scss b/apps/sandbox/angular-app/src/styles.scss index 77e408aa8b..90d4ee0072 100644 --- a/apps/sandbox/angular-app/src/styles.scss +++ b/apps/sandbox/angular-app/src/styles.scss @@ -1,5 +1 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - /* You can add global styles to this file, and also import other style files */ diff --git a/apps/sandbox/angular-app/tailwind.config.js b/apps/sandbox/angular-app/tailwind.config.js deleted file mode 100644 index 38183db2c8..0000000000 --- a/apps/sandbox/angular-app/tailwind.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); -const { join } = require('path'); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), - ...createGlobPatternsForDependencies(__dirname), - ], - theme: { - extend: {}, - }, - plugins: [], -}; diff --git a/docker/openchallenges/services/apex.yml b/docker/openchallenges/services/apex.yml index a2c35429d9..5d60861fcd 100644 --- a/docker/openchallenges/services/apex.yml +++ b/docker/openchallenges/services/apex.yml @@ -21,6 +21,7 @@ services: condition: service_started openchallenges-zipkin: condition: service_healthy + deploy: resources: limits: diff --git a/libs/model-ad/not-found/src/lib/not-found.component.scss b/libs/model-ad/not-found/src/lib/not-found.component.scss index aeeb8de789..85c6fb2191 100644 --- a/libs/model-ad/not-found/src/lib/not-found.component.scss +++ b/libs/model-ad/not-found/src/lib/not-found.component.scss @@ -1,4 +1,4 @@ -@use 'libs/model-ad/styles/src/lib/constants'; +@use 'model-ad/styles/src/variables'; #error { background: url('/model-ad-assets/images/banner-sm.svg'); @@ -8,11 +8,13 @@ flex-direction: column; justify-content: center; align-items: center; - min-height: calc(100vh - constants.$navbar-height); + min-height: calc(100vh - variables.$navbar-height); } + .err-message { padding-top: 100px; } + .btn-group { margin-top: 48px; diff --git a/libs/model-ad/styles/src/lib/_general.scss b/libs/model-ad/styles/src/_general.scss similarity index 88% rename from libs/model-ad/styles/src/lib/_general.scss rename to libs/model-ad/styles/src/_general.scss index e774e40dbb..b492b4ba61 100644 --- a/libs/model-ad/styles/src/lib/_general.scss +++ b/libs/model-ad/styles/src/_general.scss @@ -1,12 +1,17 @@ -@use 'libs/model-ad/styles/src/lib/constants'; +/* stylelint-disable no-descending-specificity */ +/* stylelint-disable no-duplicate-selectors */ +/* stylelint-disable scss/at-extend-no-missing-placeholder */ +@use 'model-ad/styles/src/variables'; html { - margin-top: constants.$navbar-height; + margin-top: variables.$navbar-height; } + body { max-height: 100vh; margin: 0; } + em { color: #00b1e5; font-style: normal; @@ -16,24 +21,28 @@ em { // GRID .base { width: 100%; - min-height: calc(100vh - constants.$navbar-height - constants.$footer-height); + min-height: calc(100vh - variables.$navbar-height - variables.$footer-height); } + .row { display: flex; - flex-direction: row; - flex-wrap: wrap; + flex-flow: row wrap; width: 100%; } + .col { display: flex; flex-direction: column; flex-basis: 100%; } + .fill-empty { max-width: 920px; } + .content { @extend .row; + max-width: 2000px; margin: auto; padding: 48px 32px; @@ -44,16 +53,20 @@ em { .text-left { text-align: left; } + .text-center { text-align: center; } + .text-right { text-align: right; } + .text-grey, span.text-grey a { color: rgba(black, 0.38); } + .text-right { font-weight: 600; } @@ -63,6 +76,7 @@ span.text-grey a { width: 140px; height: 140px; } + .top-design { background: url('/model-ad-assets/images/banner-sm.svg'); background-size: cover; @@ -86,12 +100,14 @@ span.text-grey a { text-decoration: none; transition: background 0.15s ease !important; } + .btn-block { width: 100%; padding: 16px 32px !important; text-transform: uppercase; text-decoration: none; } + .btn-group { display: flex; flex-flow: column wrap; @@ -101,9 +117,11 @@ span.text-grey a { text-decoration: none; } } + .btn-group > *:first-child { align-self: stretch; } + .btn-group button, .btn-group a { margin: 5px; @@ -114,23 +132,28 @@ span.text-grey a { #search-top { padding: 0 32px 80px; } + .search-sort-container { gap: 24px; justify-content: space-between; } + .search-field { position: relative; flex-grow: 1; margin: auto; } + .sort-field { display: flex; gap: 16px; align-items: center; } + .sort-title { white-space: nowrap; } + .facets { padding: 24px 16px; height: 100%; @@ -153,17 +176,21 @@ span.text-grey a { min-height: 320px; justify-content: center; } + #profile-top { padding-top: 60px; } + #profile-bottom { padding-top: 32px; } + #profile-top > div { align-self: center; flex: 1 0 auto; padding: 22px; } + .profile-pic, .organization-card-banner { // width: 40px; @@ -172,8 +199,8 @@ span.text-grey a { align-self: center; flex: 0 0 auto; box-sizing: border-box; - border-radius: constants.$dl-radius-radius-round; - box-shadow: 1px 5px 18px 0px #d4d4d4; + border-radius: variables.$dl-radius-radius-round; + box-shadow: 1px 5px 18px 0 #d4d4d4; div.avatar-content { height: 100%; @@ -182,13 +209,16 @@ span.text-grey a { justify-content: center; } } + #profile-details { max-width: 100%; text-align: center; } + .profile-activity-card { text-align: right; } + .profile-nav-group { margin: 18px; padding: 48px 18px; @@ -199,10 +229,11 @@ span.text-grey a { border-width: 2px; border-radius: 8px; } + .profile-nav-item { width: 100%; height: 54px; - margin: 0 0 18px 0; + margin: 0 0 18px; padding: 13px 18px; box-sizing: border-box; transition: 0.3s; @@ -211,6 +242,7 @@ span.text-grey a { text-align: left; text-decoration: none; } + .profile-type { width: 100%; max-width: 148px; @@ -220,13 +252,14 @@ span.text-grey a { text-align: center; align-self: center; } + .stats-group { display: flex; - flex-direction: row; - flex-wrap: wrap; + flex-flow: row wrap; width: 100%; justify-content: center; } + .stat-item { flex-direction: column; flex: 1; @@ -243,6 +276,7 @@ span.text-grey a { margin-bottom: 2px !important; } } + .action-btn, .disabled-btn { height: 100%; @@ -254,34 +288,42 @@ span.text-grey a { justify-content: center; align-items: center; } + .action-btn { background-color: var(--color-btn-primary); color: white; border-color: transparent; cursor: pointer; } + .disabled-btn { background-color: var(--color-btn-disabled); border-right-width: 1px; } + .stats-card-icon { margin-right: 4px; } + table { border-spacing: 11px; } + #bio div a mat-icon { font-size: 15px; vertical-align: middle; } + .created-updated-dates, .created-updated-dates a, .read-more { color: #1f3b8f; } + .created-updated-dates { font-style: italic !important; } + .read-more { text-underline-offset: 3px; } @@ -294,32 +336,39 @@ table { -webkit-box-orient: vertical; -webkit-line-clamp: $max-lines; } + .card-group { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24px; padding: 16px 0; } + .card-banner { width: 100%; height: 130px; display: flex; align-items: flex-start; } + .card-icon { margin: 0 3px; padding-right: 24px; } + .card-title { margin: 15px 0 !important; font-weight: 700 !important; line-height: 24px !important; min-height: 48px; + @include line-clamp(2); } + .mat-caption { @include line-clamp(2); } + .card-body { width: 100%; margin: 8px; @@ -333,6 +382,7 @@ table { text-align: center; } } + .card-footer { display: flex; flex-direction: row; @@ -343,86 +393,106 @@ table { align-items: center; gap: 6px; } + .star-btn { position: absolute; top: 18px; right: 18px; + // height: 32px; padding: 0 8px; display: flex; justify-content: center; align-items: center; - border-radius: constants.$dl-radius-radius-radius16; + border-radius: variables.$dl-radius-radius-radius16; cursor: pointer; } // MEDIA QUERIES -@media screen and (max-width: constants.$sm-breakpoint) { +@media screen and (max-width: variables.$sm-breakpoint) { .profile-activity-card { text-align: center; } } -@media screen and (max-width: constants.$md-breakpoint) { + +@media screen and (max-width: variables.$md-breakpoint) { .content { flex-direction: column; } } -@media screen and (min-width: constants.$md-breakpoint) { + +@media screen and (min-width: variables.$md-breakpoint) { .col, .col-1 { flex: 1; } + .col-2 { flex: 2; } + .col-3 { flex: 3; } + .col-4 { flex: 4; } + .col-5 { flex: 5; } + .col-6 { flex: 6; } + .col-7 { flex: 7; } + .col-8 { flex: 8; } + .col-9 { flex: 9; } + .col-10 { flex: 10; } + .col-11 { flex: 11; } + .col-12 { flex: 12; } + #search-top { padding: 0 132px 80px; } + #profile-details { // max-width: 760px; max-width: 1060px; text-align: left; } + .profile-type { align-self: auto; } + .profile-sidenav { max-width: 320px; } } -@media only screen and (max-width: constants.$lg-breakpoint) { + +@media only screen and (max-width: variables.$lg-breakpoint) { html { - margin-top: constants.$navbar-height-tall; + margin-top: variables.$navbar-height-tall; } .card-group { diff --git a/libs/model-ad/styles/src/_index.scss b/libs/model-ad/styles/src/_index.scss index 144ee1f14c..e752f5e4bc 100644 --- a/libs/model-ad/styles/src/_index.scss +++ b/libs/model-ad/styles/src/_index.scss @@ -1,4 +1,3 @@ -@use 'libs/shared/typescript/styles/src/index'; - -@use './lib/constants'; -@use './lib/general'; +// @forward 'shared/typescript/styles/src/index'; +@forward './variables'; +@forward './general'; diff --git a/libs/model-ad/styles/src/lib/_constants.scss b/libs/model-ad/styles/src/_variables.scss similarity index 98% rename from libs/model-ad/styles/src/lib/_constants.scss rename to libs/model-ad/styles/src/_variables.scss index ceb3628f29..c974bf978c 100644 --- a/libs/model-ad/styles/src/lib/_constants.scss +++ b/libs/model-ad/styles/src/_variables.scss @@ -1,7 +1,7 @@ // See https://github.com/angular/components/blob/master/src/material/core/style/_variables.scss // Global constants -$pi: 3.14159264; +$pi: 3.1416; $padding-test: 20px; // Figma variables diff --git a/libs/model-ad/ui/src/lib/footer/_footer-theme.scss b/libs/model-ad/ui/src/lib/footer/_footer-theme.scss index cffd8d5eaf..d4cd34a6e6 100644 --- a/libs/model-ad/ui/src/lib/footer/_footer-theme.scss +++ b/libs/model-ad/ui/src/lib/footer/_footer-theme.scss @@ -1,6 +1,7 @@ +/* stylelint-disable scss/at-if-no-null */ @use 'sass:map'; @use '@angular/material' as mat; -@use 'libs/model-ad/styles/src/lib/constants'; +@use 'model-ad/styles/src/variables'; @mixin color($theme) { $config: mat.m2-get-color-config($theme); @@ -12,10 +13,12 @@ background-color: mat.m2-get-color-from-palette($primary, 600); color: #fff; } + .footer-link-group a, .footer-links a { color: #fff; } + .footer-bottom { background-color: mat.m2-get-color-from-palette($primary, 800); } @@ -28,19 +31,23 @@ font-weight: 500; line-height: normal; } + .app-info { font-size: 14px; line-height: 21px; } + .footer-link-group { font-size: 16px; } + .footer-subtext, .footer-links, .footer-links a { font-size: 16px; } - @media only screen and (max-width: constants.$md-breakpoint) { + + @media only screen and (max-width: variables.$md-breakpoint) { .footer-link-group { font-size: 14px !important; } @@ -49,11 +56,13 @@ @mixin theme($theme) { $color-config: mat.m2-get-color-config($theme); + @if $color-config != null { @include color($theme); } $typography-config: mat.m2-get-typography-config($theme); + @if $typography-config != null { @include typography($theme); } diff --git a/libs/model-ad/ui/src/lib/footer/footer.component.scss b/libs/model-ad/ui/src/lib/footer/footer.component.scss index f9fb0b6e21..767fadd7b8 100644 --- a/libs/model-ad/ui/src/lib/footer/footer.component.scss +++ b/libs/model-ad/ui/src/lib/footer/footer.component.scss @@ -1,8 +1,8 @@ -@use 'libs/model-ad/styles/src/lib/constants'; +@use 'model-ad/styles/src/variables'; footer { width: 100%; - height: constants.$footer-height; + height: variables.$footer-height; position: relative; padding: 42px 52px; display: flex; @@ -10,6 +10,7 @@ footer { align-items: flex-start; box-sizing: border-box; } + .footer-link-group { width: 420px; height: 30px; @@ -18,21 +19,25 @@ footer { justify-content: space-between; list-style: none; } + .footer-link-group li { flex: 0 0 auto; } + .app-info { text-align: right; align-self: center; } + .app-info ul { list-style-type: none; padding: 0; margin: 0; } + .footer-bottom { bottom: 0; - left: 0px; + left: 0; width: 100%; height: 70px; display: flex; @@ -40,21 +45,25 @@ footer { align-items: center; justify-content: center; } + .footer-links, .footer-links a { text-decoration: none; } + .footer-links a:hover { text-decoration: underline; } + .logo { height: 56px; } -@media only screen and (max-width: constants.$md-breakpoint) { +@media only screen and (max-width: variables.$md-breakpoint) { footer { height: 410px; } + .about-oc, .app-info, .footer-link-group { @@ -62,9 +71,11 @@ footer { text-align: center; justify-content: space-around; } + .footer-link-group li { flex: 1; } + .app-info { margin-top: 45px; } diff --git a/libs/openchallenges/about/src/lib/about.component.html b/libs/openchallenges/about/src/lib/about.component.html index 2808807cf1..fcd11092e6 100644 --- a/libs/openchallenges/about/src/lib/about.component.html +++ b/libs/openchallenges/about/src/lib/about.component.html @@ -2,7 +2,7 @@