This commit is contained in:
2023-08-14 13:49:06 -04:00
commit aa0f98e742
84 changed files with 19695 additions and 0 deletions

18
client/.browserslistrc Normal file
View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
client/.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
client/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

112
client/angular.json Normal file
View File

@ -0,0 +1,112 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"GrowBot": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/GrowBot",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.json",
"aot": true,
"assets": [
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "GrowBot:build"
},
"configurations": {
"production": {
"browserTarget": "GrowBot:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "GrowBot:build"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "GrowBot:serve"
},
"configurations": {
"production": {
"devServerTarget": "GrowBot:serve:production"
}
}
}
}
}
},
"defaultProject": "GrowBot",
"cli": {
"analytics": false
}
}

13574
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
client/package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "grow-bot",
"version": "0.3.10",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.0.5",
"@angular/cdk": "^10.1.0",
"@angular/common": "~10.0.5",
"@angular/compiler": "~10.0.5",
"@angular/core": "~10.0.5",
"@angular/forms": "~10.0.5",
"@angular/material": "^10.1.0",
"@angular/platform-browser": "~10.0.5",
"@angular/platform-browser-dynamic": "~10.0.5",
"@angular/router": "~10.0.5",
"bootstrap-scss": "^4.5.0",
"chart.js": "^2.9.3",
"chartjs-plugin-annotation": "^0.5.7",
"ng2-charts": "^2.3.3",
"rxjs": "~6.5.5",
"socket.io-client": "^2.3.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.4",
"@angular/cli": "~10.0.4",
"@angular/compiler-cli": "~10.0.5",
"@types/node": "^12.11.1",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~3.3.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~3.9.5"
}
}

View File

@ -0,0 +1,50 @@
import {
trigger,
animate,
transition,
style,
query, group
} from '@angular/animations';
export const collapseUp = trigger('collapseUp', [
transition('* => void', [
style({opacity: 1, transform: 'translateY(0%)'}),
animate('0.5s', style({opacity: 0, transform: 'translateY(-100%)'}))
])
]);
export const expandDown = trigger('expandDown', [
transition('void => *', [
style({opacity: 0, transform: 'translateY(-100%)'}),
animate('0.5s', style({opacity: 1, transform: 'translateY(0%)'}))
])
]);
export const fadeIn = trigger('fadeIn', [
transition('void => *', [
style({opacity: 0}),
animate('0.5s ease-in-out', style({opacity: 1}))
])
]);
export const fadeOut = trigger('fadeOut', [
transition('* => void', [
style({opacity: 1}),
animate('0.5s ease-in-out', style({opacity: 0}))
])
]);
export const routerTransition = trigger('routerTransition', [
transition('* <=> *', [
query(':enter, :leave', style({position: 'fixed', width: '100%'}), {optional: true}),
group([
query(':enter', [
style({transform: 'translateX(100%)'}),
animate('0.5s ease-in-out', style({transform: 'translateX(0%)'}))
], {optional: true}), query(':leave', [
style({transform: 'translateX(0%)'}),
animate('0.5s ease-in-out', style({transform: 'translateX(-100%)'}))
], {optional: true}),
])
])
]);

View File

