diff --git a/.gitignore b/.gitignore index 00c405127..3ec0bea37 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ node_modules !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +!.vscode/tailwind.json # misc /.sass-cache @@ -58,6 +59,7 @@ libs/**/*.ngsummary.json libs/environments/environment.prod.ts libs/environments/firebase/config.prod.ts +!libs/theme/src/tailwind.config.js !libs/webcomponents/** # map tiles diff --git a/.vscode/settings.json b/.vscode/settings.json index 19aa47027..e6c1e9928 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,6 @@ ], "deno.unstable": true, "deno.config": "apps/picsa-server/supabase/functions/deno.jsonc", - "angular.enable-strict-mode-prompt": false + "angular.enable-strict-mode-prompt": false, + "css.customData": [".vscode/tailwind.json"] } diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json new file mode 100644 index 000000000..96a1f5797 --- /dev/null +++ b/.vscode/tailwind.json @@ -0,0 +1,55 @@ +{ + "version": 1.1, + "atDirectives": [ + { + "name": "@tailwind", + "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" + } + ] + }, + { + "name": "@apply", + "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#apply" + } + ] + }, + { + "name": "@responsive", + "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" + } + ] + }, + { + "name": "@screen", + "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#screen" + } + ] + }, + { + "name": "@variants", + "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#variants" + } + ] + } + ] +} diff --git a/apps/picsa-apps/extension-app-native/android/.idea/misc.xml b/apps/picsa-apps/extension-app-native/android/.idea/misc.xml index fdc12d097..c5ccf361a 100644 --- a/apps/picsa-apps/extension-app-native/android/.idea/misc.xml +++ b/apps/picsa-apps/extension-app-native/android/.idea/misc.xml @@ -1,3 +1,4 @@ + - + diff --git a/apps/picsa-apps/extension-app-native/android/app/build.gradle b/apps/picsa-apps/extension-app-native/android/app/build.gradle index f5e075fb6..b530fd419 100644 --- a/apps/picsa-apps/extension-app-native/android/app/build.gradle +++ b/apps/picsa-apps/extension-app-native/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "io.picsa.extension" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 3045000 - versionName "3.45.0" + versionCode 3046000 + versionName "3.46.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/apps/picsa-apps/extension-app-native/capacitor.config.ts b/apps/picsa-apps/extension-app-native/capacitor.config.ts index 09aaf0638..e96b90306 100644 --- a/apps/picsa-apps/extension-app-native/capacitor.config.ts +++ b/apps/picsa-apps/extension-app-native/capacitor.config.ts @@ -5,6 +5,7 @@ const config: CapacitorConfig = { appName: 'PICSA Extension', webDir: '../../../dist/apps/picsa-apps/extension-app', bundledWebRuntime: false, + zoomEnabled:true, // manually include plugins here as top-level package.json not checked correctly // note - see which plugins are detected via `npx cap ls` includePlugins: [ diff --git a/apps/picsa-apps/extension-app-native/project.json b/apps/picsa-apps/extension-app-native/project.json index ede31216f..35f787f9f 100644 --- a/apps/picsa-apps/extension-app-native/project.json +++ b/apps/picsa-apps/extension-app-native/project.json @@ -21,6 +21,7 @@ "executor": "nx:run-commands", "description": "Serve extension app in external mode to support live-reload", "dependsOn": ["^build"], + "inputs": ["{projectRoot}/capacitor.config.ts","{projectRoot}/.env.local"], "options": { "commands": ["npx cap sync && nx run picsa-apps-extension-app:serve --configuration=external"], "cwd": "apps/picsa-apps/extension-app-native", diff --git a/apps/picsa-apps/extension-app/src/app/components/layout.html b/apps/picsa-apps/extension-app/src/app/components/layout.html index 52e26bf48..d8ba1bf99 100644 --- a/apps/picsa-apps/extension-app/src/app/components/layout.html +++ b/apps/picsa-apps/extension-app/src/app/components/layout.html @@ -30,15 +30,7 @@
- - @if(showMenuButton()){ - - - - } - + @if(ready){ } @@ -51,3 +43,12 @@ } + + + + @if(showMenuButton()){ + + } + diff --git a/apps/picsa-apps/extension-app/src/app/components/layout.ts b/apps/picsa-apps/extension-app/src/app/components/layout.ts index a634973f9..392b27970 100644 --- a/apps/picsa-apps/extension-app/src/app/components/layout.ts +++ b/apps/picsa-apps/extension-app/src/app/components/layout.ts @@ -1,12 +1,13 @@ +import { TemplatePortal } from '@angular/cdk/portal'; import { CommonModule } from '@angular/common'; -import { Component, computed, Input, viewChild } from '@angular/core'; +import { Component, computed, effect, Input, TemplateRef, viewChild, ViewContainerRef } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListItem, MatNavList } from '@angular/material/list'; import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav'; import { NavigationEnd, Router, RouterModule, RouterOutlet } from '@angular/router'; -import { PicsaCommonComponentsModule } from '@picsa/components'; +import { PicsaCommonComponentsModule, PicsaCommonComponentsService } from '@picsa/components'; import { ConfigurationService } from '@picsa/configuration/src'; import { APP_VERSION } from '@picsa/environments'; import { PicsaLoadingComponent } from '@picsa/shared/features/loading/loading'; @@ -35,6 +36,7 @@ import { filter, map } from 'rxjs'; export class AppLayoutComponent { @Input() showLoader: boolean; @Input() ready: boolean; + menuButtonTemplate = viewChild.required>('menuButtonTemplate'); public drawer = viewChild.required(MatDrawer); public showMenuButton = toSignal( this.router.events.pipe( @@ -46,7 +48,31 @@ export class AppLayoutComponent { public userType = computed(() => this.configurationService.userSettings().user_type); public version = APP_VERSION; - constructor(private router: Router, private configurationService: ConfigurationService) {} + constructor( + private router: Router, + private configurationService: ConfigurationService, + componentService: PicsaCommonComponentsService, + viewContainer: ViewContainerRef + ) { + effect( + () => { + // Inject menu button into global header when on farmer or extension home + const { cdkPortalStart } = componentService.headerOptions(); + if (this.showMenuButton()) { + if (!cdkPortalStart) { + componentService.patchHeader({ + cdkPortalStart: new TemplatePortal(this.menuButtonTemplate(), viewContainer), + }); + } + } else { + if (cdkPortalStart) { + componentService.patchHeader({ cdkPortalStart: undefined }); + } + } + }, + { allowSignalWrites: true } + ); + } public toggleUserType() { const targetType = this.userType() === 'extension' ? 'farmer' : 'extension'; diff --git a/apps/picsa-apps/extension-app/tailwind.config.js b/apps/picsa-apps/extension-app/tailwind.config.js new file mode 100644 index 000000000..d51db2b67 --- /dev/null +++ b/apps/picsa-apps/extension-app/tailwind.config.js @@ -0,0 +1,3 @@ +const generateSharedTailwindConfig = require('../../../libs/theme/src/tailwind.config'); + +module.exports = generateSharedTailwindConfig(__dirname); diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/budget-card.scss b/apps/picsa-tools/budget-tool/src/app/components/card/budget-card.scss index 47a73d759..6a1318320 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/budget-card.scss +++ b/apps/picsa-tools/budget-tool/src/app/components/card/budget-card.scss @@ -1,5 +1,4 @@ :host { - margin: 10px; display: inline-block; cursor: pointer; text-align: center; @@ -9,7 +8,7 @@ } mat-card.budget-card { - width: 110px; + width: 100px; padding: 0; } diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss index 40522116a..d07fc482a 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss @@ -2,7 +2,7 @@ display: block; } .budget-card { - width: 110px; + width: 100px; margin-bottom: 1rem; } .card-title { diff --git a/apps/picsa-tools/budget-tool/src/app/components/editor/card-select/card-select.scss b/apps/picsa-tools/budget-tool/src/app/components/editor/card-select/card-select.scss index e69de29bb..5f73601f2 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/editor/card-select/card-select.scss +++ b/apps/picsa-tools/budget-tool/src/app/components/editor/card-select/card-select.scss @@ -0,0 +1,7 @@ +:host { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 6px; + justify-content: center; +} diff --git a/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.scss b/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.scss index aadc5c457..10b2d8c8a 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.scss +++ b/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.scss @@ -22,29 +22,29 @@ border-bottom: 1px solid var(--color-black); } .type-label { - width: 100px; + @apply w-20 p-1 sm:w-28 sm:p-2; border-right: 1px solid var(--color-black); - padding: 8px; } #cardsList { - height: calc(100% - 16px); - width: calc(100% - 16px); + height: 100%; background: white; display: flex; flex-direction: column; - padding: 8px; + @apply sm:p-4; } .selected-summary { flex: 1; min-height: 140px; + // HACK - assign width to prevent expanding over title column + min-width: 0px; } .selected-cards-container { display: grid; - padding: 10px; + padding: 8px; gap: 8px; - grid-template-columns: repeat(auto-fit, 128px); + grid-template-columns: repeat(auto-fit, 120px); // arrange input and output into larger grid for values editor &[data-type='inputs'], &[data-type='outputs'] { diff --git a/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.ts b/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.ts index c7d229b1c..3023a2e8e 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/editor/editor.component.ts @@ -67,11 +67,7 @@ export class BudgetEditorComponent implements OnDestroy { this.periodType$.next(type); this.dialog.open(this.cardsListDialog, { - width: '90vw', - height: '90vh', - maxWidth: '90vw', - maxHeight: '90vh', - panelClass: 'no-padding', + panelClass: 'budget-dialog', }); // scroll existing dialog to top if exists as dialog opens setTimeout(() => { diff --git a/apps/picsa-tools/climate-tool/src/app/components/climate-chart-options/climate-chart-options.component.html b/apps/picsa-tools/climate-tool/src/app/components/climate-chart-options/climate-chart-options.component.html index b667f95a0..095b65c89 100644 --- a/apps/picsa-tools/climate-tool/src/app/components/climate-chart-options/climate-chart-options.component.html +++ b/apps/picsa-tools/climate-tool/src/app/components/climate-chart-options/climate-chart-options.component.html @@ -5,8 +5,3 @@