@ -0,0 +1,27 @@
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {CameraComponent} from "./views/camera/camera.component";
import {ClimateComponent} from "./views/climate/climate.component";
import {DashboardComponent} from "./views/dashboard/dashboard.component";
import {WaterComponent} from "./views/water/water.component";
import {GrowOpsComponent} from "./views/growOps/growOps.component";
import {ScheduleComponent} from "./views/schedule/schedule.component";
import {NotesComponent} from "./views/notes/notes.component";
import {SettingsComponent} from "./views/settings/settings.component";
const routes: Routes = [
{path: 'camera', component: CameraComponent},
{path: 'climate', component: ClimateComponent},
{path: 'growops', component: GrowOpsComponent},
{path: 'notes', component: NotesComponent},
{path: 'schedule', component: ScheduleComponent},
{path: 'settings', component: SettingsComponent},
{path: 'water', component: WaterComponent},
{path: '**', component: DashboardComponent, data: {noAnimation: true}},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,67 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppRoutingModule} from './app-routing.module';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MaterialModule} from "./material.module";
import {LogoComponent} from "./components/logo/logo.component";
import {MenuComponent} from "./components/menu/menu.component";
import {StreamComponent} from "./components/stream/stream.component";
import {CameraComponent} from "./views/camera/camera.component";
import {FullscreenCameraComponent} from "./views/fullscreenCamera/fullscreenCamera.component";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatNativeDateModule} from "@angular/material/core";
import {HttpClientModule} from "@angular/common/http";
import {ClimateComponent} from "./views/climate/climate.component";
import {ChartsModule} from "ng2-charts";
import {ClimateGraphComponent} from "./components/climateGraph/climateGraph.component";
import {DashboardComponent} from "./views/dashboard/dashboard.component";
import {AppComponent} from "./views/app.component";
import {TemperatureComponent} from "./components/temperature/temperature.component";
import {FanAutoComponent} from "./components/fanAuto/fanAuto.component";
import {LightAutoComponent} from "./components/lightAuto/lightAuto.component";
import {FanToggleComponent} from "./components/fanToggle/fanToggle.component";
import {LightToggleComponent} from "./components/lightToggle/lightToggle.component";
import {WaterComponent} from "./views/water/water.component";
import {GrowOpsComponent} from "./views/growOps/growOps.component";
import {ScheduleComponent} from "./views/schedule/schedule.component";
import {NotesComponent} from "./views/notes/notes.component";
import {SettingsComponent} from "./views/settings/settings.component";
@NgModule({
declarations: [
AppComponent,
CameraComponent,
ClimateComponent,
ClimateGraphComponent,
DashboardComponent,
FanAutoComponent,
FanToggleComponent,
FullscreenCameraComponent,
GrowOpsComponent,
LightAutoComponent,
LightToggleComponent,
LogoComponent,
MenuComponent,
NotesComponent,
ScheduleComponent,
SettingsComponent,
StreamComponent,
TemperatureComponent,
WaterComponent
],
imports: [
AppRoutingModule,
BrowserModule,
BrowserAnimationsModule,
ChartsModule,
FormsModule,
HttpClientModule,
MaterialModule,
MatNativeDateModule,
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

5
client/src/app/bootstrap.scss vendored Normal file
View File

@ -0,0 +1,5 @@
@import 'node_modules/bootstrap-scss/bootstrap';
.text-primary {
color: #689f38 !important
}

View File

@ -0,0 +1,31 @@
<div class="w-100 h-100">
<div>
<div style="width: 250px">
<mat-form-field appearance="fill" color="accent">
<mat-label>Time Frame</mat-label>
<mat-select [(ngModel)]="timeFrame">
<mat-option value="12">12 hours</mat-option>
<mat-option value="24">24 hours</mat-option>
<mat-option value="72">3 Days</mat-option>
<mat-option value="168">7 Days</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-checkbox class="mr-5 red" [(ngModel)]="toggleTemp" (change)="refresh()">Temperature</mat-checkbox>
<mat-checkbox class="mr-5 green" [(ngModel)]="toggleHumidity" (change)="refresh()">Humidity</mat-checkbox>
<mat-checkbox class="mr-5 yellow" [(ngModel)]="toggleLight" (change)="refresh()">Lights</mat-checkbox>
<mat-checkbox class="mr-5 blue" [(ngModel)]="toggleFans" (change)="refresh()">Fans</mat-checkbox>
</div>
</div>
<mat-divider></mat-divider>
<canvas baseChart class="mt-4"
[datasets]="data | async"
[labels]="labels | async"
[colors]="colors"
[legend]="false"
chartType="line"
[plugins]="plugins"
[options]="options | async">
</canvas>
</div>

View File

@ -0,0 +1,79 @@
.red {
::ng-deep.mat-checkbox-label {
color: rgb(150, 80, 80) !important;
}
::ng-deep.mat-checkbox-frame {
border-color: rgb(150,80,80);
}
&.mat-checkbox-checked {
::ng-deep.mat-checkbox-background {
background-color: rgb(150,80,80) !important;
}
}
::ng-deep.mat-ripple-element {
background: rgb(150,80,80) !important;
}
}
.green {
::ng-deep.mat-checkbox-label {
color: rgb(80,150,80) !important;
}
::ng-deep.mat-checkbox-frame {
border-color: rgb(80,150,80);
}
&.mat-checkbox-checked {
::ng-deep.mat-checkbox-background {
background-color: rgb(80,150,80) !important;
}
}
::ng-deep.mat-ripple-element {
background: rgb(80,150,80) !important;
}
}
.blue {
::ng-deep.mat-checkbox-label {
color: rgb(80,80,230) !important;
}
::ng-deep.mat-checkbox-frame {
border-color: rgb(80,80,230);
}
&.mat-checkbox-checked {
::ng-deep.mat-checkbox-background {
background-color: rgb(80,80,230) !important;
}
}
::ng-deep.mat-ripple-element {
background: rgb(80,80,230) !important;
}
}
.yellow {
::ng-deep.mat-checkbox-label {
color: rgb(150,150,80) !important;
}
::ng-deep.mat-checkbox-frame {
border-color: rgb(150,150,80);
}
&.mat-checkbox-checked {
::ng-deep.mat-checkbox-background {
background-color: rgb(150,150,80) !important;
}
}
::ng-deep.mat-ripple-element {
background: rgb(150,150,80) !important;
}
}

View File

@ -0,0 +1,135 @@
import {Component} from "@angular/core";
import {ChartDataSets} from "chart.js";
import * as pluginAnnotations from 'chartjs-plugin-annotation';
import {BehaviorSubject} from "rxjs";
@Component({
selector: 'climate-graph',
templateUrl: './climateGraph.component.html',
styleUrls: ['./climateGraph.component.scss']
})
export class ClimateGraphComponent {
timeFrame = '12';
xLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
tempData: number[] = [28, 48, 40, 19, 86, 27, 90];
toggleTemp = true;
humidityData: number[] = [38, 28, 50, 9, 76, 17, 80];
toggleHumidity = true;
lightData: number[][] = [[1, 3], [4, 6]];
toggleLight = false;
fanData: number[][] = [[2, 5]];
toggleFans = false;
colors = [{
backgroundColor: 'rgba(150,0,0,0.5)',
borderColor: 'rgba(150,0,0,0.5)',
pointBackgroundColor: 'rgba(0,0,0,0)',
pointBorderColor: 'rgba(0,0,0,0)',
pointHoverBackgroundColor: 'rgba(0,0,0,0)',
pointHoverBorderColor: 'rgba(0,0,0,0)',
}, {
backgroundColor: 'rgba(0,150,0,0.5)',
borderColor: 'rgba(0,150,0,0.5)',
pointBackgroundColor: 'rgba(0,0,0,0)',
pointBorderColor: 'rgba(0,0,0,0)',
pointHoverBackgroundColor: 'rgba(0,0,0,0)',
pointHoverBorderColor: 'rgba(0,0,0,0)',
}];
data = new BehaviorSubject(null);
labels = new BehaviorSubject(null);
options = new BehaviorSubject(null);
plugins = [pluginAnnotations];
constructor() {
this.refresh();
}
refresh() {
this.data.next([
{data: this.tempData, label: 'Temperature', fill: false, showLine: this.toggleTemp},
{data: this.humidityData, label: 'Humidity', fill: false, showLine: this.toggleHumidity}
]);
this.labels.next(this.xLabels);
let options = {
responsive: true,
scales: {
xAxes: [{}],
yAxes: [
{
id: 'y-axis-0',
position: 'left',
gridLines: {
color: 'rgba(0,0,0,0)',
},
ticks: {
callback: (val) => `${val} °C`,
beginAtZero: true,
maxTicksLimit: 10,
precision: 0,
stepSize: 5
}
},
{
id: 'y-axis-1',
position: 'right',
gridLines: {
color: 'rgba(0,0,0,0)',
},
ticks: {
callback: (val) => `${Math.round(val * 100)} %`,
beginAtZero: true,
maxTicksLimit: 10,
precision: 0,
stepSize: 0.1,
suggestedMax: 1,
suggestedMin: 0
}
}
]
},
annotation: {
annotations: [],
},
};
if(this.toggleLight) {
this.lightData.forEach((dataPoint: number[]) => {
options.annotation.annotations.push({
type: 'box',
drawTime: 'beforeDatasetsDraw',
xScaleID: 'x-axis-0',
yScaleID: 'y-axis-0',
xMin: dataPoint[0],
xMax: dataPoint[1],
yMin: 0,
yMax: 100,
borderWidth: 3,
borderColor: 'orange',
backgroundColor: 'rgba(255,255,0,0.1)',
});
});
}
if(this.toggleFans) {
this.fanData.forEach((dataPoint: number[]) => {
options.annotation.annotations.push({
type: 'box',
drawTime: 'beforeDatasetsDraw',
xScaleID: 'x-axis-0',
yScaleID: 'y-axis-0',
xMin: dataPoint[0],
xMax: dataPoint[1],
yMin: 0,
yMax: 100,
borderWidth: 3,
borderColor: 'blue',
backgroundColor: 'rgba(0,0,255,0.1)',
});
});
}
this.options.next(options);
}
}

View File

@ -0,0 +1,41 @@
<div class="row">
<div class="col-12 col-xl-6">
<mat-slide-toggle color="accent" [(ngModel)]="climateService.fanConfig.autoFan" (change)="save()">Automatic</mat-slide-toggle>
</div>
<div class="col-12 col-xl-6 mt-4 mt-xl-0">
<mat-radio-group [disabled]="!climateService.fanConfig.autoFan" [(ngModel)]="climateService.fanConfig.fanMode" (change)="save()">
<mat-radio-button value="time" checked>Time</mat-radio-button>
<mat-radio-button class="ml-3" value="temp">Temp</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div *ngIf="climateService.fanConfig.fanMode == 'time'" class="mt-4 row">
<div class="col">
<mat-form-field class="w-100" appearance="fill" color="accent">
<mat-label>From</mat-label>
<input matInput type="time" [(ngModel)]="climateService.fanConfig.fanOn" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
</mat-form-field>
</div>
<div class="col">
<mat-form-field class="w-100" appearance="fill" color="accent">
<mat-label>To</mat-label>
<input matInput type="time" [(ngModel)]="climateService.fanConfig.fanOff" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
</mat-form-field>
</div>
</div>
<div *ngIf="climateService.fanConfig.fanMode == 'temp'" class="mt-4 row">
<div class="col">
<mat-form-field class="w-100" appearance="fill" color="accent">
<mat-label>Temperature</mat-label>
<input matInput type="number" [(ngModel)]="climateService.fanConfig.fanTemp" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
<span matSuffix>°C</span>
</mat-form-field>
</div>
<div class="col">
<mat-form-field class="w-100" appearance="fill" color="accent">
<mat-label>Humidity</mat-label>
<input matInput type="number" [(ngModel)]="climateService.fanConfig.fanHumidity" (change)="save()" [disabled]="!climateService.fanConfig.autoFan">
<span matSuffix>%</span>
</mat-form-field>
</div>
</div>

View File

@ -0,0 +1,23 @@
import {Component} from "@angular/core";
import {ClimateService} from "../../services/climate.service";
@Component({
selector: 'fan-auto',
templateUrl: './fanAuto.component.html'
})
export class FanAutoComponent {
private readonly TIMEOUT = 2500;
private debuf;
constructor(public climateService: ClimateService) { }
save() {
if(this.debuf == null) {
this.debuf = setTimeout(() => {
let ignore = this.climateService.saveFan();
this.debuf = null;
}, this.TIMEOUT)
}
}
}

View File

@ -0,0 +1,3 @@
<button mat-mini-fab [color]="climateService.fanConfig.on ? 'primary' : ''" (click)="climateService.toggleFan()">
<mat-icon>power_settings_new</mat-icon>
</button>

View File

@ -0,0 +1,10 @@
import {Component} from "@angular/core";
import {ClimateService} from "../../services/climate.service";
@Component({
selector: 'fan-toggle',
templateUrl: './fanToggle.component.html'
})
export class FanToggleComponent {
constructor(public climateService: ClimateService) { }
}

View File

@ -0,0 +1,15 @@
<mat-slide-toggle color="accent" [(ngModel)]="climateService.lightConfig.autoLight" (change)="save()">Automatic</mat-slide-toggle>
<div class="mt-4 row">
<div class="col">
<mat-form-field class="w-100" appearance="fill" color="accent">
<mat-label>From</mat-label>
<input matInput type="time" [(ngModel)]="climateService.lightConfig.lightOn" (change)="save()" [disabled]="!climateService.lightConfig.autoLight">
</mat-form-field>
</div>
<div class="col">
<mat-form-field class="w-100" appearance="fill" color="accent">
<mat-label>To</mat-label>
<input matInput type="time" [(ngModel)]="climateService.lightConfig.lightOff" (change)="save()" [disabled]="!climateService.lightConfig.autoLight">
</mat-form-field>
</div>
</div>

View File

@ -0,0 +1,23 @@
import {Component} from "@angular/core";
import {ClimateService} from "../../services/climate.service";
@Component({
selector: 'light-auto',
templateUrl: './lightAuto.component.html'
})
export class LightAutoComponent {
private readonly TIMEOUT = 2500;
private debuf;
constructor(public climateService: ClimateService) { }
save() {
if(this.debuf == null) {
this.debuf = setTimeout(() => {
let ignore = this.climateService.saveLight();
this.debuf = null;
}, this.TIMEOUT)
}
}
}

View File

@ -0,0 +1,3 @@
<button mat-mini-fab [color]="climateService.lightConfig.on ? 'primary' : ''" (click)="climateService.toggleLight()">
<mat-icon>power_settings_new</mat-icon>
</button>

View File

@ -0,0 +1,10 @@
import {Component} from "@angular/core";
import {ClimateService} from "../../services/climate.service";
@Component({
selector: 'light-toggle',
templateUrl: './lightToggle.component.html'
})
export class LightToggleComponent {
constructor(public climateService: ClimateService) { }
}

View File

@ -0,0 +1 @@
<img [ngClass]="{'glow': glow}" src="/assets/images/logo.png" alt="GrowBot">

View File

@ -0,0 +1,14 @@
.glow {
animation: glow;
animation-delay: 1s;
animation-duration: 2s;
animation-direction: alternate;
animation-timing-function: ease-in;
animation-iteration-count: infinite;
filter: drop-shadow(0px 0px 0px green);
}
@keyframes glow {
from {filter: drop-shadow(0px 0px 0px #00ff00);}
to {filter: drop-shadow(0px 0px 5px #00ff00);}
}

View File

@ -0,0 +1,12 @@
import {Component, Input} from "@angular/core";
@Component({
selector: 'logo',
templateUrl: `./logo.component.html`,
styleUrls: ['./logo.component.scss']
})
export class LogoComponent {
@Input() glow = true;
@Input() height = 500;
@Input() width = 500;
}

View File

@ -0,0 +1,23 @@
<mat-nav-list class="p-0">
<ng-container *ngFor="let item of items; let i = $index">
<mat-divider *ngIf="i > 0"></mat-divider>
<a mat-list-item [routerLink]="item.link" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
<span class="p-3 pl-0 mr-5">
<mat-icon *ngIf="item.icon" class="mr-4" style="vertical-align: sub">{{item.icon}}</mat-icon>
<span>{{item.text}}</span>
</span>
</a>
<ng-container *ngIf="item.sub?.length">
<ng-container *ngFor="let subItem of item.sub">
<mat-divider></mat-divider>
<a mat-list-item class="pl-4" [routerLink]="subItem.link" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
<span class="p-3 pl-0 mr-5">
<mat-icon *ngIf="subItem.icon" class="mr-4" style="vertical-align: sub">{{subItem.icon}}</mat-icon>
<span>{{subItem.text}}</span>
</span>
</a>
</ng-container>
</ng-container>
<mat-divider></mat-divider>
</ng-container>
</mat-nav-list>

View File

@ -0,0 +1,4 @@
.active {
background-color: #689f38;
color: #ffffff;
}

View File

@ -0,0 +1,11 @@
import {Component, Input} from "@angular/core";
import {MenuItem} from "../../models/menuItem";
@Component({
selector: 'menu',
templateUrl: `./menu.component.html`,
styleUrls: ['./menu.component.scss']
})
export class MenuComponent {
@Input() items: MenuItem[] = [];
}

View File

@ -0,0 +1,17 @@
<div class="stream h-100 w-100 position-relative">
<div class="top-controls text-white">
<button mat-icon-button matTooltip="Take Picture" (click)="cameraService.snap()"><mat-icon style=" text-shadow: 0 0 5px #000;">camera_alt</mat-icon></button>
<button mat-icon-button *ngIf="fullscreen" matTooltip="Close" [mat-dialog-close]><mat-icon style=" text-shadow: 0 0 5px #000;">fullscreen_exit</mat-icon></button>
<button mat-icon-button *ngIf="!fullscreen" matTooltip="Fullscreen" (click)="openFullscreen()"><mat-icon style=" text-shadow: 0 0 5px #000;">fullscreen</mat-icon></button>
</div>
<mat-progress-spinner *ngIf="loading" class="center" mode="indeterminate"></mat-progress-spinner>
<img id="stream" [src]="stream | async" width="100%" height="100%" alt="stream" [hidden]="loading">
<div class="bottom-controls text-white">
<button mat-icon-button *ngIf="pause" (click)="pause = !pause" matTooltip="Resume">
<mat-icon style="font-size: 2rem; text-shadow: 0 0 5px #000;">play_circle_outline</mat-icon>
</button>
<button mat-icon-button *ngIf="!pause" (click)="pause = !pause" matTooltip="Pause">
<mat-icon style="font-size: 2rem; text-shadow: 0 0 5px #000;">pause_circle_outline</mat-icon>
</button>
</div>
</div>

View File

@ -0,0 +1,33 @@
.stream {
min-height: 250px;
min-width: 250px;
.center {
position: absolute;
left: calc(50% - 50px);
top: calc(50% - 50px);
}
.top-controls {
position: absolute;
top: 1.5vh;
right: 1.5vh;
}
.bottom-controls {
position: absolute;
bottom: 1.5vh;
left: 50%;
transform: translateX(-50%);
}
::ng-deep.fill-it {
border-radius: 0;
max-width: 100% !important;
mat-dialog-container {
border-radius: 0;
padding: 0 !important;
}
}
}

View File

@ -0,0 +1,46 @@
import {Component, Input, OnDestroy, OnInit} from "@angular/core";
import {CameraService} from "../../services/camera.service";
import {filter, map, tap} from "rxjs/operators";
import {Observable} from "rxjs";
import {MatDialog} from "@angular/material/dialog";
import {FullscreenCameraComponent} from "../../views/fullscreenCamera/fullscreenCamera.component";
@Component({
selector: 'stream',
templateUrl: './stream.component.html',
styleUrls: ['./stream.component.scss']
})
export class StreamComponent implements OnDestroy, OnInit{
@Input() fullscreen = false;
@Input() pause: boolean = false;
loading = true;
stream: Observable<any>;
constructor(public cameraService: CameraService, private matDialog: MatDialog) {
this.stream = this.cameraService.stream.pipe(
filter(() => !this.pause),
tap(() => this.loading = false),
map(data => `data:image/jpeg;base64,${data}`)
);
}
openFullscreen() {
let orgStatus = this.pause
this.pause = true;
this.matDialog.open(FullscreenCameraComponent, {
width: '100%',
height: '100%',
panelClass: 'fill-it',
autoFocus: false
}).afterClosed().subscribe(() => this.pause = orgStatus);
}
ngOnInit() {
this.cameraService.start();
}
ngOnDestroy() {
this.cameraService.stop();
}
}

View File

@ -0,0 +1,31 @@
<div>
<div class="d-block d-md-none">
<div class="row">
<h1 class="ml-3 mb-1">Temperature: {{climateService.temp}} °C</h1>
</div>
<mat-divider></mat-divider>
<div class="row">
<h1 class="ml-3 mb-0">
Humidity: {{climateService.humidity | percent}}
</h1>
</div>
</div>
<div class="d-none d-md-block">
<div class="row">
<div class="col border-right pr-2">
<h1 class="mb-0">
Temperature
<br>
{{climateService.temp}} °C
</h1>
</div>
<div class="col border-left pl-3">
<h1 class="mb-0">
Humidity
<br>
{{climateService.humidity | percent}}
</h1>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
import {Component} from "@angular/core";
import {ClimateService} from "../../services/climate.service";
@Component({
selector: 'temperature',
templateUrl: './temperature.component.html'
})
export class TemperatureComponent {
constructor(public climateService: ClimateService) { }
}

View File

@ -0,0 +1,43 @@
import {NgModule} from '@angular/core';
import {MatIconModule} from "@angular/material/icon";
import {MatToolbarModule} from "@angular/material/toolbar";
import {MatDividerModule} from "@angular/material/divider";
import {MatSidenavModule} from "@angular/material/sidenav";
import {MatListModule} from "@angular/material/list";
import {MatButtonModule} from "@angular/material/button";
import {MatCardModule} from "@angular/material/card";
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatDialogModule} from "@angular/material/dialog";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatInputModule} from "@angular/material/input";
import {MatSelectModule} from "@angular/material/select";
import {MatRadioModule} from "@angular/material/radio";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
const modules = [
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatListModule,
MatRadioModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSidenavModule,
MatSlideToggleModule,
MatToolbarModule,
MatTooltipModule,
];
@NgModule({
imports: modules,
exports: modules
})
export class MaterialModule {
}

View File

@ -0,0 +1,99 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons&display=swap);
@import 'node_modules/@angular/material/theming';
@include mat-core();
$GrowBot-primary: mat-palette($mat-light-green, 700);
$GrowBot-accent: mat-palette($mat-light-blue, 700);
$GrowBot-warn: mat-palette($mat-red);
$GrowBot-light-theme: mat-light-theme((
color: (
primary: $GrowBot-primary,
accent: $GrowBot-accent,
warn: $GrowBot-warn,
)
));
$GrowBot-dark-theme: mat-dark-theme((
color: (
primary: $GrowBot-primary,
accent: $GrowBot-accent,
warn: $GrowBot-warn,
)
));
.light-theme {
@include angular-material-theme($GrowBot-light-theme);
.mat-toolbar-row, .mat-toolbar-single-row {
height: 48px;
}
.mat-drawer-container {
background-color: rgba(255, 255, 255, 0) !important;
}
.mat-toolbar-row, .mat-toolbar-single-row {
height: 48px;
}
.p-0 {
mat-dialog-container {
padding: 0;
border-radius: 0;
}
}
.mat-list-item {
&.active {
color: #FFFFFF !important;
}
&:hover {
background: #94ba73 !important;
color: #ffffff;
}
&:focus {
background: #689f38 !important;
color: #ffffff;
}
}
}
.dark-theme {
@include angular-material-theme($GrowBot-dark-theme);
.mat-toolbar-row, .mat-toolbar-single-row {
height: 48px;
}
.mat-drawer-container {
background-color: rgba(255, 255, 255, 0) !important;
}
.mat-toolbar-row, .mat-toolbar-single-row {
height: 48px;
}
.p-0 {
mat-dialog-container {
padding: 0;
border-radius: 0;
}
}
.mat-list-item {
&:hover {
background: #94ba73 !important;
color: #ffffff;
}
&:focus {
background: #689f38 !important;
color: #ffffff;
}
}
}

View File

@ -0,0 +1,7 @@
export interface MenuItem {
class?: string;
icon?: string;
link?: string;
text: string;
sub?: MenuItem[];
}

View File

@ -0,0 +1,71 @@
import {Injectable} from "@angular/core";
import SocketIO from 'socket.io-client';
import {environment} from "../../environments/environment";
import {Subject} from "rxjs";
import {HttpClient} from "@angular/common/http";
@Injectable({
providedIn: 'root'
})
export class CameraService {
private socket;
images: string[] = [];
stream: Subject<string>;
config = {
timelapseEnabled: false,
timelapseFrequency: '0 0 12 * * *'
};
constructor(private http: HttpClient) {
this.socket = SocketIO(environment.api)
this.stream = new Subject();
}
async del(filename) {
let resp = <any>(await this.http.delete(`${environment.api}/timelapse/${filename}`).toPromise());
this.images = resp.files;
this.config = {
timelapseEnabled: resp.timelapseEnabled,
timelapseFrequency: resp.timelapseFrequency
};
}
async list() {
let resp = <any>(await this.http.get(`${environment.api}/timelapse`).toPromise());
this.images = resp.files;
this.config = {
timelapseEnabled: resp.timelapseEnabled,
timelapseFrequency: resp.timelapseFrequency
};
}
async save(config) {
let resp = <any>(await this.http.put(`${environment.api}/timelapse`, config).toPromise());
this.images = resp.files;
this.config = {
timelapseEnabled: resp.timelapseEnabled,
timelapseFrequency: resp.timelapseFrequency
};
}
async snap() {
let resp = <any>(await this.http.post(`${environment.api}/timelapse`, {}).toPromise());
this.images = resp.files;
this.config = {
timelapseEnabled: resp.timelapseEnabled,
timelapseFrequency: resp.timelapseFrequency
};
}
start() {
this.socket.on('stream', data => {
this.stream.next(data);
});
return this.stream;
}
stop() {
this.socket.off('stream');
}
}

View File

@ -0,0 +1,59 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {timer} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class ClimateService {
private readonly interval = 5 // Seconds
private first = true;
fanConfig = {
on: false,
autoFan: false,
fanMode: 'time',
fanOn: null,
fanOff: null,
fanTemp: null,
fanHumidity: null,
}
lightConfig = {
on: false,
autoLight: false,
lightOn: null,
lightOff: null
}
temp: number = 0;
humidity: number = 0;
constructor(private http: HttpClient) {
timer(0, this.interval * 1000).subscribe(async () => {
if(this.first) {
this.fanConfig = <any>(await this.http.get(`${environment.api}/fan/`).toPromise());
this.lightConfig = <any>(await this.http.get(`${environment.api}/light/`).toPromise());
this.first = false;
} else {
this.fanConfig.on = <any>(await this.http.get(`${environment.api}/fan/`).toPromise())['on'];
this.lightConfig.on = <any>(await this.http.get(`${environment.api}/light/`).toPromise())['on'];
}
});
}
async toggleFan() {
return this.fanConfig.on = <any>(await this.http.post(`${environment.api}/fan/`, {}).toPromise())['on'];
}
async toggleLight() {
return this.lightConfig.on = <any>(await this.http.post(`${environment.api}/light/`, {}).toPromise())['on'];
}
async saveFan() {
this.fanConfig = <any>(await this.http.put(`${environment.api}/fan/`, this.fanConfig).toPromise());
}
async saveLight() {
this.lightConfig = <any>(await this.http.put(`${environment.api}/light/`, this.lightConfig).toPromise());
}
}

View File

@ -0,0 +1,30 @@
export interface WebStorageOptions {
fieldName?: string;
encryptionKey?: string;
defaultValue?: any;
}
export function LocalStorage(opts: WebStorageOptions = {}) {
return storage(localStorage, opts);
}
export function SessionStorage(opts: WebStorageOptions = {}) {
return storage(sessionStorage, opts);
}
function storage(storageType: Storage, opts: WebStorageOptions) {
return function(target: object, key: string) {
if(!opts.fieldName) opts.fieldName = key;
Object.defineProperty(target, key, {
get: function() {
let value = storageType.getItem(<string>opts.fieldName);
if(!value && opts.defaultValue != null) return opts.defaultValue;
return JSON.parse(<string>value);
},
set: function(value) {
storageType.setItem(<string>opts.fieldName, JSON.stringify(value));
}
});
};
}

View File

@ -0,0 +1,29 @@
<div [ngClass]="{'light-theme': !darkMode, 'dark-theme': darkMode}">
<mat-toolbar *ngIf="!hide" [@collapseUp]="!hide" [@expandDown]="!hide">
<button mat-icon-button *ngIf="mobile" class="mr-2">
<mat-icon (click)="open = !open">menu</mat-icon>
</button>
<img src="assets/images/logo.png" class="mr-2" height="24px" width="auto">
<span>
<span style="font-weight: 500;">GrowBot</span>
<small class="text-primary ml-2">v{{version}}</small>
</span>
<span class="mx-auto"></span>
<span>
<mat-slide-toggle [(ngModel)]="darkMode"><mat-icon>invert_colors</mat-icon></mat-slide-toggle>
</span>
</mat-toolbar>
<mat-drawer-container class="fill-height" [hasBackdrop]="false">
<mat-drawer [mode]="mobile ? 'push' : 'side'" [opened]="open" [disableClose]="!mobile" [autoFocus]="false">
<menu class="m-0 p-0" [items]="menuItems"></menu>
</mat-drawer>
<mat-drawer-content>
<main class="h-100" [@routerTransition]="transition(o)">
<router-outlet #o="outlet"></router-outlet>
</main>
</mat-drawer-content>
</mat-drawer-container>
<div class="background">
<logo style="position: fixed; left: 50vw; top: 50vh; transform: translate(-50%, -50%);"></logo>
</div>
</div>

View File

@ -0,0 +1,12 @@
.background {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: -1;
}
.fill-height {
height: calc(100vh - 48px);
}

View File

@ -0,0 +1,61 @@
import {Component, HostBinding} from '@angular/core';
import {version} from '../../../package.json';
import {ActivatedRoute, NavigationEnd, Router} from "@angular/router";
import {BreakpointObserver, Breakpoints} from "@angular/cdk/layout";
import {filter} from "rxjs/operators";
import {collapseUp, expandDown, routerTransition} from "../animations";
import {MenuItem} from "../models/menuItem";
import {LocalStorage} from "../utils/webStorage";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [collapseUp, expandDown, routerTransition]
})
export class AppComponent {
// Theme
private body = document.getElementsByTagName('body')[0];
@LocalStorage({defaultValue: false}) private _darkMode: boolean;
get darkMode(): boolean { return this._darkMode; }
set darkMode(val: boolean) {
this._darkMode = val;
this.body.className = val ? 'dark-theme' : 'light-theme';
}
hide = false; // Hide nav
mobile = true; // Mobile or desktop size
noTransition = false; // Stop router transitions
open = false; // Side nav open
version = version; // Version number from package.json
menuItems: MenuItem[] = [
{text: 'Dashboard', icon: 'dashboard', link: '/'},
{text: 'Climate', icon: 'eco', link: '/climate'},
{text: 'Water', icon: 'waves', link: '/water'},
{text: 'Camera', icon: 'videocam', link: '/camera'},
{text: 'GrowOps', icon: 'spa', link: '/growops', sub: [
{text: 'Schedule', icon: 'event', link: '/schedule'},
{text: 'Notes', icon: 'notes', link: '/notes'},
]},
{text: 'Settings', icon: 'settings', link: '/settings'},
]
constructor(private router: Router, route: ActivatedRoute, breakpointObserver: BreakpointObserver) {
this.darkMode = this._darkMode;
router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
this.hide = route.root.firstChild != null ? !!route.root.firstChild.snapshot.data.hide : false;
this.open = !this.hide && !this.mobile;
});
breakpointObserver.observe([Breakpoints.Handset, Breakpoints.Tablet]).subscribe(result => {
this.mobile = result.matches;
this.open = !this.mobile;
})
}
transition(outlet) {
if(!outlet.isActivated || !!outlet.activatedRouteData.noAnimation || this.noTransition) return '';
return outlet.activatedRoute;
}
}

View File

@ -0,0 +1,57 @@
<div class="container-fluid">
<div class="row pt-3">
<div class="col-12 col-lg-8">
<mat-card style="padding: 3px">
<mat-card-content>
<stream></stream>
</mat-card-content>
</mat-card>
</div>
<div class="col-12 col-lg-4 mt-3 mt-lg-0">
<mat-card>
<mat-card-content>
<div>
<h2>Timelapse</h2>
<mat-divider></mat-divider>
<div>
<mat-slide-toggle class="mt-3" [(ngModel)]="cameraService.config.timelapseEnabled" color="accent" (change)="save()">Enable</mat-slide-toggle>
<mat-form-field class="w-100 mt-3" appearance="fill">
<mat-label>Photo Frequency</mat-label>
<mat-select [(ngModel)]="cameraService.config.timelapseFrequency" [disabled]="!cameraService.config.timelapseEnabled" (selectionChange)="save()">
<mat-option value="0 * * * * *">Minute</mat-option>
<mat-option value="0 0 * * * *">Hourly</mat-option>
<mat-option value="0 0 12 * * *">Daily</mat-option>
<mat-option value="0 0 12 */2 * *">Alternating Days</mat-option>
<mat-option value="0 0 12 * * 0">Weekly</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="mt-3">
<h2>Images</h2>
<mat-divider></mat-divider>
<div>
<div *ngIf="cameraService.images.length == 0" class="pt-4 text-primary">No Images</div>
<mat-list>
<mat-list-item *ngFor="let image of cameraService.images" class="hover">
<div class="w-100 d-flex align-items-center hover">
<div class="pr-2 pt-1">
<mat-icon>image</mat-icon>
</div>
<div class="flex-grow-1">
<a target="_blank" [href]="environment.api + '/images/' + image">{{image}}</a>
</div>
<div>
<button mat-icon-button class="text-danger" (click)="cameraService.del(image)"><mat-icon>delete</mat-icon></button>
</div>
</div>
</mat-list-item>
</mat-list>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
import {Component} from "@angular/core";
import {CameraService} from "../../services/camera.service";
import {environment} from "../../../environments/environment";
@Component({
selector: 'camera',
templateUrl: `./camera.component.html`
})
export class CameraComponent {
environment = environment;
config;
constructor(public cameraService: CameraService) {
let ignore = this.cameraService.list();
}
async save() {
await this.cameraService.save(this.cameraService.config);
this.config = this.cameraService.config;
}
}