{{ 'Chart' | translate }}

{{ 'Tools' | translate }}

- - - diff --git a/apps/picsa-tools/climate-tool/src/app/pages/site-select/site-select.page.ts b/apps/picsa-tools/climate-tool/src/app/pages/site-select/site-select.page.ts index f3cbae854..482cc3d0d 100644 --- a/apps/picsa-tools/climate-tool/src/app/pages/site-select/site-select.page.ts +++ b/apps/picsa-tools/climate-tool/src/app/pages/site-select/site-select.page.ts @@ -52,7 +52,7 @@ export class SiteSelectPage implements OnInit { goToSite(site: IStationMeta) { // record current map bound positions for returning back - const mapBounds = this.picsaMap.map.getBounds(); + const mapBounds = this.picsaMap.map().getBounds(); localStorage.setItem('picsaSiteSelectBounds', JSON.stringify([mapBounds.getSouthWest(), mapBounds.getNorthEast()])); // navigate this.router.navigate(['./', 'site', site.id], { diff --git a/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.html b/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.html index 3d8dcb1e2..d278bd2f4 100644 --- a/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.html +++ b/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.html @@ -9,7 +9,7 @@ - - - - - -
- + diff --git a/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.ts b/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.ts index 989374c92..c998e05f8 100644 --- a/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.ts +++ b/apps/picsa-tools/climate-tool/src/app/pages/site-view/site-view.page.ts @@ -7,6 +7,7 @@ import { computed, effect, OnDestroy, + signal, TemplateRef, ViewChild, ViewContainerRef, @@ -38,7 +39,7 @@ interface ISiteViewParams { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ClimateSiteViewComponent implements OnDestroy, AfterViewInit { - public showRotateAnimation = false; + public showRotateAnimation = signal(false); public stationSelectOptions = computed(() => { const stations = this.dataService.stations(); @@ -63,32 +64,35 @@ export class ClimateSiteViewComponent implements OnDestroy, AfterViewInit { private viewContainer: ViewContainerRef, private cdr: ChangeDetectorRef ) { - effect(async () => { - const viewId = this.viewId(); - const siteId = this.siteId(); - if (siteId && viewId) { - // same site, just view changed - if (siteId === this._siteId) { - await this.loadView(viewId); + effect( + async () => { + const viewId = this.viewId(); + const siteId = this.siteId(); + if (siteId && viewId) { + // same site, just view changed + if (siteId === this._siteId) { + await this.loadView(viewId); + } + // site changed + else { + this._siteId = siteId; + await this.chartService.setStation(siteId); + await this.loadView(viewId); + await _wait(50); + this.checkOrientation(); + } } - // site changed - else { - this._siteId = siteId; - await this.chartService.setStation(siteId); - await this.loadView(viewId); - } - } - this.cdr.markForCheck(); - }); + this.cdr.markForCheck(); + }, + { allowSignalWrites: true } + ); } - async ngAfterViewInit() { + ngAfterViewInit() { this.componentsService.patchHeader({ cdkPortalCenter: new TemplatePortal(this.headerPortal, this.viewContainer), }); - this.promptScreenRotate(); - this.cdr.markForCheck(); } ngOnDestroy() { @@ -116,9 +120,8 @@ export class ClimateSiteViewComponent implements OnDestroy, AfterViewInit { await this.chartService.setChart(viewId); } - private promptScreenRotate() { - if (window.innerHeight > window.innerWidth) { - this.showRotateAnimation = true; - } + private checkOrientation() { + const shouldRotate = window.innerHeight > window.innerWidth; + this.showRotateAnimation.set(shouldRotate); } } diff --git a/apps/picsa-tools/climate-tool/src/app/services/climate-chart.service.ts b/apps/picsa-tools/climate-tool/src/app/services/climate-chart.service.ts index ecebffc07..d96711afa 100644 --- a/apps/picsa-tools/climate-tool/src/app/services/climate-chart.service.ts +++ b/apps/picsa-tools/climate-tool/src/app/services/climate-chart.service.ts @@ -85,9 +85,6 @@ export class ClimateChartService { if (definition) { // apply translations definition.name = await this.translateService.translateText(definition.name); - if (this.station) { - definition.name = this.station.name; - } definition.yLabel = await this.translateService.translateText(definition.yLabel); definition.xLabel = await this.translateService.translateText(definition.xLabel); // generate config and apply custom onrendered callback diff --git a/apps/picsa-tools/farmer-content/src/app/app.routes.ts b/apps/picsa-tools/farmer-content/src/app/app.routes.ts index 4f1a771a7..e30301fa8 100644 --- a/apps/picsa-tools/farmer-content/src/app/app.routes.ts +++ b/apps/picsa-tools/farmer-content/src/app/app.routes.ts @@ -2,11 +2,6 @@ import { Route } from '@angular/router'; import { FarmerToolPlaceholderComponent } from './pages/tool/farmer-tool.component'; -// eslint-disable-next-line @nx/enforce-module-boundaries -// import { APP_TOOL_ROUTES } from '../../../../picsa-apps/extension-app/src/app/app-routing.module'; - -// console.log({ APP_TOOL_ROUTES }); - export const appRoutes: Route[] = [ { path: '', diff --git a/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.html b/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.html index 69d59c352..35a3333df 100644 --- a/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.html +++ b/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.html @@ -15,8 +15,6 @@

{{ step.title | translate }}

@for(tag of step.tags; track tag){ {{ tag.label | translate }} - } @for(tool of step.tools; track tool){ - {{ tool.label | translate }} }
diff --git a/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.scss b/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.scss index 03a913d19..4e47e9af6 100644 --- a/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.scss +++ b/apps/picsa-tools/farmer-content/src/app/pages/home/farmer-home.component.scss @@ -52,13 +52,15 @@ img.step-image { border-radius: 50%; } .step-content { - margin: 0 24px; + margin: 0; + @apply ml-6 sm:ml-12 sm:mr-12; flex: 1; } .step-title { text-transform: capitalize; font-weight: bold; color: var(--color-secondary); + text-align: left; } button.mat-mdc-icon-button.nav-button { height: 60px; diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.html b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.html new file mode 100644 index 000000000..8d6283bdb --- /dev/null +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.html @@ -0,0 +1,18 @@ + + + + + diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.scss b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.scss new file mode 100644 index 000000000..95a727d6b --- /dev/null +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.scss @@ -0,0 +1,28 @@ +:host { + position: sticky; + bottom: 0; + left: 0; + width: 100%; + height: var(--footer-height); // inherited from parent + overflow: hidden; + display: flex; + align-items: center; + --mdc-text-button-label-text-color: white; + color: white; + background: var(--color-secondary); + z-index: 10; +} + +.footer-nav-buttons { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + & > button { + flex: 1; + } +} +.footer-tab-counter { + flex: 1; + text-align: center; +} diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.spec.ts b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.spec.ts new file mode 100644 index 000000000..b550eced4 --- /dev/null +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModuleFooterComponent } from './module-footer.component'; + +describe('ModuleFooterComponent', () => { + let component: ModuleFooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ModuleFooterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ModuleFooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.ts b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.ts new file mode 100644 index 000000000..8c2a03312 --- /dev/null +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/footer/module-footer.component.ts @@ -0,0 +1,33 @@ +import { CommonModule } from '@angular/common'; +import { Component, input, model } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { Router } from '@angular/router'; +import { PicsaTranslateModule } from '@picsa/shared/modules'; + +@Component({ + selector: 'farmer-module-footer', + standalone: true, + imports: [CommonModule, MatButtonModule, MatIconModule, PicsaTranslateModule], + templateUrl: './module-footer.component.html', + styleUrl: './module-footer.component.scss', +}) +export class FarmerModuleFooterComponent { + public totalSections = input.required(); + + public selectedIndex = model.required(); + + public goHome() { + this.router.navigate(['/', 'farmer'], { replaceUrl: true }); + } + + constructor(private router: Router) {} + + public next() { + this.selectedIndex.update((v) => v + 1); + } + + public previous() { + this.selectedIndex.update((v) => v - 1); + } +} diff --git a/apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.html b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.html similarity index 100% rename from apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.html rename to apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.html diff --git a/apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.scss b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.scss similarity index 100% rename from apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.scss rename to apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.scss diff --git a/apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.spec.ts b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.spec.ts similarity index 100% rename from apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.spec.ts rename to apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.spec.ts diff --git a/apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.ts b/apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.ts similarity index 100% rename from apps/picsa-tools/farmer-content/src/app/components/step-video/step-video.component.ts rename to apps/picsa-tools/farmer-content/src/app/pages/module-home/components/step-video/step-video.component.ts diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html index 507b5ced5..e9b9e2b64 100644 --- a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.html @@ -2,97 +2,48 @@ @if(content(); as content){
- - - @for(step of steps(); track $index){ + @for(step of content.steps; track $index){ - @switch (step.type){ - - @case ("video") { - - {{ step.tabMatIcon || 'slideshow' }} - {{ step.tabLabel || 'video' | translate }} - -
- @if(step.video){ - - } -
- } - - @case ("video_playlist") { - - {{ step.tabMatIcon || 'slideshow' }} - {{ step.tabLabel || 'video' | translate }} - -
- @for(video of step.videos; track video.id){ - +
+ @for(block of step; track $index){ @switch (block.type) { + + + @case ('text') { @if(block.title){ +

{{ block.title | translate }}

+ } @if(block.text) { +

{{ block.text | translate }}

+ } } + + @case('video'){ + } -
- } } - - } - - @if(tools()[0]; as tool_0){ - - - smartphone - {{ tool_0.tabLabel || 'Tool' | translate }} - -
-
- -
-
-
- } - - @if(content.showReviewSection){ @if(photoAlbum(); as album){ - - - perm_media - {{ 'Review' | translate }} - -
+ + + @case ('review') { @if(photoAlbum(); as album){ +

{{ 'Review' | translate }}

-
-
+ } } - } } - + @case ('tool') { +
+
+ } } }
- } --> + }
+ + } diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.scss b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.scss index d0b2b2c5f..4e75d36ca 100644 --- a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.scss +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.scss @@ -1,7 +1,14 @@ +:host { + --footer-height: 36px; + // Adjust farmer tabs to compensate for header and increased footer size + @screen sm { + --footer-height: 48px; + } + @screen md { + --footer-height: 60px; + } +} .content-container { - width: 100%; - max-width: 800px; - margin: auto; flex: 1; } .top-banner { @@ -30,30 +37,22 @@ } .tab-content { - padding: 16px; -} -.tab-content.router-tab { + --tab-height: calc(100vh - var(--footer-height) - 2 * var(--page-padding)); // page-padding inherited + padding: 0; height: 100%; // background: yellow; display: flex; flex-direction: column; - // Some styles also inherited from main .page - .page { - overflow: auto; - max-height: calc(100vh - 136px); - } + @apply md:max-w-screen-md m-auto; + padding: var(--page-padding); + max-height: var(--tab-height); + overflow: auto; +} +.tab-content[data-tool-content='true'] { + --page-padding: 0px; +} +// HACK - reduce page size when using header +.page[data-has-header='true'] { + max-height: calc(var(--tab-height) - 3.5rem); } -// Force child-router tab to fill screen and provide page region -// .tab-content.router-tab { -// padding: 0; -// height: calc(100% - 32px); -// // background: yellow; -// display: flex; -// flex-direction: column; -// // Some styles also inherited from main .page -// .page { -// overflow: auto; -// max-height: calc(100vh - 104px); -// } -// } diff --git a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.ts b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.ts index 414059e73..9e9e088f0 100644 --- a/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.ts +++ b/apps/picsa-tools/farmer-content/src/app/pages/module-home/module-home.component.ts @@ -1,26 +1,26 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, effect, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, model, OnDestroy, OnInit, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; -import { MatIconModule } from '@angular/material/icon'; -import { MatTabChangeEvent, MatTabsModule } from '@angular/material/tabs'; +import { MatTabsModule } from '@angular/material/tabs'; import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; import { PicsaCommonComponentsService } from '@picsa/components/src'; -import { FARMER_CONTENT_DATA_BY_SLUG, IFarmerContent, IFarmerContentStep, IToolData } from '@picsa/data'; +import { FARMER_CONTENT_DATA_BY_SLUG, IFarmerContent, IFarmerContentStep, IToolData, StepTool } from '@picsa/data'; import { FadeInOut } from '@picsa/shared/animations'; import { PhotoInputComponent, PhotoListComponent, PhotoViewComponent } from '@picsa/shared/features'; import { PicsaTranslateModule } from '@picsa/shared/modules'; import { TourService } from '@picsa/shared/services/core/tour'; -import { FarmerStepVideoComponent } from '../../components/step-video/step-video.component'; +import { FarmerModuleFooterComponent } from './components/footer/module-footer.component'; +import { FarmerStepVideoComponent } from './components/step-video/step-video.component'; @Component({ selector: 'farmer-content-module-home', standalone: true, imports: [ CommonModule, + FarmerModuleFooterComponent, FarmerStepVideoComponent, PicsaTranslateModule, - MatIconModule, MatTabsModule, PhotoInputComponent, PhotoViewComponent, @@ -33,11 +33,25 @@ import { FarmerStepVideoComponent } from '../../components/step-video/step-video // Ensure url changes update in nested tools by using default change detection changeDetection: ChangeDetectionStrategy.Default, }) -export class FarmerContentModuleHomeComponent { +export class FarmerContentModuleHomeComponent implements OnInit, OnDestroy { private params = toSignal(this.route.params); - public content = signal(null); - public steps = signal([]); - public tools = signal([]); + + public content = computed(() => { + const { slug } = this.params() || {}; + return this.loadContentBySlug(slug); + }); + + /** Content to display within mat-tabs */ + public tabs = computed(() => { + const content = this.content(); + return content?.steps || []; + }); + + /** Selected tab index. Used to programatically change tabs from custom footer */ + public selectedIndex = model(0); + + /** Track whether tool is active in mat-stepper */ + public toolTabIndex = signal(-1); /** Store any user-generated photos within a folder named after module */ public photoAlbum = computed(() => { @@ -54,6 +68,7 @@ export class FarmerContentModuleHomeComponent { private componentService: PicsaCommonComponentsService, private tourService: TourService ) { + // load content on slug change and fix tour implementation effect( (onCleanup) => { const { slug } = this.params() || {}; @@ -66,21 +81,35 @@ export class FarmerContentModuleHomeComponent { }, { allowSignalWrites: true } ); + // If tool tab selected handle side-effects (routing and header) + effect( + () => { + const selectedTabIndex = this.selectedIndex(); + const contentBlocks = this.tabs()[selectedTabIndex]; + this.handleContentChangeEffects(contentBlocks); + }, + { allowSignalWrites: true } + ); } - public handleTabChange(e: MatTabChangeEvent) { - const content = this.content(); - if (content) { - const { steps, tools } = content; - // HACK - assume 1 tool which is last tab - if (e.index === steps.length) { - const [tool] = tools; - this.loadToolTab(tool); - } - // HACK - clear any headers set from within tool - else { - this.componentService.patchHeader({ title: ' ' }); - } + ngOnInit() { + this.componentService.patchHeader({ hideHeader: true, hideBackButton: true, style: 'inverted' }); + } + ngOnDestroy() { + this.componentService.patchHeader({ hideHeader: false, hideBackButton: false, style: 'primary' }); + } + + /** Handle tool routing and header changes when stepper content changed */ + private handleContentChangeEffects(stepContent: IFarmerContentStep[]) { + const toolBlock = stepContent.find((b) => b.type === 'tool') as StepTool | undefined; + if (toolBlock) { + this.toolTabIndex.set(this.selectedIndex()); + this.setToolUrl(toolBlock.tool); + } + // toogle app header if required by tool + const hideHeader = toolBlock?.tool?.showHeader ? false : true; + if (this.componentService.headerOptions().hideHeader !== hideHeader) { + this.componentService.patchHeader({ hideHeader }); } } @@ -88,19 +117,17 @@ export class FarmerContentModuleHomeComponent { if (slug) { const content: IFarmerContent = FARMER_CONTENT_DATA_BY_SLUG[slug]; if (content) { - this.content.set(content); - this.steps.set(content.steps); - this.tools.set(content.tools); - return; + return content; } } - // if content not loaded simply navigate back to parent + // If content not loaded simply navigate back to parent. this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); + return undefined; } /** When navigating to the tool tab update the url to allow the correct tool to load within a child route */ - private loadToolTab(tool: IToolData) { - if (!location.pathname.endsWith(tool.href)) { + private setToolUrl(tool: IToolData) { + if (!location.pathname.includes(`/${tool.href}`)) { this.router.navigate([tool.href], { relativeTo: this.route, replaceUrl: true }); } } diff --git a/apps/picsa-tools/resources-tool/src/app/components/resource-item/file/file.html b/apps/picsa-tools/resources-tool/src/app/components/resource-item/file/file.html index f7951ea40..607eb8f65 100644 --- a/apps/picsa-tools/resources-tool/src/app/components/resource-item/file/file.html +++ b/apps/picsa-tools/resources-tool/src/app/components/resource-item/file/file.html @@ -1,7 +1,7 @@ -
+
@if(fileURI){ } diff --git a/apps/picsa-tools/resources-tool/src/app/components/resource-item/video.ts b/apps/picsa-tools/resources-tool/src/app/components/resource-item/video.ts index eb1f0112e..0c09bc86e 100644 --- a/apps/picsa-tools/resources-tool/src/app/components/resource-item/video.ts +++ b/apps/picsa-tools/resources-tool/src/app/components/resource-item/video.ts @@ -5,7 +5,9 @@ import { IResourceFile } from '../../schemas'; @Component({ selector: 'resource-item-video', template: ` + @if(resource.title){

{{ resource.title | translate }}

+ }

{{ resource.description | translate }}

diff --git a/apps/picsa-tools/seasonal-calendar-tool/src/app/pages/create-calendar/create-calendar.component.html b/apps/picsa-tools/seasonal-calendar-tool/src/app/pages/create-calendar/create-calendar.component.html index 41d0c94d0..bc0527c18 100644 --- a/apps/picsa-tools/seasonal-calendar-tool/src/app/pages/create-calendar/create-calendar.component.html +++ b/apps/picsa-tools/seasonal-calendar-tool/src/app/pages/create-calendar/create-calendar.component.html @@ -3,7 +3,8 @@
} diff --git a/libs/components/src/components/picsa-breadcrumbs.component.ts b/libs/components/src/components/picsa-breadcrumbs.component.ts index 8811eda2d..e2fd92eb7 100644 --- a/libs/components/src/components/picsa-breadcrumbs.component.ts +++ b/libs/components/src/components/picsa-breadcrumbs.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, effect, OnDestroy, OnInit } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { debounce, filter, interval, Subject, takeUntil } from 'rxjs'; @@ -32,7 +32,18 @@ export class PicsaBreadcrumbsComponent implements OnInit, OnDestroy { public options: IBreadcrumbOptions = { hideOnPaths: {}, enabled: false }; private destroyed$ = new Subject(); private rebuild$ = new Subject(); - constructor(private componentsService: PicsaCommonComponentsService, private router: Router) {} + constructor(private componentsService: PicsaCommonComponentsService, private router: Router) { + effect(() => { + const headerOptions = this.componentsService.headerOptions(); + const title = headerOptions.title; + if (title) { + if (this.getAlias(location.pathname) !== title) { + this.setAlias(location.pathname, title); + this.rebuild$.next(true); + } + } + }); + } ngOnInit() { this.constructBreadcrumbAliases(); @@ -75,14 +86,6 @@ export class PicsaBreadcrumbsComponent implements OnInit, OnDestroy { } /** Listen to changes to title triggered directly service */ private listenToServiceChanges() { - this.componentsService.headerOptions$.pipe(takeUntil(this.destroyed$)).subscribe(({ title }) => { - if (title) { - if (this.getAlias(location.pathname) !== title) { - this.setAlias(location.pathname, title); - this.rebuild$.next(true); - } - } - }); this.componentsService.breadcrumbOptions$.pipe(takeUntil(this.destroyed$)).subscribe((options) => { this.options = options; // diff --git a/libs/components/src/components/picsa-header.component.scss b/libs/components/src/components/picsa-header.component.scss index ec3b7b44c..94905e1bd 100644 --- a/libs/components/src/components/picsa-header.component.scss +++ b/libs/components/src/components/picsa-header.component.scss @@ -19,14 +19,10 @@ header[data-style='inverted'] { display: flex; align-items: center; justify-content: flex-start; - min-width: 100px; + @apply sm:min-w-24; } .central-content { - // use absolute position so content can still be central despite start/end content - position: absolute; - left: 100px; // ensure fits alongside back button - width: calc(100% - 164px); flex: 1; display: flex; align-items: center; @@ -37,7 +33,7 @@ header[data-style='inverted'] { display: flex; align-items: center; justify-content: flex-end; - min-width: 100px; + @apply sm:min-w-24; } h1 { font-size: 2rem; diff --git a/libs/components/src/components/picsa-header.component.ts b/libs/components/src/components/picsa-header.component.ts index 3d027ee9d..e73f2ba45 100644 --- a/libs/components/src/components/picsa-header.component.ts +++ b/libs/components/src/components/picsa-header.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, effect, OnDestroy, OnInit, signal } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRouteSnapshot, @@ -14,14 +14,14 @@ import { IHeaderOptions, PicsaCommonComponentsService } from '../services/compon @Component({ selector: 'picsa-header', template: ` -
+
- - + +

@@ -37,13 +37,15 @@ import { IHeaderOptions, PicsaCommonComponentsService } from '../services/compon `, styleUrls: ['./picsa-header.component.scss'], }) -export class PicsaHeaderComponent implements OnInit, OnDestroy, AfterViewInit { +export class PicsaHeaderComponent implements OnInit, OnDestroy { public title = ''; public style: 'primary' | 'inverted' = 'primary'; - public hideBackButton? = false; + public hideBackButton = signal(false); + public hideHeader = signal(false); private destroyed$ = new Subject(); /** Inject dynamic content into header slots using angular cdk portal */ + public cdkPortalStart: IHeaderOptions['cdkPortalStart']; public cdkPortalCenter: IHeaderOptions['cdkPortalCenter']; public cdkPortalEnd: IHeaderOptions['cdkPortalEnd']; @@ -52,14 +54,16 @@ export class PicsaHeaderComponent implements OnInit, OnDestroy, AfterViewInit { private router: Router, private titleStrategy: DefaultTitleStrategy, private titleService: Title - ) {} + ) { + effect(() => { + const headerOptions = this.componentsService.headerOptions(); + this.handleHeaderOptionsChange(headerOptions); + }); + } ngOnInit() { this.listenToRouteChanges(); } - ngAfterViewInit(): void { - this.listenToServiceOptionChanges(); - } ngOnDestroy() { this.destroyed$.next(true); @@ -104,26 +108,33 @@ export class PicsaHeaderComponent implements OnInit, OnDestroy, AfterViewInit { }); } - /** Listen to changes to title triggered directly service */ - private listenToServiceOptionChanges() { - this.componentsService.headerOptions$.pipe(takeUntil(this.destroyed$)).subscribe((options) => { - const { title, style, hideBackButton } = options; - requestAnimationFrame(() => { - if (title) { - this.title = title; - this.titleService.setTitle(title); - } - if (style) { - this.style = style; - } - this.setPortalContent(options); - // hide back button when set or if on farmer or extension homepages - this.hideBackButton = hideBackButton || ['/', '/farmer', '/extension'].includes(location.pathname); - }); + private handleHeaderOptionsChange(options: IHeaderOptions) { + const { title, style, hideBackButton, hideHeader } = options; + requestAnimationFrame(() => { + if (title) { + this.title = title; + this.titleService.setTitle(title); + } + if (style) { + this.style = style; + } + this.setPortalContent(options); + // hide back button when set or if on farmer or extension homepages + const shouldHideBackButton = hideBackButton || ['/', '/farmer', '/extension'].includes(location.pathname); + this.hideBackButton.set(shouldHideBackButton); + this.hideHeader.set(hideHeader ? true : false); }); } + private setPortalContent(options: IHeaderOptions) { - const { cdkPortalCenter, cdkPortalEnd } = options; + const { cdkPortalStart, cdkPortalCenter, cdkPortalEnd } = options; + // Center Portal + if (!cdkPortalStart) { + this.cdkPortalStart = undefined; + } + if (!cdkPortalStart?.isAttached) { + this.cdkPortalStart = cdkPortalStart; + } // Center Portal if (!cdkPortalCenter) { this.cdkPortalCenter = undefined; diff --git a/libs/components/src/services/components.service.ts b/libs/components/src/services/components.service.ts index 84cdcb054..2cc1f8581 100644 --- a/libs/components/src/services/components.service.ts +++ b/libs/components/src/services/components.service.ts @@ -1,15 +1,17 @@ import { DomPortal, TemplatePortal } from '@angular/cdk/portal'; -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; export interface IHeaderOptions { title?: string; style?: 'inverted' | 'primary'; - /** Angular portal cdk to inject component into header central slot */ + /** Angular portal cdk to inject component into header slots */ + cdkPortalStart?: DomPortal | TemplatePortal; cdkPortalEnd?: DomPortal | TemplatePortal; cdkPortalCenter?: DomPortal | TemplatePortal; hideBackButton?: boolean; + hideHeader?: boolean; } export interface IBreadcrumbOptions { enabled?: boolean; @@ -18,7 +20,7 @@ export interface IBreadcrumbOptions { @Injectable({ providedIn: 'root' }) export class PicsaCommonComponentsService { - headerOptions$ = new BehaviorSubject({}); + headerOptions = signal({}); breadcrumbOptions$ = new BehaviorSubject({}); /** Track navigation history - used by back-button components (multi-instance) */ @@ -26,11 +28,11 @@ export class PicsaCommonComponentsService { /** Programatically set the header options such as title and style */ public setHeader(options: Partial) { - this.headerOptions$.next(options); + this.headerOptions.set(options); } /** Update partial header options, retaining existing options where not defined */ public patchHeader(update: Partial) { - this.setHeader({ ...this.headerOptions$.value, ...update }); + this.setHeader({ ...this.headerOptions(), ...update }); } public updateBreadcrumbOptions(update: Partial) { diff --git a/libs/data/deployments/countries.ts b/libs/data/deployments/countries.ts index 25862d210..1f88faea0 100644 --- a/libs/data/deployments/countries.ts +++ b/libs/data/deployments/countries.ts @@ -9,10 +9,11 @@ import { arrayToHashmap } from '@picsa/utils'; export type ICountryCode = Database['public']['Enums']['country_code']; const COUNTRIES_BASE: { [key in ICountryCode]: { label: string } } = { - global: { label: 'Global' }, mw: { label: 'Malawi' }, zm: { label: 'Zambia' }, tj: { label: 'Tajikistan' }, + // order entry will also be used for language select screen, so keep global at bottom + global: { label: 'Global' }, }; export const COUNTRIES_DATA = Object.entries(COUNTRIES_BASE) diff --git a/libs/data/farmer_content/data/content/0_intro.ts b/libs/data/farmer_content/data/content/0_intro.ts index a8fe5504c..9e628cd7a 100644 --- a/libs/data/farmer_content/data/content/0_intro.ts +++ b/libs/data/farmer_content/data/content/0_intro.ts @@ -1,23 +1,33 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; import { IFarmerContent, IFarmerContentStep } from '../../types'; -import { PICSA_FARMER_VIDEOS_HASHMAP, PICSA_VIDEO_TESTIMONIAL_HASHMAP } from '@picsa/data/resources'; +import { PICSA_FARMER_VIDEOS_HASHMAP, PICSA_VIDEO_TESTIMONIAL_DATA } from '@picsa/data/resources'; -const steps: IFarmerContentStep[] = [ - { type: 'video', video: PICSA_FARMER_VIDEOS_HASHMAP.intro, tabLabel: translateMarker('Intro') }, - { - type: 'video_playlist', - videos: Object.values(PICSA_VIDEO_TESTIMONIAL_HASHMAP), - tabLabel: translateMarker('Testimonials'), - tabMatIcon: 'people', - }, +const title = translateMarker('What is PICSA?'); + +const steps: IFarmerContent['steps'] = [ + [ + { type: 'text', title }, + { type: 'video', video: PICSA_FARMER_VIDEOS_HASHMAP.intro }, + ], + [ + { + type: 'text', + title: translateMarker('Testimonials'), + }, + ...PICSA_VIDEO_TESTIMONIAL_DATA.map( + (video): IFarmerContentStep => ({ + type: 'video', + video, + }) + ), + ], ]; const content: Omit = { slug: 'intro', - title: translateMarker('What is PICSA?'), - tools: [], - tags: [{ label: translateMarker('Tutorials'), color: 'secondary' }], + title, steps, + tags: [{ label: translateMarker('Tutorials'), color: 'secondary' }], }; export default content; diff --git a/libs/data/farmer_content/data/content/1_what_you_do.ts b/libs/data/farmer_content/data/content/1_what_you_do.ts index 18a774785..12ae74055 100644 --- a/libs/data/farmer_content/data/content/1_what_you_do.ts +++ b/libs/data/farmer_content/data/content/1_what_you_do.ts @@ -1,30 +1,34 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; +import { IFarmerContent } from '../../types'; import { TOOLS_DATA_HASHMAP } from '../tools'; import { PICSA_FARMER_VIDEOS_HASHMAP } from '@picsa/data/resources'; -const { seasonal_calendar } = TOOLS_DATA_HASHMAP; +const title = translateMarker('What do you currently do?'); -const steps: IFarmerContentStep[] = [ - { - type: 'video', - video: PICSA_FARMER_VIDEOS_HASHMAP.ram, - tabLabel: translateMarker('Ram'), - }, - { - type: 'video', - video: PICSA_FARMER_VIDEOS_HASHMAP.seasonal_calendar, - tabLabel: translateMarker('Calendar'), - }, +const steps: IFarmerContent['steps'] = [ + [ + { type: 'text', title: translateMarker('Resource Allocation Map') }, + { + type: 'video', + video: PICSA_FARMER_VIDEOS_HASHMAP.ram, + }, + ], + [ + { type: 'text', title: translateMarker('Seasonal Calendar') }, + { + type: 'video', + video: PICSA_FARMER_VIDEOS_HASHMAP.seasonal_calendar, + }, + ], + [{ type: 'tool', tool: TOOLS_DATA_HASHMAP.seasonal_calendar }], + [{ type: 'review' }], ]; const content: Omit = { slug: 'what-do-you-currently-do', - title: translateMarker('What do you currently do?'), - tools: [seasonal_calendar], - tags: [{ label: translateMarker('Resource Allocation Map') }], + title, + tags: [{ label: translateMarker('Resource Allocation Map') }, { label: TOOLS_DATA_HASHMAP.seasonal_calendar.label }], steps, - showReviewSection: true, }; export default content; diff --git a/libs/data/farmer_content/data/content/2_climate_change.ts b/libs/data/farmer_content/data/content/2_climate_change.ts index bd940dc62..f366f4ade 100644 --- a/libs/data/farmer_content/data/content/2_climate_change.ts +++ b/libs/data/farmer_content/data/content/2_climate_change.ts @@ -1,24 +1,35 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; +import { IFarmerContent } from '../../types'; import { TOOLS_DATA_HASHMAP } from '../tools'; import { PICSA_FARMER_VIDEOS_HASHMAP } from '@picsa/data/resources'; -const { climate } = TOOLS_DATA_HASHMAP; +const title = translateMarker('What is happening to the climate in your area?'); -const steps: IFarmerContentStep[] = [ - { - type: 'video', - video: PICSA_FARMER_VIDEOS_HASHMAP.historic_climate, - }, +const steps: IFarmerContent['steps'] = [ + [ + { + type: 'text', + title, + }, + { + type: 'video', + video: PICSA_FARMER_VIDEOS_HASHMAP.historic_climate, + }, + ], + [ + { + type: 'tool', + tool: TOOLS_DATA_HASHMAP.climate, + }, + ], + [{ type: 'review' }], ]; const content: Omit = { slug: 'is-the-climate-changing', - title: translateMarker('What is happening to the climate in your area?'), - tools: [climate], - tags: [], + title, + tags: [{ label: TOOLS_DATA_HASHMAP.climate.label }], steps, - showReviewSection: true, }; export default content; diff --git a/libs/data/farmer_content/data/content/3_opportunities_risks.ts b/libs/data/farmer_content/data/content/3_opportunities_risks.ts index 89a41ce26..c3ddce6cb 100644 --- a/libs/data/farmer_content/data/content/3_opportunities_risks.ts +++ b/libs/data/farmer_content/data/content/3_opportunities_risks.ts @@ -1,24 +1,27 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; +import { IFarmerContent } from '../../types'; import { TOOLS_DATA_HASHMAP } from '../tools'; import { PICSA_FARMER_VIDEOS_HASHMAP } from '@picsa/data/resources'; -const { probability_and_risk } = TOOLS_DATA_HASHMAP; +const title = translateMarker('What are the opportunities and risk?'); -const steps: IFarmerContentStep[] = [ - { - type: 'video', - video: PICSA_FARMER_VIDEOS_HASHMAP.probability_risk, - }, +const steps: IFarmerContent['steps'] = [ + [ + { type: 'text', title }, + { + type: 'video', + video: PICSA_FARMER_VIDEOS_HASHMAP.probability_risk, + }, + ], + [{ type: 'tool', tool: TOOLS_DATA_HASHMAP.probability_and_risk }], + [{ type: 'review' }], ]; const content: Omit = { slug: 'opportunities-and-risk', - title: translateMarker('What are the opportunities and risk?'), - tools: [probability_and_risk], - tags: [], + title, + tags: [{ label: TOOLS_DATA_HASHMAP.probability_and_risk.label }], steps, - showReviewSection: true, }; export default content; diff --git a/libs/data/farmer_content/data/content/4_what_are_the_options.ts b/libs/data/farmer_content/data/content/4_what_are_the_options.ts index 67af2baab..6a454ba9d 100644 --- a/libs/data/farmer_content/data/content/4_what_are_the_options.ts +++ b/libs/data/farmer_content/data/content/4_what_are_the_options.ts @@ -1,24 +1,35 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; +import { IFarmerContent } from '../../types'; import { TOOLS_DATA_HASHMAP } from '../tools'; import { PICSA_FARMER_VIDEOS_HASHMAP } from '@picsa/data/resources'; -const { options } = TOOLS_DATA_HASHMAP; +const title = translateMarker('What changes can you make?'); -const steps: IFarmerContentStep[] = [ - { - type: 'video', - video: PICSA_FARMER_VIDEOS_HASHMAP.options, - }, +const steps: IFarmerContent['steps'] = [ + [ + { + type: 'text', + title, + }, + { + type: 'video', + video: PICSA_FARMER_VIDEOS_HASHMAP.options, + }, + ], + [ + { + type: 'tool', + tool: TOOLS_DATA_HASHMAP.options, + }, + ], + [{ type: 'review' }], ]; const content: Omit = { slug: 'what-are-the-options', - title: translateMarker('What changes can you make?'), - tools: [options], - tags: [], + title, + tags: [{ label: translateMarker('Options') }], steps, - showReviewSection: true, }; export default content; diff --git a/libs/data/farmer_content/data/content/5_compare_options.ts b/libs/data/farmer_content/data/content/5_compare_options.ts index 65072b080..b83c1cd01 100644 --- a/libs/data/farmer_content/data/content/5_compare_options.ts +++ b/libs/data/farmer_content/data/content/5_compare_options.ts @@ -1,24 +1,36 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; +import { IFarmerContent } from '../../types'; import { TOOLS_DATA_HASHMAP } from '../tools'; import { PICSA_FARMER_VIDEOS_HASHMAP } from '@picsa/data/resources'; -const { budget } = TOOLS_DATA_HASHMAP; +const title = translateMarker('Are the changes a good idea?'); -const steps: IFarmerContentStep[] = [ - { - type: 'video', - video: PICSA_FARMER_VIDEOS_HASHMAP.participatory_budget, - }, +const steps: IFarmerContent['steps'] = [ + [ + { type: 'text', title }, + { + type: 'video', + video: PICSA_FARMER_VIDEOS_HASHMAP.participatory_budget, + }, + ], + [ + { + type: 'tool', + tool: TOOLS_DATA_HASHMAP.budget, + }, + ], + [ + { + type: 'review', + }, + ], ]; const content: Omit = { slug: 'compare-options', - title: translateMarker('Are the changes a good idea?'), - tools: [budget], - tags: [], + title, + tags: [{ label: TOOLS_DATA_HASHMAP.budget.label }], steps, - showReviewSection: true, }; export default content; diff --git a/libs/data/farmer_content/data/content/6_decide_and_plan.ts b/libs/data/farmer_content/data/content/6_decide_and_plan.ts index 869fce884..2e0429dee 100644 --- a/libs/data/farmer_content/data/content/6_decide_and_plan.ts +++ b/libs/data/farmer_content/data/content/6_decide_and_plan.ts @@ -1,19 +1,16 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; -import { TOOLS_DATA_HASHMAP } from '../tools'; +import { IFarmerContent } from '../../types'; -const {} = TOOLS_DATA_HASHMAP; +const title = translateMarker('You decide and make a plan'); -const steps: IFarmerContentStep[] = []; +const steps: IFarmerContent['steps'] = []; const content: Omit = { slug: 'decide-and-plan', - title: translateMarker('You decide and make a plan'), - tools: [], + title, tags: [], steps, disabled: true, - showReviewSection: true, }; export default content; diff --git a/libs/data/farmer_content/data/content/7_use_forecasts.ts b/libs/data/farmer_content/data/content/7_use_forecasts.ts index cb9cd644c..46e0caad3 100644 --- a/libs/data/farmer_content/data/content/7_use_forecasts.ts +++ b/libs/data/farmer_content/data/content/7_use_forecasts.ts @@ -1,16 +1,14 @@ import { marker as translateMarker } from '@biesbjerg/ngx-translate-extract-marker'; -import { IFarmerContent, IFarmerContentStep } from '../../types'; -import { TOOLS_DATA_HASHMAP } from '../tools'; +import { IFarmerContent } from '../../types'; -const {} = TOOLS_DATA_HASHMAP; +const title = translateMarker('Use the forecasts to update and adapt your plans'); -const steps: IFarmerContentStep[] = []; +const steps: IFarmerContent['steps'] = []; const content: Omit = { slug: 'use-forecasts', - title: translateMarker('Use the forecasts to update and adapt your plans'), - tools: [], + title, tags: [], steps, disabled: true, diff --git a/libs/data/farmer_content/data/tools.ts b/libs/data/farmer_content/data/tools.ts index fd10d821c..43ef94ac2 100644 --- a/libs/data/farmer_content/data/tools.ts +++ b/libs/data/farmer_content/data/tools.ts @@ -9,18 +9,18 @@ import { IToolData } from '../types'; // TODO - consider including svgIcons and using for extension tool also (refactor to folder and icon pack) const TOOLS_BASE = { - budget: { label: translateMarker('Budget'), href: 'budget', tabLabel: translateMarker('Tool') }, - climate: { label: translateMarker('Climate'), href: 'climate', tabLabel: translateMarker('Tool') }, - options: { label: translateMarker('Options'), href: 'option', tabLabel: translateMarker('Tool') }, + budget: { label: translateMarker('Budget'), href: 'budget', title: translateMarker('Tool'), showHeader: true }, + climate: { label: translateMarker('Climate'), href: 'climate', title: translateMarker('Tool'), showHeader: true }, + options: { label: translateMarker('Options'), href: 'option', title: translateMarker('Tool') }, probability_and_risk: { label: translateMarker('Probability and Risk'), href: 'crop-probability', - tabLabel: translateMarker('Tool'), + title: translateMarker('Tool'), }, seasonal_calendar: { label: translateMarker('Seasonal Calendar'), href: 'seasonal-calendar', - tabLabel: translateMarker('Tool'), + title: translateMarker('Tool'), }, }; diff --git a/libs/data/farmer_content/types.ts b/libs/data/farmer_content/types.ts index cb82dd177..f4afedea6 100644 --- a/libs/data/farmer_content/types.ts +++ b/libs/data/farmer_content/types.ts @@ -9,37 +9,41 @@ export interface IToolData { /** base url to access tool within app */ href: string; tabLabel?: string; + /** Show default app header of tool directly uses */ + showHeader?: boolean; } -interface IContentStepBase { - type: string; - /** Label to show when selecting content from tab */ - tabLabel?: string; - /** Icon to show in tab */ - tabMatIcon?: string; +interface StepText { + type: 'text'; + text?: string; + title?: string; } -interface IFarmerContentStepVideo extends IContentStepBase { +export interface StepTool { + type: 'tool'; + tool: IToolData; +} +interface StepVideo { type: 'video'; video: IPicsaVideoData; } -interface IFarmerContentStepVideoPlaylist extends IContentStepBase { - type: 'video_playlist'; - videos: IPicsaVideoData[]; +interface StepReview { + type: 'review'; } -export type IFarmerContentStep = IFarmerContentStepVideo | IFarmerContentStepVideoPlaylist; +export type IFarmerContentStep = StepReview | StepVideo | StepText | StepTool; export interface IFarmerContent { id: IFarmerContentId; slug: string; icon_path: string; title: string; - tools: IToolData[]; + + /** Steps contain dynamic content blocks, grouped within a mat-stepper **/ + steps: IFarmerContentStep[][]; + tags: { label: string; color?: 'primary' | 'secondary' }[]; - steps: IFarmerContentStep[]; + disabled?: boolean; - /** Include a photo-input section as part of review */ - showReviewSection?: boolean; } diff --git a/libs/environments/src/version.ts b/libs/environments/src/version.ts index 11c5173be..c4797dfd5 100644 --- a/libs/environments/src/version.ts +++ b/libs/environments/src/version.ts @@ -4,5 +4,5 @@ import packageJson from '../../../package.json'; export const APP_VERSION = { number: packageJson.version, - date: '2024-08-14', + date: '2024-10-01', }; diff --git a/libs/shared/src/features/animations/animation.component.html b/libs/shared/src/features/animations/animation.component.html new file mode 100644 index 000000000..f5b03e07c --- /dev/null +++ b/libs/shared/src/features/animations/animation.component.html @@ -0,0 +1,7 @@ + + + +
+ +
+
diff --git a/libs/shared/src/features/animations/animation.component.scss b/libs/shared/src/features/animations/animation.component.scss index 5dfc06884..f70facfba 100644 --- a/libs/shared/src/features/animations/animation.component.scss +++ b/libs/shared/src/features/animations/animation.component.scss @@ -1,25 +1,9 @@ -:host { - display: block; +.animation-background { + background: var(--color-primary); + opacity: 0.95; + border-radius: 4px; width: 165px; height: 165px; - --background-color: var(--color-primary); - --background-opacity: 0.95; - --background-border-radius: 4px; - // Float in center of screen - &[position='float'] { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 2; - } -} -.animation-background { - background: var(--background-color); - opacity: var(--background-opacity); - border-radius: var(--background-border-radius); - width: 100%; - height: 100%; position: relative; } .animation-container { diff --git a/libs/shared/src/features/animations/animation.component.ts b/libs/shared/src/features/animations/animation.component.ts index f0de06ffc..1e3e57bb5 100644 --- a/libs/shared/src/features/animations/animation.component.ts +++ b/libs/shared/src/features/animations/animation.component.ts @@ -1,4 +1,5 @@ -import { Component, ElementRef, Input, OnInit } from '@angular/core'; +import { Component, ElementRef, input, OnInit, TemplateRef, viewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { _wait } from '@picsa/utils'; import { AnimationOptions, BMCompleteLoopEvent } from 'ngx-lottie'; @@ -7,51 +8,48 @@ import type { IAvailableAnimations } from './models'; @Component({ selector: 'picsa-animation', styleUrls: ['./animation.component.scss'], - template: `
- - - -
`, + templateUrl: 'animation.component.html', }) export class PicsaAnimationComponent implements OnInit { /** Name of animation file to display */ - @Input() name: IAvailableAnimations; - /** Duration in ms to show animation for */ - @Input() duration?: number; + name = input.required(); /** Number of loops to show animation for */ - @Input() loops?: number; - /** Apply absolute positioning to float in center (default inline) */ - @Input() position?: 'inline' | 'float' = 'inline'; + loops = input.required(); /** Delay display of animation by set number of ms */ - @Input() delay?: number; + delay = input(); + + private dialogTemplate = viewChild.required>('dialogTemplate'); public options: AnimationOptions; - constructor(private host: ElementRef) {} + constructor(private host: ElementRef, private dialog: MatDialog) {} - private selfDestruct() { + private async selfDestruct() { + this.dialog.closeAll(); + await _wait(300); this.host.nativeElement.remove(); } async ngOnInit() { - if (this.delay) { - await _wait(this.delay); + if (this.delay()) { + await _wait(this.delay()); } - this.loadAnimation(this.name); + this.loadAnimation(this.name()); } /** Track number of times animation has looped, destroy if loops limit provided */ loopComplete(e: BMCompleteLoopEvent) { - if (this.loops && (e.currentLoop as number) >= this.loops) { + if ((e.currentLoop as number) >= this.loops()) { this.selfDestruct(); } } private loadAnimation(name: string) { - requestAnimationFrame(() => { + requestAnimationFrame(async () => { this.options = { path: `assets/animations/${name}.json`, }; + this.dialog.open(this.dialogTemplate(), { disableClose: true, panelClass: 'no-padding' }); }); } } diff --git a/libs/shared/src/features/map/map.html b/libs/shared/src/features/map/map.html index ff4f39fcc..86d625f93 100644 --- a/libs/shared/src/features/map/map.html +++ b/libs/shared/src/features/map/map.html @@ -1,6 +1 @@ -
+
diff --git a/libs/shared/src/features/map/map.scss b/libs/shared/src/features/map/map.scss index cdb927301..03e2fffba 100644 --- a/libs/shared/src/features/map/map.scss +++ b/libs/shared/src/features/map/map.scss @@ -1,6 +1,10 @@ // import "./node_modules/leaflet/dist/leaflet.css" to add leaflet styles // this has to be done in angular.json +.map-container { + height: 100%; +} + .countryLabel { fill: transparent; background: rgba(255, 255, 255, 0); diff --git a/libs/shared/src/features/map/map.ts b/libs/shared/src/features/map/map.ts index daf69f628..cc455e3f2 100644 --- a/libs/shared/src/features/map/map.ts +++ b/libs/shared/src/features/map/map.ts @@ -1,5 +1,15 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + effect, + EventEmitter, + input, + OnInit, + Output, + signal, + ViewEncapsulation, +} from '@angular/core'; import { LeafletModule } from '@asymmetrik/ngx-leaflet'; import type { Feature, GeoJsonObject, Geometry } from 'geojson'; import * as L from 'leaflet'; @@ -13,39 +23,53 @@ import * as GEOJSON from './geoJson'; styleUrls: ['./map.scss'], encapsulation: ViewEncapsulation.None, standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PicsaMapComponent { +export class PicsaMapComponent implements OnInit { @Output() onMapReady = new EventEmitter(); @Output() onLayerClick = new EventEmitter(); @Output() onMarkerClick = new EventEmitter(); - @Input() mapOptions: L.MapOptions = {}; - @Input() basemapOptions: Partial = {}; - - /** Stored list of input marker data */ - private _markers: IMapMarker[]; - /** Handle adding map markers on input change */ - @Input() set markers(markers: IMapMarker[]) { - if (markers) { - this._markers = markers; - // add markers if map already initialised, otherwise will be added onMapReady - if (this.map) { - this.addMarkers(markers); - } - } - } + mapOptions = input({}); + basemapOptions = input>({}); + markers = input([]); /** List of rendered markers with map data */ private renderedMarkers: L.Marker[] = []; - // make native map element available directly - public map: L.Map; + // make native map element available directly as signal + public map = signal(null as any); + // expose full leaflet functionality for use within parent components public L = L; // active marker used to toggle style classes private _activeMarker: L.Marker; - // default options are overwritten via input setter - _mapOptions: L.MapOptions = MAP_DEFAULTS; + + /** Full set of map options merged from input options and default */ + public _mapOptions = signal(null as any); + + constructor() { + // Load any input markers whenever both markers and map exist + effect(() => { + const inputMarkers = this.markers(); + const map = this.map(); + if (map && inputMarkers?.length > 0) { + this.addMarkers(inputMarkers); + } + }); + // Observe layout size changes, use map invalidate method on change + // to ensure map correctly setup. E.g. when switching tabs in farmer version + effect((cleanup) => { + const container = this.map().getContainer(); + const observer = new ResizeObserver(() => { + this.map().invalidateSize(); + }); + observer.observe(container); + cleanup(() => { + observer.disconnect(); + }); + }); + } ngOnInit() { // the user provides basemap options separate to general map options, so combine here @@ -53,14 +77,13 @@ export class PicsaMapComponent { const basemapOptions = { ...BASEMAP_DEFAULTS, ...this.basemapOptions }; const basemap = L.tileLayer(basemapOptions.src, basemapOptions); const mapOptions = { ...MAP_DEFAULTS, ...this.mapOptions }; - this._mapOptions = { ...mapOptions, layers: [basemap] }; - // this.markOffClosestStation() + this._mapOptions.set({ ...mapOptions, layers: [basemap] }); } /** Programatically set the active map marker and trigger click callback */ public setActiveMarker(marker: IMapMarker) { const { _index } = marker; - this._onMarkerClick(this._markers[_index], this.renderedMarkers[_index]); + this._onMarkerClick(this.markers()[_index], this.renderedMarkers[_index]); } /** Render a marker for current user location */ @@ -71,7 +94,7 @@ export class PicsaMapComponent { html: L.Util.template(LOCATION_ICON_BLACK, 'color:white'), }); const userMarker = L.marker([lat, lng], { icon }); - userMarker.addTo(this.map); + userMarker.addTo(this.map()); } private addMarkers(mapMarkers: IMapMarker[], fitMap = true) { @@ -86,7 +109,7 @@ export class PicsaMapComponent { marker.on({ click: () => this._onMarkerClick(m, marker), }); - marker.addTo(this.map); + marker.addTo(this.map()); this.renderedMarkers.push(marker); }); if (fitMap && mapMarkers.length > 0) { @@ -97,10 +120,7 @@ export class PicsaMapComponent { // when the map is ready it emits event with map, and also binds map to // public api to be accessed by other services _onMapReady(map: L.Map) { - this.map = map; - if (this._markers) { - this.addMarkers(this._markers); - } + this.map.set(map); this.onMapReady.emit(map); } @@ -108,7 +128,7 @@ export class PicsaMapComponent { private fitMapToMarkers(markers: IMapMarker[]) { const latLngs = markers.map((m) => m.latlng); const bounds = new L.LatLngBounds(latLngs as any); - this.map.fitBounds(bounds, { maxZoom: 8, padding: [10, 10] }); + this.map().fitBounds(bounds, { maxZoom: 8, padding: [10, 10] }); } /** Generate default (inactive) and active icons for a marker */ @@ -128,7 +148,7 @@ export class PicsaMapComponent { // NOTE L.Layer doesn't recognize _bounds prop so just pass as any protected _onLayerClick(layer: any) { const bounds = layer._bounds as L.LatLngBounds; - this.map.fitBounds(bounds); + this.map().fitBounds(bounds); this.onLayerClick.emit(layer); } @@ -139,7 +159,7 @@ export class PicsaMapComponent { this._activeMarker.setIcon(icon); } const [lat, lng] = m.latlng; - this.map.flyTo([lat, lng], 10); + this.map().flyTo([lat, lng], 10); marker.setIcon(activeIcon); this._activeMarker = marker; this.onMarkerClick.emit(m); @@ -154,7 +174,7 @@ export class PicsaMapComponent { onEachFeature: (feature, layer) => this.setFeature(feature, layer), style: GEOJSON_STYLE, }); - geojsonLayer.addTo(this.map); + geojsonLayer.addTo(this.map()); // *** TODO - ADD METHOD TO CALCULATE AND AUTO FIT BOUNDS DEPENDENT ON USER } diff --git a/libs/shared/src/features/video-player/video-player.component.scss b/libs/shared/src/features/video-player/video-player.component.scss index 9c768e860..75a0128cf 100644 --- a/libs/shared/src/features/video-player/video-player.component.scss +++ b/libs/shared/src/features/video-player/video-player.component.scss @@ -7,13 +7,6 @@ $playerWidth: 854px; width: 100%; max-width: $playerWidth; aspect-ratio: 16/9; - // Assuming that video player displayed within 1rem content padding, - // force video to display across all space (full width) - @media screen and (max-width: $playerWidth) { - width: calc(100% + 2rem); - margin-left: -1rem; - margin-right: -1rem; - } } .placeholderContainer { position: relative; diff --git a/libs/shared/src/features/video-player/video-player.component.ts b/libs/shared/src/features/video-player/video-player.component.ts index dee3fb60c..eded100c8 100644 --- a/libs/shared/src/features/video-player/video-player.component.ts +++ b/libs/shared/src/features/video-player/video-player.component.ts @@ -195,6 +195,8 @@ export class VideoPlayerComponent implements OnDestroy { width: clientWidth, height: Math.round((clientWidth * 9) / 16), displayMode: 'landscape', + bkmodeEnabled: false, + pipEnabled: false, }; if (Capacitor.isNativePlatform()) { defaultOptions.mode = 'fullscreen'; diff --git a/libs/theme/src/_index.scss b/libs/theme/src/_index.scss index dc077e618..abf8ac59d 100644 --- a/libs/theme/src/_index.scss +++ b/libs/theme/src/_index.scss @@ -8,6 +8,7 @@ @import 'layout'; @import 'misc'; @import 'overrides'; +@import 'tailwind'; @import 'themes'; @import 'typography'; @import 'variables'; diff --git a/libs/theme/src/_layout.scss b/libs/theme/src/_layout.scss index 602a5da92..cd5baf805 100644 --- a/libs/theme/src/_layout.scss +++ b/libs/theme/src/_layout.scss @@ -3,6 +3,16 @@ body { margin: 0; height: 100vh; overflow: hidden; + + // Add page padding for different breakpoints + // Store as variable so can also used to calculate positioning on nested elements + --page-padding: 0px; + @screen sm { + --page-padding: theme(spacing.4); + } + @screen md { + --page-padding: theme(spacing.4); + } } .page { @@ -18,12 +28,17 @@ body { .page-content { flex: 1; background-color: white; - padding: 1rem; display: flex; flex-direction: column; position: relative; z-index: 1; overflow: auto; + + padding: var(--page-padding); +} +// remove padding when displaying within farmer-content +farmer-content-module-home > .page-content { + padding: 0; } .no-padding { padding: 0 !important; diff --git a/libs/theme/src/_overrides.scss b/libs/theme/src/_overrides.scss index 74bbfa880..c63f026e1 100644 --- a/libs/theme/src/_overrides.scss +++ b/libs/theme/src/_overrides.scss @@ -49,6 +49,20 @@ mat-dialog-container.mat-mdc-dialog-container { } } +// Increase dialog overlay width on small devices (600px) +.cdk-overlay-pane.mat-mdc-dialog-panel { + --mat-dialog-container-small-max-width: calc(100vw - theme(spacing.4)); + // Budget tool picker full screen + &.budget-dialog { + --mat-dialog-container-small-max-width: 100vw; + width: 100vw; + @apply max-w-screen-md; + .mat-mdc-dialog-surface { + padding: 0; + } + } +} + // Remove padding from dialog action buttons div.mdc-dialog__actions { padding-left: 0; @@ -133,22 +147,10 @@ mat-tab-group.height-fill { flex: 1; } } -// Allow tabs which have bottom header absolute positioned -mat-tab-group.sticky-header-bottom { - position: relative; + +mat-tab-group.no-header { & > mat-tab-header { - background: green; - position: fixed; - bottom: 0; - // HACK - ensure farmer content aligns on larger screen sizes - width: calc(100% - 32px); - background: white; - z-index: 2; - // HACK - use with farmer content that has max width (hard to inherit) - max-width: 800px; - } - & > .mat-mdc-tab-body-wrapper { - margin-bottom: 32px; + display: none; } } diff --git a/libs/theme/src/_tailwind.scss b/libs/theme/src/_tailwind.scss new file mode 100644 index 000000000..194dbbacd --- /dev/null +++ b/libs/theme/src/_tailwind.scss @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + h2 { + @apply text-2xl my-4 text-center; + } +} diff --git a/libs/theme/src/tailwind.config.js b/libs/theme/src/tailwind.config.js new file mode 100644 index 000000000..8668a6314 --- /dev/null +++ b/libs/theme/src/tailwind.config.js @@ -0,0 +1,61 @@ +const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); +const { join } = require('path'); + +/** + * @type {import('tailwindcss').Config} + * Base config used as preset in child modules + */ +const configBase = { + theme: { + extend: {}, + // only provide breakpoints for sm, md and lg, aligned to angular-material spec + // https://material.angular.io/cdk/layout/overview + screens: { + sm: '600px', + md: '960px', + lg: '1280px', + }, + // expose same colors as theme + // https://www.freedium.cfd/https://medium.com/@icedlee337/how-to-integrate-tailwind-and-angular-material-themes-1591af005457 + colors: { + primary: 'var(--color-primary)', + secondary: 'var(--color-primary)', + black: 'var(--color-black)', + }, + }, + plugins: [], + corePlugins: { + // disable pre-flight plugin that removes base styles (backwards compatibility with existing app) + // https://tailwindcss.com/docs/preflight#disabling-preflight + preflight: false, + }, +}; + +/** + * Generate a list of all filepath dependencies for a given project + * This includes child dependencies (e.g. picsa-tools or common libs) as extracted by NX + * It excludes webcomponents which bundle their own code + * + * This list is used by tailwind to identify what classes to include/exclude in bundling + */ +const getContentDependencies = (dirname) => + [join(dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), ...createGlobPatternsForDependencies(dirname)].filter( + (p) => !p.includes('webcomponents') + ); + +/** + * Generate a full tailwind configuration to use within any project or lib + * + * NOTE - whilst tailwind can use config `presets:[]` property to manage inheritence, + * it is assumed easier (for now) to just generate a single config file and avoid tool-specific overrides + * @type {import('tailwindcss').Config} + */ +const generateSharedTailwindConfig = (dirname) => { + // when generating from child dir include generation of all project dependency paths + + /** @type {import('tailwindcss').Config} */ + const config = { ...configBase, content: getContentDependencies(dirname) }; + return config; +}; + +module.exports = generateSharedTailwindConfig; diff --git a/package.json b/package.json index a3f072fa2..280e5ad07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "picsa-apps", - "version": "3.45.0", + "version": "3.46.0", "license": "See LICENSE", "scripts": { "ng": "nx", @@ -79,7 +79,7 @@ "@uppy/tus": "^3.4.0", "c3": "^0.7.20", "capacitor-blob-writer": "^1.1.16", - "capacitor-video-player": "^5.5.1", + "capacitor-video-player": "^6.0.0", "cordova-plugin-codeplay-share-own-apk": "0.0.7", "cordova-plugin-file": "^8.0.1", "cordova-plugin-file-opener2": "^4.0.0", @@ -162,7 +162,7 @@ "@typescript-eslint/eslint-plugin": "7.3.0", "@typescript-eslint/parser": "7.3.0", "@vendure/ngx-translate-extract": "^9.0.3", - "autoprefixer": "^10.4.0", + "autoprefixer": "^10.4.20", "cspell": "^6.31.1", "cypress": "^13.6.6", "dotenv": "^16.0.1", @@ -188,7 +188,7 @@ "ng-packagr": "17.3.0", "nx": "18.2.1", "openapi-typescript": "^6.7.3", - "postcss": "^8.4.5", + "postcss": "^8.4.47", "postcss-import": "~14.1.0", "postcss-url": "~10.1.3", "prettier": "2.6.2", @@ -197,7 +197,7 @@ "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-visualizer": "^5.8.3", "supabase": "^1.148.6", - "tailwindcss": "^3.0.2", + "tailwindcss": "^3.4.13", "ts-jest": "29.1.1", "ts-node": "10.9.2", "typescript": "5.4.3" diff --git a/yarn.lock b/yarn.lock index 18e35d4a4..519b5d18a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10482,7 +10482,25 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:^10.4.0, autoprefixer@npm:^10.4.9": +"autoprefixer@npm:^10.4.20": + version: 10.4.20 + resolution: "autoprefixer@npm:10.4.20" + dependencies: + browserslist: ^4.23.3 + caniuse-lite: ^1.0.30001646 + fraction.js: ^4.3.7 + normalize-range: ^0.1.2 + picocolors: ^1.0.1 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.1.0 + bin: + autoprefixer: bin/autoprefixer + checksum: 187cec2ec356631932b212f76dc64f4419c117fdb2fb9eeeb40867d38ba5ca5ba734e6ceefc9e3af4eec8258e60accdf5cbf2b7708798598fde35cdc3de562d6 + languageName: node + linkType: hard + +"autoprefixer@npm:^10.4.9": version: 10.4.16 resolution: "autoprefixer@npm:10.4.16" dependencies: @@ -11005,6 +11023,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.23.3": + version: 4.24.0 + resolution: "browserslist@npm:4.24.0" + dependencies: + caniuse-lite: ^1.0.30001663 + electron-to-chromium: ^1.5.28 + node-releases: ^2.0.18 + update-browserslist-db: ^1.1.0 + bin: + browserslist: cli.js + checksum: de200d3eb8d6ed819dad99719099a28fb6ebeb88016a5ac42fbdc11607e910c236a84ca1b0bbf232477d4b88ab64e8ab6aa67557cdd40a73ca9c2834f92ccce0 + languageName: node + linkType: hard + "bs-logger@npm:0.x, bs-logger@npm:^0.2.6": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -11215,6 +11247,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001663": + version: 1.0.30001663 + resolution: "caniuse-lite@npm:1.0.30001663" + checksum: 489a642feb6826a0fc7cfd7dbc35a3341cc1439eafdf0dae79338cf9033c5d9eddaedacbef7935acaddbb3c226a51097ed53d66dc6d8128cd6938c6763e1bbc4 + languageName: node + linkType: hard + "capacitor-blob-writer@npm:^1.1.16": version: 1.1.16 resolution: "capacitor-blob-writer@npm:1.1.16" @@ -11225,13 +11264,13 @@ __metadata: languageName: node linkType: hard -"capacitor-video-player@npm:^5.5.1": - version: 5.5.1 - resolution: "capacitor-video-player@npm:5.5.1" +"capacitor-video-player@npm:^6.0.0": + version: 6.0.0 + resolution: "capacitor-video-player@npm:6.0.0" peerDependencies: - "@capacitor/core": ^5.0.0 + "@capacitor/core": ^6.0.0 hls.js: ^1.4.0 - checksum: 63636a02d4927c4795e448d9913aa2f5ce86b856da4fe6b4f6c38322a31506ac951b1c5a595ddf6f2af2ebbb1ce693abaa4c205fa305358d4b021ba7adff69fe + checksum: 815fa67bd455f408c7258eecf357d7588730b4760987b8002aae320b7f873f844b63582e4f36041d794fdd39c260ee463d2e5743b62791712d17936c7259e8d3 languageName: node linkType: hard @@ -13301,6 +13340,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.28": + version: 1.5.29 + resolution: "electron-to-chromium@npm:1.5.29" + checksum: c1de62aaea88c9b3ba32f8f2703b9d77a81633099a8f61365eaf9855d36e72189dcd99b9c3b8b2804afa403ac2ce0b00c23affa6f19d17b04ce0076f66a546b6 + languageName: node + linkType: hard + "elementtree@npm:^0.1.7": version: 0.1.7 resolution: "elementtree@npm:0.1.7" @@ -13774,6 +13820,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.2": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 47b029c83de01b0d17ad99ed766347b974b0d628e848de404018f3abee728e987da0d2d370ad4574aa3d5b5bfc368754fd085d69a30f8e75903486ec4b5b709e + languageName: node + linkType: hard + "escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -16765,7 +16818,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.19.1, jiti@npm:^1.20.0": +"jiti@npm:^1.20.0": version: 1.21.0 resolution: "jiti@npm:1.21.0" bin: @@ -16774,6 +16827,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^1.21.0": + version: 1.21.6 + resolution: "jiti@npm:1.21.6" + bin: + jiti: bin/jiti.js + checksum: 9ea4a70a7bb950794824683ed1c632e2ede26949fbd348e2ba5ec8dc5efa54dc42022d85ae229cadaa60d4b95012e80ea07d625797199b688cc22ab0e8891d32 + languageName: node + linkType: hard + "jquery-touchswipe@npm:^1.6.19": version: 1.6.19 resolution: "jquery-touchswipe@npm:1.6.19" @@ -18565,6 +18627,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: ef55a3d853e1269a6d6279b7692cd6ff3e40bc74947945101138745bfdc9a5edabfe72cb19a31a8e45752e1910c4c65c77d931866af6357f242b172b7283f5b3 + languageName: node + linkType: hard + "nopt@npm:^7.0.0": version: 7.2.0 resolution: "nopt@npm:7.2.0" @@ -19480,6 +19549,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.0.1, picocolors@npm:^1.1.0": + version: 1.1.0 + resolution: "picocolors@npm:1.1.0" + checksum: a64d653d3a188119ff45781dfcdaeedd7625583f45280aea33fcb032c7a0d3959f2368f9b192ad5e8aade75b74dbd954ffe3106c158509a45e4c18ab379a2acd + languageName: node + linkType: hard + "picomatch@npm:4.0.1": version: 4.0.1 resolution: "picomatch@npm:4.0.1" @@ -19583,10 +19659,10 @@ __metadata: "@uppy/status-bar": ^3.2.5 "@uppy/tus": ^3.4.0 "@vendure/ngx-translate-extract": ^9.0.3 - autoprefixer: ^10.4.0 + autoprefixer: ^10.4.20 c3: ^0.7.20 capacitor-blob-writer: ^1.1.16 - capacitor-video-player: ^5.5.1 + capacitor-video-player: ^6.0.0 cordova-plugin-codeplay-share-own-apk: 0.0.7 cordova-plugin-file: ^8.0.1 cordova-plugin-file-opener2: ^4.0.0 @@ -19641,7 +19717,7 @@ __metadata: papaparse: ^5.3.2 parse: 3.4.2 perfect-freehand: ^1.2.2 - postcss: ^8.4.5 + postcss: ^8.4.47 postcss-import: ~14.1.0 postcss-url: ~10.1.3 prettier: 2.6.2 @@ -19655,7 +19731,7 @@ __metadata: sharp: ^0.31.3 stacktrace-js: ^2.0.2 supabase: ^1.148.6 - tailwindcss: ^3.0.2 + tailwindcss: ^3.4.13 ts-jest: 29.1.1 ts-node: 10.9.2 tslib: ^2.5.0 @@ -20275,7 +20351,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.2.14, postcss@npm:^8.4.14, postcss@npm:^8.4.21, postcss@npm:^8.4.23, postcss@npm:^8.4.24, postcss@npm:^8.4.31, postcss@npm:^8.4.5": +"postcss@npm:^8.2.14, postcss@npm:^8.4.14, postcss@npm:^8.4.21, postcss@npm:^8.4.23, postcss@npm:^8.4.24, postcss@npm:^8.4.31": version: 8.4.32 resolution: "postcss@npm:8.4.32" dependencies: @@ -20297,6 +20373,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.47": + version: 8.4.47 + resolution: "postcss@npm:8.4.47" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.1.0 + source-map-js: ^1.2.1 + checksum: f78440a9d8f97431dd2ab1ab8e1de64f12f3eff38a3d8d4a33919b96c381046a314658d2de213a5fa5eb296b656de76a3ec269fdea27f16d5ab465b916a0f52c + languageName: node + linkType: hard + "preact@npm:^10.5.13": version: 10.19.3 resolution: "preact@npm:10.19.3" @@ -21892,6 +21979,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b + languageName: node + linkType: hard + "source-map-loader@npm:5.0.0": version: 5.0.0 resolution: "source-map-loader@npm:5.0.0" @@ -22439,9 +22533,9 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:^3.0.2": - version: 3.3.6 - resolution: "tailwindcss@npm:3.3.6" +"tailwindcss@npm:^3.4.13": + version: 3.4.13 + resolution: "tailwindcss@npm:3.4.13" dependencies: "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 @@ -22451,7 +22545,7 @@ __metadata: fast-glob: ^3.3.0 glob-parent: ^6.0.2 is-glob: ^4.0.3 - jiti: ^1.19.1 + jiti: ^1.21.0 lilconfig: ^2.1.0 micromatch: ^4.0.5 normalize-path: ^3.0.0 @@ -22468,7 +22562,7 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 44632ac471248ecebcee1a2f15a0c3e9b8383513e71692b586aa2fe56dca12828ff70de3d340c898f27b27480e8475e5eb345fb2ebb813028bb2393578a34337 + checksum: 0e85717b4276b884c3ba762a72fececcf6c27f54fe4deb660e22b7f278f33e6f806af4e4c4fad27e8de74fa28872cea75fe121b5751ef861cb2cc423a2fc8fc4 languageName: node linkType: hard @@ -23265,6 +23359,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.0": + version: 1.1.0 + resolution: "update-browserslist-db@npm:1.1.0" + dependencies: + escalade: ^3.1.2 + picocolors: ^1.0.1 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 7b74694d96f0c360f01b702e72353dc5a49df4fe6663d3ee4e5c628f061576cddf56af35a3a886238c01dd3d8f231b7a86a8ceaa31e7a9220ae31c1c1238e562 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1"