View File

@ -0,0 +1,57 @@
<div class="container-fluid">
<div class="row" style="align-items: flex-end;">
<div class="d-none d-lg-block col-lg-4">
<mat-card>
<mat-card-content>
<temperature></temperature>
</mat-card-content>
</mat-card>
</div>
<div class="col-12 col-lg-4 mt-4">
<mat-card>
<mat-card-content>
<div class="d-flex">
<div style="writing-mode: vertical-lr;">
<light-toggle></light-toggle>
<h1 class=" pt-3 d-inline">Light</h1>
</div>
<div class="flex-grow-1 ml-3 border-left p-3">
<light-auto></light-auto>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="col-12 col-lg-4 mt-4">
<mat-card>
<mat-card-content>
<div class="d-flex">
<div style="writing-mode: vertical-lr;">
<fan-toggle></fan-toggle>
<h1 class=" pt-3 d-inline">Fan</h1>
</div>
<div class="flex-grow-1 ml-3 border-left p-3">
<fan-auto></fan-auto>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="d-block d-lg-none col-12 mt-4">
<mat-card>
<mat-card-content>
<temperature></temperature>
</mat-card-content>
</mat-card>
</div>
</div>
<div class="row my-4">
<div class="col">
<mat-card>
<mat-card-content>
<climate-graph></climate-graph>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

View File

@ -0,0 +1,8 @@
import {Component} from "@angular/core";
import {ClimateService} from "../../services/climate.service";
@Component({
selector: 'climate',
templateUrl: 'climate.component.html'
})
export class ClimateComponent { }

View File

@ -0,0 +1,22 @@
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-4 mt-4">
<div class="row">
<div class="col-12">
<mat-card>
<mat-card-content>
<temperature></temperature>
</mat-card-content>
</mat-card>
</div>
<div class="col-12 mt-4">
<mat-card style="padding: 3px">
<mat-card-content>
<stream></stream>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
import {Component} from "@angular/core";
@Component({
selector: 'dashboard',
templateUrl: './dashboard.component.html'
})
export class DashboardComponent{ }

View File

@ -0,0 +1 @@
<stream [fullscreen]="true"></stream>

View File

@ -0,0 +1,7 @@
import {Component} from "@angular/core";
@Component({
selector: 'fullscreen-camera',
templateUrl: `./fullscreenCamera.component.html`,
})
export class FullscreenCameraComponent { }

View File

@ -0,0 +1,9 @@
import {Component} from "@angular/core";
@Component({
selector: 'grow-ops',
templateUrl: 'growOps.component.html'
})
export class GrowOpsComponent {
constructor() { }
}

View File

@ -0,0 +1,7 @@
import {Component} from "@angular/core";
@Component({
selector: 'notes',
templateUrl: './notes.component.html'
})
export class NotesComponent { }

View File

@ -0,0 +1,7 @@
import {Component} from "@angular/core";
@Component({
selector: 'schedule',
templateUrl: 'schedule.component.html'
})
export class ScheduleComponent { }

View File

@ -0,0 +1,7 @@
import {Component} from "@angular/core";
@Component({
selector: 'settings',
templateUrl: 'settings.component.html'
})
export class SettingsComponent { }

View File

@ -0,0 +1,9 @@
import {Component} from "@angular/core";
@Component({
selector: 'water',
templateUrl: 'water.component.html'
})
export class WaterComponent {
constructor() { }
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,4 @@
export const environment = {
api: 'http://localhost:5000',
production: false
};

24
client/src/index.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<base href="/">
<title>GrowBot</title>
<link rel="icon" type="image/png" href="/assets/images/logo.png">
<!-- TODO: meta, manifest & PWA
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#032f1f">
-->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="background-color: #053825">
<app-root>
<img style="position: fixed; left: 50vw; top: 50vh; transform: translate(-50%, -50%);" src="/assets/images/logo.png" src="GrowBot">
</app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>

12
client/src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

63
client/src/polyfills.ts Normal file
View File

@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

27
client/src/styles.scss Normal file
View File

@ -0,0 +1,27 @@
@import 'src/app/bootstrap';
@import 'src/app/material';
html, body { height: 100%; }
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
button {
outline: none !important;
}
// Scrollbar
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #FFFFFF15;
}
::-webkit-scrollbar {
width: 10px;
background-color: #FFFFFF15;
}
::-webkit-scrollbar-thumb {
background-color: #689f38;
}

33
client/tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./out-tsc/app",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"types": [],
"lib": [
"es2018",
"dom"
]
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}