├── src ├── assets │ ├── .gitkeep │ ├── notes.png │ ├── clipboard.png │ ├── empty-todos.png │ ├── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── emojione_fire.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ └── auth │ │ │ ├── facebook.svg │ │ │ ├── twitter.svg │ │ │ ├── google.svg │ │ │ └── github.svg │ ├── empty-collaborators.png │ └── firebase-logo-built_white.svg ├── app │ ├── app.component.html │ ├── notes │ │ ├── components │ │ │ ├── notes-geolocation │ │ │ │ ├── notes-geolocation.component.scss │ │ │ │ ├── notes-geolocation.component.html │ │ │ │ ├── notes-geolocation.component.spec.ts │ │ │ │ └── notes-geolocation.component.ts │ │ │ ├── note │ │ │ │ ├── note.component.scss │ │ │ │ ├── note.component.ts │ │ │ │ └── note.component.html │ │ │ ├── shared-with │ │ │ │ ├── shared-with.component.ts │ │ │ │ ├── shared-with.component.html │ │ │ │ └── shared-with.component.spec.ts │ │ │ ├── note-collaborators │ │ │ │ ├── note-collaborators.component.spec.ts │ │ │ │ ├── note-collaborators.component.html │ │ │ │ └── note-collaborators.component.ts │ │ │ ├── notes-list │ │ │ │ ├── notes-list.component.ts │ │ │ │ └── notes-list.component.html │ │ │ ├── todos │ │ │ │ ├── todos.component.html │ │ │ │ └── todos.component.ts │ │ │ └── note-add │ │ │ │ ├── note-add.component.ts │ │ │ │ └── note-add.component.html │ │ ├── models │ │ │ ├── todo.model.ts │ │ │ ├── collaborator.model.ts │ │ │ ├── place.model.ts │ │ │ ├── location.model.ts │ │ │ └── note.model.ts │ │ ├── notes-routing.module.ts │ │ ├── notes.module.ts │ │ └── directives │ │ │ └── google-places-autocomplete.directive.ts │ ├── shared │ │ ├── services │ │ │ ├── location.service.spec.ts │ │ │ └── location.service.ts │ │ └── shared.module.ts │ ├── auth │ │ ├── auth.component.scss │ │ ├── auth.component.ts │ │ └── auth.component.html │ ├── app-routing.module.ts │ ├── core │ │ ├── nav-bar │ │ │ ├── nav-bar.component.ts │ │ │ └── nav-bar.component.html │ │ ├── auth.guard.ts │ │ ├── core.module.ts │ │ └── auth.service.ts │ ├── app.module.ts │ ├── app.component.ts │ ├── app.component.spec.ts │ └── material.module.ts ├── favicon.ico ├── typings.d.ts ├── tsconfig.app.json ├── material-theme.scss ├── tsconfig.spec.json ├── main.ts ├── main.augury.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── ngsw-config.json ├── test.ts ├── index.html ├── manifest.json ├── polyfills.ts └── styles.scss ├── index.d.ts ├── .firebaserc ├── typings.json ├── e2e ├── app.po.ts ├── tsconfig.e2e.json └── app.e2e-spec.ts ├── functions ├── tsconfig.json ├── package.json ├── src │ └── index.ts └── tslint.json ├── .editorconfig ├── firestore.indexes.json ├── tsconfig.json ├── ngsw-config.json ├── firebase.json ├── .gitignore ├── protractor.conf.js ├── firestore.rules ├── karma.conf.js ├── README.md ├── package.json ├── tslint.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'googlemaps'; -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "firelist-angular-dev" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/assets/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/notes.png -------------------------------------------------------------------------------- /src/assets/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/clipboard.png -------------------------------------------------------------------------------- /src/assets/empty-todos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/empty-todos.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "google.maps": "registry:dt/google.maps#3.25.0+20161208205818" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/empty-collaborators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/empty-collaborators.png -------------------------------------------------------------------------------- /src/assets/icons/emojione_fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/emojione_fire.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/firelist-angular/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/notes/components/notes-geolocation/notes-geolocation.component.scss: -------------------------------------------------------------------------------- 1 | agm-map { 2 | width: 100%; 3 | height: calc(100vh - 64px); 4 | } -------------------------------------------------------------------------------- /src/app/notes/models/todo.model.ts: -------------------------------------------------------------------------------- 1 | export class Todo { 2 | id?: string; 3 | title: string; 4 | completed: boolean; 5 | createdAt?: any; 6 | createdBy?: any; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/notes/models/collaborator.model.ts: -------------------------------------------------------------------------------- 1 | export interface Collaborator { 2 | email: string; 3 | photoURL: string; 4 | uid?: string; 5 | owner?: boolean; 6 | invitedBy?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/notes/components/notes-geolocation/notes-geolocation.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/notes/models/place.model.ts: -------------------------------------------------------------------------------- 1 | export class Place { 2 | name: string; 3 | phone_number?: string; 4 | place_id: string; 5 | rating?: number; 6 | types: []; 7 | vicinity: string; 8 | website?: string; 9 | } 10 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/notes/components/note/note.component.scss: -------------------------------------------------------------------------------- 1 | .date-picker-calendar-only { 2 | width: 1px; 3 | position: absolute; 4 | left: 10px; 5 | height: 1px; 6 | z-index: -999; 7 | } 8 | 9 | .note-archived { 10 | color: rgba(0,0,0,.3); 11 | font-style: italic; 12 | } -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | // Example: 3 | // 4 | // "indexes": [ 5 | // { 6 | // "collectionId": "widgets", 7 | // "fields": [ 8 | // { "fieldPath": "foo", "mode": "ASCENDING" }, 9 | // { "fieldPath": "bar", "mode": "DESCENDING" } 10 | // ] 11 | // } 12 | // ] 13 | "indexes": [] 14 | } -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('firelist-angular App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/material-theme.scss: -------------------------------------------------------------------------------- 1 | // http://mcg.mbitson.com/ MATERIAL DESIGN PALETTE GENERATOR 2 | @import '~@angular/material/theming'; 3 | @include mat-core(); 4 | 5 | $custom-primary: mat-palette($mat-yellow); 6 | $custom-accent: mat-palette($mat-pink); 7 | 8 | $custom-theme: mat-light-theme($custom-primary, $custom-accent); 9 | 10 | @include angular-material-theme($custom-theme); -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts", 14 | "polyfills.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/notes/models/location.model.ts: -------------------------------------------------------------------------------- 1 | import { Place } from './place.model'; 2 | 3 | export class NoteLocation { 4 | admin_area_l1: string; 5 | country: string; 6 | country_code: string; 7 | formatted_address: string; 8 | lat: number; 9 | lng: number; 10 | locality: string; 11 | place: Place; 12 | postal_code?: string; 13 | route: string; 14 | street_number?: string; 15 | url: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/services/location.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LocationService } from './location.service'; 4 | 5 | describe('LocationService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: LocationService = TestBed.get(LocationService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/notes/components/shared-with/shared-with.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Collaborator } from '../../models/collaborator.model'; 3 | 4 | @Component({ 5 | selector: 'app-note-shared-with', 6 | templateUrl: './shared-with.component.html' 7 | }) 8 | export class SharedWithComponent { 9 | @Input() collaborators: Collaborator[]; 10 | @Input() noteId: string; 11 | 12 | constructor() { } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import 'hammerjs'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.log(err)); 15 | -------------------------------------------------------------------------------- /src/main.augury.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { NgZone } from '@angular/core'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | 6 | import { auguryBootstrap } from '@augury/core'; 7 | import { PerformanceProfilerPlugin } from '@augury/performance-profiler-plugin'; 8 | 9 | auguryBootstrap({ 10 | platform: platformBrowserDynamic, 11 | ngModule: AppModule, 12 | NgZone, 13 | plugins: [new PerformanceProfilerPlugin()], 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ], 18 | "module": "es2015", 19 | "baseUrl": "./" 20 | } 21 | } -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | firebase: { 4 | apiKey: 'AIzaSyAO2qoUsT13mjZQCkN68JxV1dq9X1ffBaI', 5 | authDomain: 'firelist-angular-dev.firebaseapp.com', 6 | databaseURL: 'https://firelist-angular-dev.firebaseio.com', 7 | projectId: 'firelist-angular-dev', 8 | storageBucket: 'firelist-angular-dev.appspot.com', 9 | messagingSenderId: '131167589613' 10 | }, 11 | googleMapsKey: 'AIzaSyCTDRoGnPwEHP_Iw1Vc68GLrmNQ7iwBkhA' 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | .identity-providers { 2 | button { 3 | color: #FFF; 4 | 5 | &.auth-google { 6 | background-color: #FFF; 7 | color: #727272; 8 | } 9 | 10 | &.auth-facebook { 11 | background-color: #3b5998; 12 | } 13 | 14 | &.auth-twitter { 15 | background-color: #55acee; 16 | } 17 | 18 | &.auth-github { 19 | background-color: #333; 20 | } 21 | 22 | .mat-icon { 23 | height: 24px; 24 | margin-right: 4px; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [ 4 | { 5 | "name": "app", 6 | "installMode": "prefetch", 7 | "resources": { 8 | "files": [ 9 | "/favicon.ico", 10 | "/index.html", 11 | "/*.css", 12 | "/*.js" 13 | ] 14 | } 15 | }, { 16 | "name": "assets", 17 | "installMode": "lazy", 18 | "updateMode": "prefetch", 19 | "resources": { 20 | "files": [ 21 | "/assets/**" 22 | ] 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [ 4 | { 5 | "name": "app", 6 | "installMode": "prefetch", 7 | "resources": { 8 | "files": [ 9 | "/favicon.ico", 10 | "/index.html", 11 | "/*.css", 12 | "/*.js" 13 | ] 14 | } 15 | }, 16 | { 17 | "name": "assets", 18 | "installMode": "lazy", 19 | "updateMode": "prefetch", 20 | "resources": { 21 | "files": [ 22 | "/assets/**" 23 | ] 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "hosting": { 7 | "public": "dist", 8 | "ignore": [ 9 | "firebase.json", 10 | "**/.*", 11 | "**/node_modules/**" 12 | ], 13 | "rewrites": [ 14 | { 15 | "source": "**", 16 | "destination": "/index.html" 17 | } 18 | ] 19 | }, 20 | "functions": { 21 | "predeploy": [ 22 | "npm --prefix \"$RESOURCE_DIR\" run lint", 23 | "npm --prefix \"$RESOURCE_DIR\" run build" 24 | ], 25 | "source": "functions" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AuthComponent } from './auth/auth.component'; 4 | import { AuthGuard } from './core/auth.guard'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | redirectTo: '/notes', 10 | pathMatch: 'full', 11 | canActivate: [ AuthGuard ] 12 | }, 13 | { 14 | path: 'auth', 15 | component: AuthComponent 16 | } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [RouterModule.forRoot(routes)], 21 | exports: [RouterModule] 22 | }) 23 | export class AppRoutingModule { } 24 | -------------------------------------------------------------------------------- /src/app/notes/models/note.model.ts: -------------------------------------------------------------------------------- 1 | import { Collaborator } from './collaborator.model'; 2 | import { NoteLocation } from './location.model'; 3 | import { Todo } from './todo.model'; 4 | 5 | export class Note { 6 | id?: string; 7 | title: string; 8 | description?: string; 9 | dueDate?: any; 10 | location?: string; 11 | geolocation?: NoteLocation; 12 | geopoint?: any; 13 | createdAt: any; 14 | createdBy?: any; 15 | photoURL: string; 16 | owner: string; 17 | archived?: boolean; 18 | todos?: Todo[]; 19 | collaborators?: any[]; 20 | sharedWith?: Collaborator[]; 21 | isInvitaionFormEnabled?: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/notes/components/shared-with/shared-with.component.html: -------------------------------------------------------------------------------- 1 |
2 | 14 |
-------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | 7 | import { LocationService } from './services/location.service'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | HttpClientModule, 14 | ReactiveFormsModule, 15 | RouterModule 16 | ], 17 | exports: [ 18 | CommonModule, 19 | FormsModule, 20 | ReactiveFormsModule, 21 | RouterModule 22 | ], 23 | providers: [ 24 | LocationService 25 | ] 26 | }) 27 | export class SharedModule { } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | **/node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /**/*.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | 43 | # firebase 44 | firebase-adminsdk-credentials.json 45 | functions/lib 46 | .firebase -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "main": "lib/index.js", 13 | "dependencies": { 14 | "firebase-admin": "~6.0.0", 15 | "firebase-functions": "^2.0.5", 16 | "node-emoji": "^1.8.1", 17 | "nodemailer": "^4.7.0" 18 | }, 19 | "devDependencies": { 20 | "tslint": "~5.8.0", 21 | "typescript": "~2.8.3" 22 | }, 23 | "private": true, 24 | "engines": { 25 | "node": "8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/notes/components/shared-with/shared-with.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SharedWithComponent } from './shared-with.component'; 4 | 5 | describe('SharedWithComponent', () => { 6 | let component: SharedWithComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SharedWithComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SharedWithComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | firebase: { 9 | apiKey: 'AIzaSyAO2qoUsT13mjZQCkN68JxV1dq9X1ffBaI', 10 | authDomain: 'firelist-angular-dev.firebaseapp.com', 11 | databaseURL: 'https://firelist-angular-dev.firebaseio.com', 12 | projectId: 'firelist-angular-dev', 13 | storageBucket: 'firelist-angular-dev.appspot.com', 14 | messagingSenderId: '131167589613' 15 | }, 16 | googleMapsKey: 'AIzaSyCTDRoGnPwEHP_Iw1Vc68GLrmNQ7iwBkhA' 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/notes/components/notes-geolocation/notes-geolocation.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotesGeolocationComponent } from './notes-geolocation.component'; 4 | 5 | describe('NotesGeolocationComponent', () => { 6 | let component: NotesGeolocationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NotesGeolocationComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotesGeolocationComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/nav-bar/nav-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthService } from '../auth.service'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-nav-bar', 7 | templateUrl: './nav-bar.component.html', 8 | styles: ['.toolbar-spacer { flex: 1 1 auto; } .emoji { height: 20px; position: relative; top: 2px; }'] 9 | }) 10 | export class NavBarComponent { 11 | 12 | constructor( 13 | public auth: AuthService, 14 | private router: Router 15 | ) {} 16 | 17 | goToNotesList() { 18 | this.router.navigate(['/notes']); 19 | } 20 | 21 | goToNotesGeolocation() { 22 | this.router.navigate(['/notes/geolocation']); 23 | } 24 | 25 | logout(evt: Event) { 26 | const message = 'You have been signed out'; 27 | this.auth.signOut(); 28 | this.router.navigate(['/auth']); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/notes/components/note-collaborators/note-collaborators.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NoteCollaboratorsComponent } from './note-collaborators.component'; 4 | 5 | describe('NoteCollaboratorsComponent', () => { 6 | let component: NoteCollaboratorsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NoteCollaboratorsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NoteCollaboratorsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/notes/components/notes-geolocation/notes-geolocation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { LocationService } from '../../../shared/services/location.service'; 3 | 4 | @Component({ 5 | selector: 'app-notes-geolocation', 6 | templateUrl: './notes-geolocation.component.html', 7 | styleUrls: ['./notes-geolocation.component.scss'] 8 | }) 9 | export class NotesGeolocationComponent implements OnInit { 10 | lat: number; 11 | lng: number; 12 | constructor(private locationService: LocationService) { } 13 | 14 | ngOnInit() { 15 | this.getUserLocation(); 16 | } 17 | 18 | private getUserLocation() { 19 | if (navigator.geolocation) { 20 | navigator.geolocation.getCurrentPosition(position => { 21 | this.lat = position.coords.latitude; 22 | this.lng = position.coords.longitude; 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Firelist Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | 4 | match /notes/{noteId} { 5 | allow read: if isAuthenticated() && isNoteOwner() 6 | allow create: if isAuthenticated() && isNoteValid() 7 | 8 | match /todos/{todoId} { 9 | allow read, write: if isAuthenticated() 10 | } 11 | } 12 | 13 | // Functions 14 | function isAuthenticated() { 15 | return request.auth.uid != null 16 | } 17 | 18 | function isNoteValid() { 19 | return incomingRequestData().title is string && 20 | incomingRequestData().title.size() > 0 && 21 | incomingRequestData().owner == request.auth.uid 22 | } 23 | 24 | function incomingRequestData() { 25 | return request.resource.data 26 | } 27 | 28 | function isNoteOwner() { 29 | return resource.data.owner == request.auth.uid 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { environment } from '../environments/environment'; 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { CoreModule } from './core/core.module'; 7 | import { ServiceWorkerModule } from '@angular/service-worker'; 8 | 9 | import { AppComponent } from './app.component'; 10 | import { AuthComponent } from './auth/auth.component'; 11 | import { NotesModule } from './notes/notes.module'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent, 16 | AuthComponent 17 | ], 18 | imports: [ 19 | AppRoutingModule, 20 | BrowserModule, 21 | CoreModule, 22 | NotesModule, 23 | ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) 24 | ], 25 | providers: [], 26 | bootstrap: [AppComponent] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /src/app/core/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { AuthService } from './auth.service'; 5 | 6 | import 'rxjs/add/operator/do'; 7 | import 'rxjs/add/operator/map'; 8 | import 'rxjs/add/operator/take'; 9 | 10 | @Injectable() 11 | export class AuthGuard implements CanActivate { 12 | constructor( 13 | private authService: AuthService, 14 | private router: Router) {} 15 | 16 | canActivate( 17 | next: ActivatedRouteSnapshot, 18 | state: RouterStateSnapshot): Observable | Promise | boolean { 19 | 20 | return this.authService.authState$ 21 | .take(1) 22 | .map(authState => !!authState) 23 | .do(authenticated => { 24 | if (!authenticated) { 25 | this.router.navigate(['/auth']); 26 | } 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/services/location.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { environment } from '../../../environments/environment'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class LocationService { 9 | lat: number; 10 | lng: number; 11 | 12 | constructor(private http: HttpClient) {} 13 | 14 | getUserLocation() { 15 | if (navigator.geolocation) { 16 | navigator.geolocation.getCurrentPosition(position => { 17 | this.lat = position.coords.latitude; 18 | this.lng = position.coords.longitude; 19 | }); 20 | } 21 | } 22 | 23 | getPlaceDetails(placeId: string) { 24 | // tslint:disable-next-line:max-line-length 25 | const placeDetailUrl = `https://maps.googleapis.com/maps/api/place/details/json?key=${environment.googleMapsKey}&placeid=${placeId}&fields=name,type,international_phone_number,opening_hours,website,price_level,rating,review`; 26 | 27 | return this.http.get(placeDetailUrl); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthService } from '../core/auth.service'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-auth', 7 | templateUrl: './auth.component.html', 8 | styleUrls: ['./auth.component.scss'] 9 | }) 10 | export class AuthComponent { 11 | 12 | constructor( 13 | public auth: AuthService, 14 | private router: Router 15 | ) { } 16 | 17 | async connectWithGoogle() { 18 | await this.auth.signInWithGoogle(); 19 | this.redirectAfterAuth(); 20 | } 21 | 22 | async connectWithFacebook() { 23 | await this.auth.signInWithFacebook(); 24 | this.redirectAfterAuth(); 25 | } 26 | 27 | async connectWithTwitter() { 28 | await this.auth.signInWithTwitter(); 29 | this.redirectAfterAuth(); 30 | } 31 | 32 | async connectWithGithub() { 33 | await this.auth.signInWithGithub(); 34 | this.redirectAfterAuth(); 35 | } 36 | 37 | private redirectAfterAuth(): void { 38 | this.router.navigate(['/']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ElementRef } from '@angular/core'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | import { MatIconRegistry } from '@angular/material'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html' 8 | }) 9 | export class AppComponent { 10 | title = 'Firelist ⚡'; 11 | 12 | constructor( 13 | iconRegistry: MatIconRegistry, 14 | sanitizer: DomSanitizer 15 | ) { 16 | /** ICONS **/ 17 | iconRegistry.addSvgIcon( 18 | 'google', 19 | sanitizer.bypassSecurityTrustResourceUrl('assets/icons/auth/google.svg')); 20 | 21 | iconRegistry.addSvgIcon( 22 | 'facebook', 23 | sanitizer.bypassSecurityTrustResourceUrl('assets/icons/auth/facebook.svg')); 24 | 25 | iconRegistry.addSvgIcon( 26 | 'twitter', 27 | sanitizer.bypassSecurityTrustResourceUrl('assets/icons/auth/twitter.svg')); 28 | 29 | iconRegistry.addSvgIcon( 30 | 'github', 31 | sanitizer.bypassSecurityTrustResourceUrl('assets/icons/auth/github.svg')); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { environment } from '../../environments/environment'; 5 | import { AngularFireModule } from '@angular/fire'; 6 | import { AngularFireAuthModule } from '@angular/fire/auth'; 7 | import { AngularFireDatabaseModule } from '@angular/fire/database'; 8 | import { AngularFirestoreModule } from '@angular/fire/firestore'; 9 | import { AngularFireStorageModule } from '@angular/fire/storage'; 10 | 11 | import { MaterialModule } from '../material.module'; 12 | import { NavBarComponent } from './nav-bar/nav-bar.component'; 13 | import { AuthService } from './auth.service'; 14 | import { AuthGuard } from './auth.guard'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | AngularFireModule.initializeApp(environment.firebase), 19 | AngularFireAuthModule, 20 | AngularFireDatabaseModule, 21 | AngularFirestoreModule.enablePersistence(), 22 | AngularFireStorageModule, 23 | CommonModule, 24 | MaterialModule 25 | ], 26 | declarations: [ NavBarComponent ], 27 | exports: [ 28 | MaterialModule, 29 | NavBarComponent 30 | ], 31 | providers: [AuthService, AuthGuard] 32 | }) 33 | export class CoreModule { } 34 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [ 8 | RouterTestingModule 9 | ], 10 | declarations: [ 11 | AppComponent 12 | ], 13 | }).compileComponents(); 14 | })); 15 | it('should create the app', async(() => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.debugElement.componentInstance; 18 | expect(app).toBeTruthy(); 19 | })); 20 | it(`should have as title 'app'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app'); 24 | })); 25 | it('should render title in a h1 tag', async(() => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to Firelist ⚡'); 30 | })); 31 | }); 32 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Firelist", 3 | "short_name": "Firelist", 4 | "theme_color": "#37474F", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /src/app/auth/auth.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Focus on being productive instead of busy.
Tim Ferriss
6 | 7 |

Welcome to the Firelist Angular web app.
Check out the entire course at Firebase Fulltasck right now.

8 |
9 | 10 | 11 | 14 | 17 | 20 | 23 | 24 |
-------------------------------------------------------------------------------- /src/app/notes/notes-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from '../core/auth.guard'; 5 | import { NotesListComponent } from './components/notes-list/notes-list.component'; 6 | import { NoteAddComponent } from './components/note-add/note-add.component'; 7 | import { NoteComponent } from './components/note/note.component'; 8 | import { NoteCollaboratorsComponent } from './components/note-collaborators/note-collaborators.component'; 9 | import { NotesGeolocationComponent } from './components/notes-geolocation/notes-geolocation.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: 'notes', 14 | component: NotesListComponent, 15 | canActivate: [ AuthGuard ] 16 | }, 17 | { 18 | path: 'notes/geolocation', 19 | component: NotesGeolocationComponent, 20 | canActivate: [AuthGuard] 21 | }, 22 | { 23 | path: 'notes/add', 24 | component: NoteAddComponent, 25 | canActivate: [ AuthGuard ] 26 | }, 27 | { 28 | path: 'note/:id', 29 | component: NoteComponent, 30 | canActivate: [AuthGuard] 31 | }, 32 | { 33 | path: 'note/:id/collaborators', 34 | component: NoteCollaboratorsComponent, 35 | canActivate: [AuthGuard] 36 | } 37 | ]; 38 | 39 | @NgModule({ 40 | imports: [RouterModule.forChild(routes)], 41 | exports: [RouterModule] 42 | }) 43 | export class NotesRoutingModule { } 44 | -------------------------------------------------------------------------------- /src/app/core/nav-bar/nav-bar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 10 | Firelist 11 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 26 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | Firelist 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/app/notes/components/notes-list/notes-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore'; 4 | 5 | import { Note } from '../../models/note.model'; 6 | import { AuthService } from '../../../core/auth.service'; 7 | 8 | @Component({ 9 | selector: 'app-notes-list', 10 | templateUrl: './notes-list.component.html' 11 | }) 12 | export class NotesListComponent implements OnInit { 13 | private notesCollection: AngularFirestoreCollection; 14 | notes$: Observable; 15 | 16 | constructor( 17 | private afs: AngularFirestore, 18 | private authService: AuthService 19 | ) {} 20 | 21 | ngOnInit() { 22 | this.authService.authState$.subscribe(user => { 23 | // Firestore Array Query (Array membership) https://firebase.google.com/docs/firestore/query-data/queries#array_membership 24 | this.notesCollection = this.afs.collection('notes', ref => ref.where('collaborators', 'array-contains', user.email)); 25 | 26 | this.notes$ = this.notesCollection.snapshotChanges().map(actions => { 27 | return actions.filter(item => !item.payload.doc.data().archived) 28 | .map(a => { 29 | const data = a.payload.doc.data() as Note; 30 | const id = a.payload.doc.id; 31 | return { id, ...data }; 32 | }); 33 | }); 34 | }); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/notes/notes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { environment } from '../../environments/environment'; 3 | 4 | import { MaterialModule } from '../material.module'; 5 | import { NotesRoutingModule } from './notes-routing.module'; 6 | import { SharedModule } from '../shared/shared.module'; 7 | 8 | import { NoteAddComponent } from './components/note-add/note-add.component'; 9 | import { NotesListComponent } from './components/notes-list/notes-list.component'; 10 | import { NoteComponent } from './components/note/note.component'; 11 | import { TodosComponent } from './components/todos/todos.component'; 12 | import { NoteCollaboratorsComponent } from './components/note-collaborators/note-collaborators.component'; 13 | import { SharedWithComponent } from './components/shared-with/shared-with.component'; 14 | import { NotesGeolocationComponent } from './components/notes-geolocation/notes-geolocation.component'; 15 | import { GooglePlacesAutocompleteDirective } from './directives/google-places-autocomplete.directive'; 16 | import { AgmCoreModule } from '@agm/core'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | MaterialModule, 21 | NotesRoutingModule, 22 | SharedModule, 23 | AgmCoreModule.forRoot({ 24 | apiKey: environment.googleMapsKey 25 | }) 26 | ], 27 | declarations: [ 28 | NotesListComponent, 29 | NoteAddComponent, 30 | NoteComponent, 31 | TodosComponent, 32 | NoteCollaboratorsComponent, 33 | SharedWithComponent, 34 | NotesGeolocationComponent, 35 | GooglePlacesAutocompleteDirective 36 | ] 37 | }) 38 | export class NotesModule { } 39 | -------------------------------------------------------------------------------- /src/assets/icons/auth/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | facebook_buttn 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { 5 | MatButtonModule, 6 | MatCardModule, 7 | MatCheckboxModule, 8 | MatChipsModule, 9 | MatDatepickerModule, 10 | MatFormFieldModule, 11 | MatGridListModule, 12 | MatIconModule, 13 | MatInputModule, 14 | MatListModule, 15 | MatMenuModule, 16 | MatNativeDateModule, 17 | MatProgressBarModule, 18 | MatProgressSpinnerModule, 19 | MatSnackBarModule, 20 | MatToolbarModule, 21 | MatTooltipModule 22 | } from '@angular/material'; 23 | 24 | @NgModule({ 25 | imports: [ 26 | BrowserAnimationsModule, 27 | HttpClientModule, 28 | MatButtonModule, 29 | MatCardModule, 30 | MatCheckboxModule, 31 | MatChipsModule, 32 | MatDatepickerModule, 33 | MatFormFieldModule, 34 | MatGridListModule, 35 | MatIconModule, 36 | MatInputModule, 37 | MatListModule, 38 | MatMenuModule, 39 | MatNativeDateModule, 40 | MatProgressBarModule, 41 | MatProgressSpinnerModule, 42 | MatSnackBarModule, 43 | MatToolbarModule, 44 | MatTooltipModule 45 | ], 46 | exports: [ 47 | BrowserAnimationsModule, 48 | HttpClientModule, 49 | MatButtonModule, 50 | MatCardModule, 51 | MatCheckboxModule, 52 | MatChipsModule, 53 | MatDatepickerModule, 54 | MatFormFieldModule, 55 | MatGridListModule, 56 | MatIconModule, 57 | MatInputModule, 58 | MatListModule, 59 | MatMenuModule, 60 | MatNativeDateModule, 61 | MatProgressBarModule, 62 | MatProgressSpinnerModule, 63 | MatSnackBarModule, 64 | MatToolbarModule, 65 | MatTooltipModule 66 | ] 67 | }) 68 | export class MaterialModule { } 69 | -------------------------------------------------------------------------------- /src/app/notes/components/todos/todos.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
If you spend too much time thinking about a thing, you’ll never get it done.Bruce Lee
6 |
7 |
8 | 9 | 10 |
11 | 12 | {{todosPercentage}}% 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | add 31 |
32 | 33 | 34 | 35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firelist Angular ⌨️🥊🔥 2 | 3 | Firelist has been built using the latest and greatest features of Angular & Firebase and to be a companion for our [Fullstack Firebase](https://www.fullstackfirebase.com) course. 4 | 5 | Following this course you will see **Angular CLI** tips, Angular **Best Practices** including **TypeScript** and **Modern JavaScript**, **Material Design** for the UI components, **AngularFire2**, and tons of Firebaaaaase awesomeness. 6 | 7 | ## What we do have in place: 8 | 9 | 🔒 [Authentication](https://firebase.google.com/docs/auth/web/start) with Google 10 | 11 | ✏️ Notes management using [Firestore](https://firebase.google.com/docs/firestore) 12 | 13 | 🎈 Activity using the [Realtime Database](https://firebase.google.com/docs/database/web/start) 14 | 15 | 🌈 [Cloud Functions](https://firebase.google.com/docs/functions) for advanced techniques including image thumbnails and sub-collections removal 16 | 17 | 🖼️ [Cloud Storage](https://firebase.google.com/docs/storage/web/start) for the built-in gallery section inside of each note 18 | 19 | 🌩️ [Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/js/client) to send push notifications when a new collaborator is added to a note 20 | 21 | 🌟 And of course, [Firebase Hosting](https://firebase.google.com/docs/hosting) which provides a fast and secure web hosting 22 | 23 | ## Development 24 | 25 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.7.4 and it's current using Angular 5.2.10. 26 | 27 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 28 | 29 | ## Build 30 | 31 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 32 | 33 | ## Further help 34 | 35 | If you got stuck at some point or just need general help please open an issue or fire up an email to [hello@fullstackfirebase.com](mailto:hello@fullstackfirebase.com) 💌 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firelist-angular", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --host firelist.angular --port 4001", 8 | "start:augury": "ng serve --port 4002 --configuration augury", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@agm/core": "^1.0.0-beta.3", 17 | "@angular/animations": "^7.1.0", 18 | "@angular/cdk": "^7.1.0", 19 | "@angular/common": "^7.1.0", 20 | "@angular/compiler": "^7.1.0", 21 | "@angular/core": "^7.1.0", 22 | "@angular/fire": "^5.1.0", 23 | "@angular/forms": "^7.1.0", 24 | "@angular/http": "^7.1.0", 25 | "@angular/material": "^7.1.0", 26 | "@angular/platform-browser": "^7.1.0", 27 | "@angular/platform-browser-dynamic": "^7.1.0", 28 | "@angular/pwa": "^0.10.6", 29 | "@angular/router": "^7.1.0", 30 | "@angular/service-worker": "^7.1.0", 31 | "core-js": "^2.5.7", 32 | "firebase": "^5.5.9", 33 | "hammerjs": "^2.0.8", 34 | "rxjs": "^6.3.3", 35 | "rxjs-compat": "^6.3.3", 36 | "zone.js": "^0.8.26" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "~0.10.0", 40 | "@angular/cli": "^7.0.6", 41 | "@angular/compiler-cli": "^7.1.0", 42 | "@angular/language-service": "^7.1.0", 43 | "@augury/core": "^0.5.0", 44 | "@augury/performance-profiler-plugin": "^0.5.0", 45 | "@types/googlemaps": "^3.30.16", 46 | "@types/jasmine": "~3.3.0", 47 | "@types/jasminewd2": "~2.0.6", 48 | "@types/node": "~10.12.10", 49 | "browser-sync": "^2.26.3", 50 | "codelyzer": "^4.5.0", 51 | "jasmine-core": "~3.3.0", 52 | "jasmine-spec-reporter": "~4.2.1", 53 | "karma": "~3.1.1", 54 | "karma-chrome-launcher": "~2.2.0", 55 | "karma-coverage-istanbul-reporter": "^2.0.4", 56 | "karma-jasmine": "~2.0.1", 57 | "karma-jasmine-html-reporter": "^1.4.0", 58 | "protractor": "^5.4.1", 59 | "ts-node": "~7.0.1", 60 | "tslint": "~5.11.0", 61 | "typescript": "~3.1.6" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/notes/components/notes-list/notes-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{note.title}} 12 | 13 | 14 |

15 | {{note.description}} 16 |

17 | 20 | 21 | 28 |
29 |
30 | 31 | 34 |
35 | 36 | 37 | 38 | 39 | 40 |
If you spend too much time thinking about a thing, you’ll never get it done.Bruce Lee
41 |

You haven't created any note yet

42 | 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /src/assets/icons/auth/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | twitter_buttn 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/icons/auth/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | google_buttn 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/app/notes/components/note-collaborators/note-collaborators.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 |
A single rose can be my garden… a single friend, my world.Leo Buscaglia
13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 24 | Collaborators 25 | 26 | 27 | 28 | 29 | 30 | 31 | ... 32 |

{{collaborator.email}} (Owner)

33 | 37 |
38 | 39 | 40 | group_add 41 |
42 | 43 | 45 | 46 |
47 |
48 |
49 |
50 |
-------------------------------------------------------------------------------- /src/app/notes/components/todos/todos.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import * as firebase from 'firebase/app'; 4 | import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore'; 5 | import { Observable } from 'rxjs/Observable'; 6 | 7 | import { Todo } from '../../models/todo.model'; 8 | 9 | @Component({ 10 | selector: 'app-todos', 11 | templateUrl: './todos.component.html' 12 | }) 13 | export class TodosComponent implements OnInit { 14 | @Input() noteId: string; 15 | 16 | private todosCollection: AngularFirestoreCollection; 17 | todos$: Observable; 18 | todosList: Todo[]; 19 | todosPercentage: number; 20 | newTodoText = ''; 21 | 22 | constructor( 23 | private afs: AngularFirestore 24 | ) { } 25 | 26 | ngOnInit() { 27 | this.todosCollection = this.afs.collection(`notes/${this.noteId}/todos`, ref => ref.orderBy('createdAt')); 28 | this.todos$ = this.todosCollection.snapshotChanges().map(actions => { 29 | return actions.map(a => { 30 | const data = a.payload.doc.data() as Todo; 31 | const id = a.payload.doc.id; 32 | return { id, ...data }; 33 | }); 34 | }); 35 | 36 | this.todos$.subscribe(items => { 37 | this.todosList = [...items]; 38 | this.calculateCompletedTodosPercentage(); 39 | }); 40 | } 41 | 42 | addTodo() { 43 | if (this.newTodoText.trim().length) { 44 | const newTodo: Todo = { 45 | completed: false, 46 | title: this.newTodoText, 47 | createdAt: firebase.firestore.FieldValue.serverTimestamp() 48 | }; 49 | 50 | this.todosCollection.add(newTodo); 51 | this.newTodoText = ''; 52 | } 53 | } 54 | 55 | deleteTodo(e, todo): void { 56 | e.stopPropagation(); 57 | 58 | this.afs.doc(`notes/${this.noteId}/todos/${todo.id}`).delete(); 59 | } 60 | 61 | onTodoToggle(e, todo): void { 62 | const todoRef = this.todosCollection.doc(todo.id); 63 | const completedAt = (e.checked) && firebase.firestore.FieldValue.serverTimestamp(); 64 | 65 | todoRef.set({ 66 | completed: e.checked, 67 | completedAt 68 | }, { merge: true }); 69 | } 70 | 71 | updateTodoItem(e, todo): void { 72 | this.todosCollection.doc(todo.id).set({ 73 | title: e.target.value 74 | }, { merge: true }); 75 | } 76 | 77 | calculateCompletedTodosPercentage(): void { 78 | const completedTodos: number = this.todosList.filter(todoItem => todoItem.completed === true).length || 0; 79 | this.todosPercentage = Math.round((completedTodos / this.todosList.length) * 100); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/notes/directives/google-places-autocomplete.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, OnInit, Output, EventEmitter } from '@angular/core'; 2 | declare const google: any; 3 | 4 | @Directive({ 5 | selector: '[appGooglePlacesAutocomplete]' 6 | }) 7 | export class GooglePlacesAutocompleteDirective implements OnInit { 8 | @Output() 9 | placeSelected = new EventEmitter(); 10 | private element: HTMLInputElement; 11 | 12 | constructor(private elementRef: ElementRef) { 13 | this.element = elementRef.nativeElement; 14 | } 15 | 16 | ngOnInit() { 17 | const autocomplete = new google.maps.places.Autocomplete(this.element); 18 | 19 | google.maps.event.addListener(autocomplete, 'place_changed', () => { 20 | this.placeSelected.emit(this.getFormattedAddress(autocomplete.getPlace())); 21 | }); 22 | } 23 | 24 | private getFormattedAddress(place) { 25 | const locationObject = {}; 26 | const placeObject = { 27 | phone_number: place.international_phone_number || null, 28 | name: place.name || null, 29 | place_id: place.place_id, 30 | rating: place.rating || null, 31 | types: place.types || null, 32 | vicinity: place.vicinity || null, 33 | website: place.website || null 34 | }; 35 | 36 | locationObject['place'] = placeObject; 37 | locationObject['lat'] = place.geometry.location.lat(); 38 | locationObject['lng'] = place.geometry.location.lng(); 39 | locationObject['url'] = place.url; 40 | 41 | for (const i in place.address_components) { 42 | if (place.address_components.hasOwnProperty(i)) { 43 | const item = place.address_components[i]; 44 | 45 | locationObject['formatted_address'] = place.formatted_address; 46 | if (item['types'].indexOf('locality') > -1) { 47 | locationObject['locality'] = item['long_name']; 48 | } else if (item['types'].indexOf('administrative_area_level_1') > -1) { 49 | locationObject['admin_area_l1'] = item['short_name']; 50 | } else if (item['types'].indexOf('street_number') > -1) { 51 | locationObject['street_number'] = item['short_name']; 52 | } else if (item['types'].indexOf('route') > -1) { 53 | locationObject['route'] = item['long_name']; 54 | } else if (item['types'].indexOf('country') > -1) { 55 | locationObject['country'] = item['long_name']; 56 | locationObject['country_code'] = item['short_name']; 57 | } else if (item['types'].indexOf('postal_code') > -1) { 58 | locationObject['postal_code'] = item['short_name']; 59 | } 60 | } 61 | } 62 | 63 | return locationObject; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/notes/components/note-collaborators/note-collaborators.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/firestore'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { FormControl } from '@angular/forms'; 6 | 7 | import { Note } from '../../models/note.model'; 8 | import { Collaborator } from '../../models/collaborator.model'; 9 | import { AuthService } from '../../../core/auth.service'; 10 | 11 | @Component({ 12 | selector: 'app-note-collaborators', 13 | templateUrl: './note-collaborators.component.html' 14 | }) 15 | export class NoteCollaboratorsComponent implements OnInit { 16 | currentUser: any; 17 | emailFormControl = new FormControl(''); 18 | note: Note; 19 | note$: Observable; 20 | private noteId = ''; 21 | private noteDoc: AngularFirestoreDocument; 22 | 23 | constructor( 24 | private auth: AuthService, 25 | private afs: AngularFirestore, 26 | private route: ActivatedRoute, 27 | private router: Router 28 | ) { 29 | route.params.subscribe((params: Object) => this.noteId = params['id']); 30 | } 31 | 32 | async ngOnInit() { 33 | this.noteDoc = this.afs.doc(`notes/${this.noteId}`); 34 | this.note$ = this.noteDoc.snapshotChanges().map(item => { 35 | const id = item.payload.id; 36 | const data = item.payload.data(); 37 | return { id, ...data }; 38 | }); 39 | this.note$.subscribe(noteItem => { 40 | this.note = noteItem; 41 | }); 42 | 43 | this.auth.authState$.subscribe(user => { 44 | this.currentUser = user; 45 | }); 46 | } 47 | 48 | async addCollaborator(e) { 49 | const email = e.target.value.trim(); 50 | if (email.length) { 51 | const photoURL = `https://avatars.io/gravatar/${email}`; 52 | const collaborator: Collaborator = { email, photoURL, invitedBy: this.currentUser.uid }; 53 | const collaborators = [...this.note.collaborators, ...[email]]; 54 | const sharedWith = [...this.note.sharedWith, ...[collaborator]]; 55 | 56 | await this.noteDoc.update({ 57 | collaborators, 58 | sharedWith 59 | }); 60 | 61 | this.emailFormControl.reset(); 62 | } 63 | } 64 | 65 | async deleteCollaborator(collab) { 66 | const sharedWith = [...this.note.sharedWith.filter(item => item.email !== collab.email)]; 67 | const collaborators = [...this.note.collaborators.filter(email => email !== collab.email)]; 68 | 69 | await this.noteDoc.update({ 70 | collaborators, 71 | sharedWith 72 | }); 73 | } 74 | 75 | enableInvitationForm() { 76 | this.noteDoc.update({ 77 | isInvitaionFormEnabled: true 78 | }); 79 | } 80 | 81 | navigateBack() { 82 | this.router.navigate([`/note/${this.noteId}`]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/notes/components/note/note.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { MatDatepickerInputEvent, MatSnackBar } from '@angular/material'; 4 | 5 | import { Note } from '../../models/note.model'; 6 | import * as firebase from 'firebase/app'; 7 | import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/firestore'; 8 | import { Observable } from 'rxjs/Observable'; 9 | import { AuthService } from '../../../core/auth.service'; 10 | 11 | @Component({ 12 | selector: 'app-note', 13 | templateUrl: './note.component.html', 14 | styleUrls: ['./note.component.scss'] 15 | }) 16 | export class NoteComponent implements OnInit { 17 | private noteDoc: AngularFirestoreDocument; 18 | currentUser: any; 19 | noteId: string; 20 | note: Note; 21 | note$: Observable; 22 | minNoteDueDate = new Date(); 23 | 24 | constructor( 25 | private auth: AuthService, 26 | private afs: AngularFirestore, 27 | private route: ActivatedRoute, 28 | private router: Router, 29 | private snackBar: MatSnackBar 30 | ) { 31 | route.params.subscribe((params: Object) => this.noteId = params['id']); 32 | } 33 | 34 | ngOnInit() { 35 | this.noteDoc = this.afs.doc(`notes/${this.noteId}`); 36 | this.note$ = this.noteDoc.snapshotChanges().map(item => { 37 | const id = item.payload.id; 38 | const data = item.payload.data(); 39 | return { id, ...data }; 40 | }); 41 | // @Todo: ngOnDestroy to remove the subscribe 42 | this.note$.subscribe(noteItem => { 43 | this.note = noteItem; 44 | }); 45 | 46 | this.auth.authState$.subscribe(user => { 47 | this.currentUser = user; 48 | }); 49 | } 50 | 51 | async deleteNote() { 52 | const deletedNote = {...this.note}; 53 | await this.noteDoc.delete(); 54 | this.redirectToNotes(deletedNote); 55 | } 56 | 57 | navigateToPreviousPage() { 58 | this.router.navigate(['notes']); 59 | } 60 | 61 | removeDueDate() { 62 | this.noteDoc.update({ 63 | // https://firebase.google.com/docs/firestore/manage-data/delete-data 64 | dueDate: firebase.firestore.FieldValue.delete() 65 | }); 66 | } 67 | 68 | toggleNoteArchive(note) { 69 | this.noteDoc.update({ 70 | archived: !note.archived 71 | }); 72 | } 73 | 74 | updateNoteDueDate(event: MatDatepickerInputEvent) { 75 | this.noteDoc.update({ 76 | dueDate: event.value 77 | }); 78 | } 79 | 80 | updateNoteDescription(e) { 81 | if (e.target.value.trim().length) { 82 | this.noteDoc.update({ 83 | description: e.target.value 84 | }); 85 | } 86 | } 87 | 88 | updateNoteTitle(e) { 89 | if (e.target.value.trim().length) { 90 | this.noteDoc.update({ 91 | title: e.target.value 92 | }); 93 | } 94 | } 95 | 96 | private async redirectToNotes(note: Note) { 97 | await this.router.navigate(['notes']); 98 | this.snackBar.open(`"${note.title}" deleted successfully 👋`, null, { duration: 2000 }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/notes/components/note-add/note-add.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { MatSnackBar } from '@angular/material'; 5 | 6 | import * as firebase from 'firebase/app'; 7 | import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore'; 8 | 9 | import { AuthService } from '../../../core/auth.service'; 10 | import { Note } from '../../models/note.model'; 11 | import { NoteLocation } from '../../models/location.model'; 12 | 13 | @Component({ 14 | selector: 'app-note-add', 15 | templateUrl: './note-add.component.html' 16 | }) 17 | export class NoteAddComponent implements OnInit { 18 | currentUser: any; 19 | isLoading: boolean; 20 | note: Note; 21 | noteForm: FormGroup; 22 | noteLocation: NoteLocation; 23 | today: Date = new Date(); 24 | private notesCollection: AngularFirestoreCollection; 25 | 26 | constructor( 27 | private afs: AngularFirestore, 28 | private auth: AuthService, 29 | private fb: FormBuilder, 30 | private router: Router, 31 | private snackBar: MatSnackBar 32 | ) { 33 | this.noteForm = this.fb.group({ 34 | title: ['', Validators.required], 35 | description: '', 36 | dueDate: ['', { disabled: true }], 37 | location: '' 38 | }); 39 | } 40 | 41 | ngOnInit() { 42 | this.notesCollection = this.afs.collection('notes'); 43 | this.auth.authState$.subscribe(user => { 44 | this.currentUser = user; 45 | }); 46 | } 47 | 48 | async onSubmit() { 49 | if (this.noteForm.valid) { 50 | this.isLoading = true; 51 | this.note = this.prepareSaveNote(); 52 | const docRef = await this.notesCollection.add(this.note); 53 | 54 | this.redirectToNote({id: docRef.id, title: this.note.title}); 55 | } 56 | } 57 | 58 | placeSelectedHandler(location) { 59 | this.noteLocation = location; 60 | } 61 | 62 | prepareSaveNote(): Note { 63 | const userDoc = this.afs.doc(`users/${this.currentUser.uid}`); 64 | const noteModel = this.noteForm.value; 65 | 66 | const newNote = { 67 | completed: false, 68 | createdAt: firebase.firestore.FieldValue.serverTimestamp(), 69 | createdBy: userDoc.ref, 70 | isInvitaionFormEnabled: false, 71 | owner: this.currentUser.uid, 72 | photoURL: this.currentUser.photoURL, 73 | sharedWith: [{ 74 | email: this.currentUser.email, 75 | photoURL: this.currentUser.photoURL, 76 | uid: this.currentUser.uid, 77 | owner: true 78 | }], 79 | collaborators: [this.currentUser.email] 80 | }; 81 | 82 | if (this.noteLocation) { 83 | noteModel.geolocation = this.noteLocation; 84 | noteModel.geopoint = new firebase.firestore.GeoPoint(this.noteLocation.lat, this.noteLocation.lng); 85 | } 86 | 87 | return {...noteModel, ...newNote}; 88 | } 89 | 90 | private redirectToNote(doc: any): any { 91 | const snackBarRef = this.snackBar.open(`${doc.title} created successfully 🙌`, null, { 92 | duration: 2000, 93 | }); 94 | 95 | snackBarRef.afterDismissed().subscribe(() => { 96 | this.router.navigate([`note/${doc.id}`]); 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | /** 56 | * By default, zone.js will patch all possible macroTask and DomEvents 57 | * user can disable parts of macroTask/DomEvents patch by setting following flags 58 | */ 59 | 60 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 61 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 62 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 63 | 64 | /* 65 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 66 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 67 | */ 68 | // (window as any).__Zone_enable_cross_context_check = true; 69 | 70 | /*************************************************************************************************** 71 | * Zone JS is required by default for Angular itself. 72 | */ 73 | import 'zone.js/dist/zone'; // Included with Angular CLI. 74 | 75 | 76 | 77 | /*************************************************************************************************** 78 | * APPLICATION IMPORTS 79 | */ 80 | -------------------------------------------------------------------------------- /src/app/notes/components/note-add/note-add.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 10 | New note 11 | 12 | 13 | 14 | 15 | 16 | edit 17 |
18 | 19 | 26 | 27 |
28 |
29 | 30 | subject 31 |
32 | 33 | 41 | 42 |
43 |
44 | 45 | place 46 |
47 | 48 | 56 | 57 |
58 |
59 | 60 | today 61 |
62 | 69 | 70 |
71 |
72 |
73 |
74 | 75 | 81 | 86 | 87 |
88 |
-------------------------------------------------------------------------------- /src/app/core/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | 4 | import * as firebase from 'firebase/app'; 5 | import { FirebaseError } from 'firebase/app'; 6 | import { AngularFireAuth } from '@angular/fire/auth'; 7 | import { AngularFirestoreDocument, AngularFirestore } from '@angular/fire/firestore'; 8 | 9 | export enum AuthProviders { 10 | Github = 0, 11 | Twitter = 1, 12 | Facebook = 2, 13 | Google = 3, 14 | Password = 4, 15 | Anonymous = 5, 16 | Custom = 6 17 | } 18 | 19 | interface User { 20 | uid: string; 21 | email: string; 22 | photoURL?: string; 23 | displayName: string; 24 | } 25 | 26 | @Injectable() 27 | export class AuthService { 28 | public user: firebase.User; 29 | public authState$: Observable; 30 | 31 | constructor( 32 | private afAuth: AngularFireAuth, 33 | private afs: AngularFirestore 34 | ) { 35 | this.user = null; 36 | this.authState$ = afAuth.authState; 37 | 38 | this.authState$.subscribe((user: firebase.User) => { 39 | this.user = user; 40 | }); 41 | } 42 | 43 | get authenticated(): boolean { 44 | return this.user !== null; 45 | } 46 | 47 | get id(): string { 48 | return this.authenticated ? this.user.uid : null; 49 | } 50 | 51 | signIn(providerId: number): Promise { 52 | let provider: firebase.auth.AuthProvider = null; 53 | 54 | switch (providerId) { 55 | case AuthProviders.Github: 56 | provider = new firebase.auth.GithubAuthProvider(); 57 | break; 58 | case AuthProviders.Twitter: 59 | provider = new firebase.auth.TwitterAuthProvider(); 60 | break; 61 | case AuthProviders.Facebook: 62 | provider = new firebase.auth.FacebookAuthProvider(); 63 | break; 64 | case AuthProviders.Google: 65 | provider = new firebase.auth.GoogleAuthProvider(); 66 | break; 67 | } 68 | 69 | return firebase.auth() 70 | .signInWithPopup(provider) 71 | .then((result: firebase.auth.UserCredential) => { 72 | // The signed-in user info. 73 | this.user = result.user; 74 | this.updateUserData(result.user); 75 | }).catch((error: FirebaseError) => { 76 | // Handle Errors here. 77 | const errorCode = error.code; 78 | const errorMessage = error.message; 79 | 80 | if (errorCode === 'authService/account-exists-with-different-credential') { 81 | alert('You have signed up with a different provider for that email.'); 82 | // Handle linking here if your app allows it. 83 | } 84 | console.error('ERROR @ AuthService#signIn() :', error); 85 | }); 86 | } 87 | 88 | signInWithGithub(): Promise { 89 | return this.signIn(AuthProviders.Github); 90 | } 91 | 92 | signInWithTwitter(): Promise { 93 | return this.signIn(AuthProviders.Twitter); 94 | } 95 | 96 | signInWithFacebook(): Promise { 97 | return this.signIn(AuthProviders.Facebook); 98 | } 99 | 100 | signInWithGoogle(): Promise { 101 | return this.signIn(AuthProviders.Google); 102 | } 103 | 104 | signOut(): void { 105 | this.afAuth.auth.signOut(); 106 | } 107 | 108 | private updateUserData(userData) { 109 | const userRef: AngularFirestoreDocument = this.afs.doc(`users/${userData.uid}`); 110 | 111 | const data: User = { 112 | uid: userData.uid, 113 | email: userData.email, 114 | displayName: userData.displayName, 115 | photoURL: userData.photoURL 116 | }; 117 | 118 | return userRef.set(data, { merge: true }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | "app", 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | "app", 129 | "kebab-case" 130 | ], 131 | "no-output-on-prefix": true, 132 | "use-input-property-decorator": true, 133 | "use-output-property-decorator": true, 134 | "use-host-property-decorator": true, 135 | "no-input-rename": true, 136 | "no-output-rename": true, 137 | "use-life-cycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "component-class-suffix": true, 140 | "directive-class-suffix": true 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/app/notes/components/note/note.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 21 | 22 | 23 | 24 | 25 |
26 | 29 | 36 | 37 |
38 | 39 | 46 | 47 | 54 | 55 | 56 | 57 | 61 | 62 |

Note has been archived

63 |
64 | 65 | 66 | 69 | 70 | 71 | 72 | 75 | 76 | 79 | 80 | 81 | 82 |
83 |
-------------------------------------------------------------------------------- /src/assets/icons/auth/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | github_buttn 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "firelist-angular": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:browser", 14 | "options": { 15 | "outputPath": "dist", 16 | "index": "src/index.html", 17 | "main": "src/main.ts", 18 | "tsConfig": "src/tsconfig.app.json", 19 | "polyfills": "src/polyfills.ts", 20 | "assets": [ 21 | "src/assets", 22 | "src/favicon.ico", 23 | "src/manifest.json" 24 | ], 25 | "styles": [ 26 | "src/styles.scss" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "optimization": true, 33 | "outputHashing": "all", 34 | "sourceMap": false, 35 | "extractCss": true, 36 | "namedChunks": false, 37 | "aot": true, 38 | "extractLicenses": true, 39 | "vendorChunk": false, 40 | "buildOptimizer": true, 41 | "serviceWorker": true, 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | } 54 | ] 55 | }, 56 | "augury": { 57 | "fileReplacements": [ 58 | { 59 | "replace": "src/main.ts", 60 | "with": "src/main.augury.ts" 61 | } 62 | ] 63 | } 64 | } 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "browserTarget": "firelist-angular:build" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "firelist-angular:build:production" 74 | }, 75 | "augury": { 76 | "browserTarget": "firelist-angular:build:augury" 77 | } 78 | } 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "firelist-angular:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "karmaConfig": "./karma.conf.js", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "src/tsconfig.spec.json", 93 | "scripts": [], 94 | "styles": [ 95 | "src/styles.scss" 96 | ], 97 | "assets": [ 98 | "src/assets", 99 | "src/favicon.ico", 100 | "src/manifest.json" 101 | ] 102 | } 103 | }, 104 | "lint": { 105 | "builder": "@angular-devkit/build-angular:tslint", 106 | "options": { 107 | "tsConfig": [ 108 | "src/tsconfig.app.json", 109 | "src/tsconfig.spec.json" 110 | ], 111 | "exclude": [ 112 | "**/node_modules/**" 113 | ] 114 | } 115 | } 116 | } 117 | }, 118 | "firelist-angular-e2e": { 119 | "root": "e2e", 120 | "sourceRoot": "e2e", 121 | "projectType": "application", 122 | "architect": { 123 | "e2e": { 124 | "builder": "@angular-devkit/build-angular:protractor", 125 | "options": { 126 | "protractorConfig": "./protractor.conf.js", 127 | "devServerTarget": "firelist-angular:serve" 128 | } 129 | }, 130 | "lint": { 131 | "builder": "@angular-devkit/build-angular:tslint", 132 | "options": { 133 | "tsConfig": [ 134 | "e2e/tsconfig.e2e.json" 135 | ], 136 | "exclude": [ 137 | "**/node_modules/**" 138 | ] 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "defaultProject": "firelist-angular", 145 | "schematics": { 146 | "@schematics/angular:component": { 147 | "prefix": "app", 148 | "styleext": "scss" 149 | }, 150 | "@schematics/angular:directive": { 151 | "prefix": "app" 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import * as admin from 'firebase-admin'; 3 | import * as nodemailer from 'nodemailer'; 4 | import * as nodemoji from 'node-emoji'; 5 | 6 | const serviceAccount = require('../firebase-adminsdk-credentials.json'); 7 | 8 | admin.initializeApp({ 9 | credential: admin.credential.cert(serviceAccount), 10 | databaseURL: 'https://firelist-angular-dev.firebaseio.com' 11 | }); 12 | 13 | const firestore = admin.firestore(); 14 | firestore.settings({ timestampsInSnapshots: true }); 15 | // // Start writing Firebase Functions 16 | // // https://firebase.google.com/docs/functions/typescript 17 | 18 | const deleteQueryBatch = (db, query, batchSize, resolve, reject) => { 19 | query.get() 20 | .then((snapshot) => { 21 | // When there are no documents left, we are done 22 | if (snapshot.size === 0) { 23 | return 0; 24 | } 25 | 26 | // Delete documents in a batch 27 | const batch = db.batch(); 28 | snapshot.docs.forEach((doc) => { 29 | batch.delete(doc.ref); 30 | }); 31 | 32 | return batch.commit().then(() => { 33 | return snapshot.size; 34 | }); 35 | }).then((numDeleted) => { 36 | if (numDeleted === 0) { 37 | resolve(); 38 | return; 39 | } 40 | 41 | // Recurse on the next process tick, to avoid 42 | // exploding the stack. 43 | process.nextTick(() => { 44 | deleteQueryBatch(db, query, batchSize, resolve, reject); 45 | }); 46 | }) 47 | .catch(reject); 48 | } 49 | 50 | const deleteCollection = (db, collectionPath, batchSize) => { 51 | const collectionRef = db.collection(collectionPath); 52 | const query = collectionRef.orderBy('__name__').limit(batchSize); 53 | 54 | return new Promise((resolve, reject) => { 55 | deleteQueryBatch(db, query, batchSize, resolve, reject); 56 | }); 57 | } 58 | 59 | // https://firebase.google.com/docs/functions/manage-functions#modify-region 60 | // https://firebase.google.com/docs/functions/locations 61 | // @Todo: Turn on retry() and set the timeout to be very long 62 | export const deleteNoteAndTodos = functions.region('europe-west1').firestore 63 | .document('notes/{noteId}') 64 | .onDelete((snap, context) => { 65 | const noteId = context.params.noteId; 66 | 67 | const collectionTodosPath = `notes/${noteId}/todos`; 68 | console.log(`Preparing to delete Note Todos: ${collectionTodosPath}`); 69 | 70 | return deleteCollection(firestore, collectionTodosPath, 50).then(() => { 71 | console.log('Todos are gone!'); 72 | }); 73 | }); 74 | 75 | 76 | // Configure the email transport using the default SMTP transport and a GMail account. 77 | // For Gmail, enable these: 78 | // 1. https://www.google.com/settings/security/lesssecureapps 79 | // 2. https://accounts.google.com/DisplayUnlockCaptcha 80 | // 3. firebase functions:config:set gmail.email="myusername@gmail.com" gmail.password="secretpassword" 81 | // For other types of transports such as Sendgrid see https://nodemailer.com/transports/ 82 | // TODO: Configure the `gmail.email` and `gmail.password` Google Cloud environment variables. 83 | const gmailEmail = functions.config().gmail.email; 84 | const gmailPassword = functions.config().gmail.password; 85 | const mailTransport = nodemailer.createTransport({ 86 | service: 'gmail', 87 | auth: { 88 | user: gmailEmail, 89 | pass: gmailPassword, 90 | }, 91 | }); 92 | 93 | const APP_NAME = 'Firelist PWA'; 94 | // Sends an email to the given collaborator. 95 | const sendEmail = async (mailOptions) => { 96 | await mailTransport.sendMail(mailOptions); 97 | console.log('New email sent to:', mailOptions.to); 98 | return null; 99 | } 100 | 101 | // [START shareNoteViaEmail] 102 | export const shareNoteViaEmail = functions.firestore 103 | .document('notes/{noteId}') 104 | .onUpdate((change, context) => { 105 | const previousNoteValue = change.before.data(); 106 | const newNoteValue = change.after.data(); 107 | 108 | if (newNoteValue.collaborators.length > previousNoteValue.collaborators.length) { 109 | const randomEmoji = nodemoji.random(); 110 | const collaboratorEmail = newNoteValue.collaborators[newNoteValue.collaborators.length - 1]; 111 | 112 | const mailOptions = { 113 | from: `${APP_NAME} `, 114 | to: collaboratorEmail, 115 | subject: `${randomEmoji.emoji} You've received an invitation to collaborate`, 116 | text: `Hey there!\n\nGreat news! You just got invited to collaborate. Access https://firelist-angular-dev.firebaseapp.com/note/${context.params.noteId} and have fun!\n\n-----------------\n${newNoteValue.title}\n${newNoteValue.description ? `${newNoteValue.description}\n` : ''}${newNoteValue.geolocation ? newNoteValue.geolocation.formatted_address : ''}` 117 | }; 118 | 119 | return sendEmail(mailOptions); 120 | } else { 121 | return null; 122 | } 123 | }); 124 | // [END shareNoteViaEmail] -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Do not allow variables to be used before they are declared. 69 | "no-use-before-declare": true, 70 | 71 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 72 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 73 | 74 | // Disallow duplicate imports in the same file. 75 | "no-duplicate-imports": true, 76 | 77 | 78 | // -- Strong Warnings -- 79 | // These rules should almost never be needed, but may be included due to legacy code. 80 | // They are left as a warning to avoid frustration with blocked deploys when the developer 81 | // understand the warning and wants to deploy anyway. 82 | 83 | // Warn when an empty interface is defined. These are generally not useful. 84 | "no-empty-interface": {"severity": "warning"}, 85 | 86 | // Warn when an import will have side effects. 87 | "no-import-side-effect": {"severity": "warning"}, 88 | 89 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 90 | // most values and let for values that will change. 91 | "no-var-keyword": {"severity": "warning"}, 92 | 93 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 94 | "triple-equals": {"severity": "warning"}, 95 | 96 | // Warn when using deprecated APIs. 97 | "deprecation": {"severity": "warning"}, 98 | 99 | // -- Light Warnings -- 100 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 101 | // if TSLint supported such a level. 102 | 103 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 104 | // (Even better: check out utils like .map if transforming an array!) 105 | "prefer-for-of": {"severity": "warning"}, 106 | 107 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 108 | "unified-signatures": {"severity": "warning"}, 109 | 110 | // Warns if code has an import or variable that is unused. 111 | "no-unused-variable": {"severity": "warning"}, 112 | 113 | // Prefer const for values that will not change. This better documents code. 114 | "prefer-const": {"severity": "warning"}, 115 | 116 | // Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts. 117 | "trailing-comma": {"severity": "warning"} 118 | }, 119 | 120 | "defaultSeverity": "error" 121 | } 122 | -------------------------------------------------------------------------------- /src/assets/firebase-logo-built_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 68 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 88 | 92 | 97 | 102 | 107 | 111 | 112 | 113 | 114 | 118 | 120 | 122 | 123 | 125 | 127 | 129 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 2 | @import "material-theme.scss"; 3 | 4 | $break-small: 599px; 5 | 6 | body { 7 | background-color: whitesmoke; 8 | font-family: Roboto, sans-serif; 9 | margin: 0; 10 | } 11 | 12 | .app-firelist { 13 | @media screen and (max-width: $break-small) { 14 | .mat-toolbar-row, 15 | .mat-toolbar-single-row { 16 | font-size: 16px; 17 | padding: 0 6px 0 12px; 18 | } 19 | 20 | .mat-icon-button { 21 | line-height: 18px; 22 | 23 | .mat-icon, i { 24 | line-height: 18px; 25 | } 26 | } 27 | 28 | .mat-icon { 29 | height: 18px; 30 | width: 18px; 31 | } 32 | 33 | .material-icons { 34 | font-size: 18px; 35 | } 36 | 37 | app-auth { 38 | .app-empty-state { 39 | background-color: transparent; 40 | box-shadow: none; 41 | } 42 | } 43 | } 44 | 45 | .app-empty-state { 46 | text-align: center; 47 | 48 | img { 49 | margin: 30px 0 40px; 50 | } 51 | 52 | blockquote { 53 | font-size: 1.25rem; 54 | font-style: italic; 55 | margin: 0 0 20px; 56 | padding: 0 10px; 57 | } 58 | 59 | p { 60 | font-size: 14px; 61 | margin-bottom: 20px; 62 | padding: 12px; 63 | } 64 | 65 | @media screen and (max-width: $break-small) { 66 | blockquote { 67 | font-size: 1rem; 68 | } 69 | 70 | p { 71 | font-size: 12px; 72 | } 73 | } 74 | 75 | } 76 | 77 | .mat-progress-bar { 78 | &.app-loading-state { 79 | height: 2px; 80 | position: absolute; 81 | top: 0; 82 | } 83 | } 84 | 85 | .mat-fab.fab-bottom-right { 86 | position: fixed; 87 | right: 12px; 88 | bottom: 12px; 89 | } 90 | 91 | .app-avatar { 92 | border-radius: 50%; 93 | 94 | &.bordered { 95 | box-shadow: 0 1px 1px 1px rgba(0,0,0,.2); 96 | border: 1px solid #fff; 97 | } 98 | } 99 | 100 | .note-shared-with { 101 | padding: 0 10px 10px; 102 | position: relative; 103 | 104 | ul { 105 | display: inline-block; 106 | list-style: none; 107 | margin: 0; 108 | padding: 0; 109 | 110 | li { 111 | float: left; 112 | } 113 | } 114 | 115 | .app-avatar { 116 | font-style: italic; 117 | margin-right: 4px; 118 | } 119 | 120 | .note-last-updated { 121 | color: rgba(0,0,0,.1); 122 | font-style: italic; 123 | position: absolute; 124 | right: 0; 125 | top: 42px; 126 | } 127 | 128 | .mat-icon-button { 129 | border: 1px solid #eee; 130 | color: rgba(0, 0, 0, 0.4); 131 | height: 35px; 132 | line-height: 35px; 133 | width: 35px; 134 | } 135 | } 136 | 137 | app-notes-list, 138 | app-note-collaborators { 139 | .app-empty-state { 140 | background-color: #6382ff; 141 | 142 | blockquote, p { 143 | color: #FFF; 144 | } 145 | } 146 | 147 | .mat-card { 148 | .mat-card-header-text { 149 | align-items: center; 150 | display: flex; 151 | } 152 | } 153 | 154 | @media screen and (max-width: $break-small) { 155 | .mat-card-avatar { 156 | height: 30px; 157 | width: 30px; 158 | } 159 | } 160 | } 161 | 162 | app-note { 163 | @media screen and (max-width: $break-small) { 164 | .mat-card-header { 165 | .mat-card-header-text { 166 | margin: 0; 167 | } 168 | } 169 | } 170 | 171 | .mat-card-header { 172 | .mat-card-header-text { 173 | width: 100%; 174 | 175 | .mat-input-element { 176 | // accent color 177 | caret-color: #e91e63; 178 | } 179 | } 180 | } 181 | } 182 | 183 | // Todos 184 | .app-note-todos-list { 185 | background: rgba(0, 0, 0, 0.01); 186 | margin: 20px 10px; 187 | border: 1px solid #ECEFF1; 188 | 189 | .app-empty-state { 190 | img { 191 | margin: 20px 0 12px; 192 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 193 | border-radius: 50%; 194 | } 195 | 196 | blockquote { 197 | font-size: .865rem; 198 | } 199 | } 200 | 201 | .todos-progress { 202 | padding: 26px 15px 0 50px; 203 | position: relative; 204 | 205 | .todos-percentage-value { 206 | color: rgba(0,0,0,.5); 207 | display: inline-block; 208 | position: absolute; 209 | left: 12px; 210 | text-align: center; 211 | top: 22px; 212 | width: 35px; 213 | } 214 | } 215 | 216 | .mat-list.note-todos { 217 | .mat-list-item { 218 | &:first-child { 219 | margin-top: 8px; 220 | } 221 | 222 | &:last-child { 223 | margin-bottom: 8px; 224 | } 225 | 226 | &:hover { 227 | .mat-list-item-content { 228 | .action-todo-delete { 229 | visibility: visible; 230 | } 231 | } 232 | } 233 | 234 | &.todo-completed { 235 | opacity: .9; 236 | 237 | .mat-input-element { 238 | color: rgba(0, 0, 0, 0.4); 239 | text-decoration: line-through; 240 | } 241 | } 242 | 243 | .mat-list-item-content { 244 | flex-direction: row-reverse; 245 | 246 | .mat-list-text { 247 | padding-top: 1px; 248 | 249 | .mat-input-element { 250 | caret-color: #e91e63; 251 | } 252 | } 253 | 254 | .mat-checkbox { 255 | margin-right: 12px; 256 | } 257 | 258 | .action-todo-delete { 259 | position: absolute; 260 | right: 0; 261 | visibility: hidden; 262 | } 263 | } 264 | } 265 | } 266 | 267 | .note-todos-add { 268 | border-top: 1px solid rgba(0, 0, 0, 0.04); 269 | background: rgba(0,0,0,.04); 270 | 271 | .mat-list-item-content { 272 | padding: 0 12px 0 11px; 273 | 274 | .mat-list-text { 275 | padding-left: 6px !important; 276 | } 277 | } 278 | } 279 | 280 | @media screen and (max-width: $break-small) { 281 | .mat-list.note-todos { 282 | .mat-list-item { 283 | .mat-list-item-content { 284 | .mat-checkbox { 285 | .mat-checkbox-inner-container { 286 | height: 15px; 287 | width: 15px; 288 | } 289 | } 290 | 291 | .action-todo-delete { 292 | height: 34px; 293 | width: 34px; 294 | } 295 | } 296 | } 297 | } 298 | 299 | .todos-progress { 300 | padding: 16px 10px 0 44px; 301 | 302 | .todos-percentage-value { 303 | font-size: 12px; 304 | left: 10px; 305 | top: 12px; 306 | } 307 | } 308 | } 309 | } 310 | 311 | .note-field { 312 | .mat-form-field { 313 | display: block; 314 | font-size: 16px; 315 | 316 | .mat-form-field-wrapper { 317 | padding: 16px; 318 | 319 | .mat-form-field-infix { 320 | border: 0 none; 321 | margin: 0; 322 | padding: 0; 323 | } 324 | } 325 | 326 | .mat-form-field-underline { 327 | display: none; 328 | } 329 | 330 | @media screen and (max-width: $break-small) { 331 | font-size: 12px; 332 | 333 | .mat-form-field-wrapper { 334 | padding-left: 12px; 335 | padding-right: 12px; 336 | } 337 | } 338 | } 339 | } 340 | 341 | .mat-card { 342 | max-width: 80%; 343 | margin: 2em auto; 344 | padding: 0 0 24px; 345 | 346 | &.note-item { 347 | cursor: pointer; 348 | } 349 | 350 | &.has-card-actions { 351 | .mat-card-content { 352 | border-bottom: 1px solid rgba(0,0,0,.03); 353 | } 354 | 355 | .mat-card-actions { 356 | button:last-child { 357 | float: right; 358 | } 359 | } 360 | } 361 | 362 | .btn-back { 363 | position: absolute; 364 | left: -34px; 365 | top: 16px; 366 | } 367 | 368 | .mat-card-header { 369 | background-color: rgba(0,0,0,.03); 370 | padding: 24px 8px; 371 | 372 | .mat-card-title { 373 | color: rgba(0,0,0,.54); 374 | font-size: 20px; 375 | font-weight: 400; 376 | margin: 0; 377 | } 378 | } 379 | 380 | .mat-card-content { 381 | padding: 16px 0; 382 | margin-bottom: 0; 383 | 384 | > p { 385 | font-size: 16px; 386 | margin: 0; 387 | padding: 16px; 388 | } 389 | 390 | .mat-list { 391 | padding-top: 0; 392 | } 393 | 394 | .note-metadata { 395 | display: inline-flex; 396 | align-items: center; 397 | background-color: rgba(0, 0, 0, 0.1); 398 | padding: 8px 12px 6px 10px; 399 | font-size: 12px; 400 | color: #888; 401 | font-weight: lighter; 402 | position: relative; 403 | 404 | strong { 405 | font-weight: 400; 406 | } 407 | 408 | &.clean { 409 | background-color: transparent; 410 | } 411 | 412 | &.note-duedate { 413 | .mat-icon-button { 414 | right: -20px; 415 | position: absolute; 416 | top: -20px; 417 | visibility: hidden; 418 | } 419 | 420 | &:hover { 421 | .mat-icon-button { 422 | visibility: visible; 423 | } 424 | } 425 | } 426 | 427 | &.note-location-url { 428 | color: #3f51b5; 429 | a { 430 | color: inherit; 431 | text-decoration: none; 432 | } 433 | } 434 | 435 | .mat-icon { 436 | margin-right: 2px; 437 | } 438 | } 439 | } 440 | 441 | .mat-card-actions { 442 | padding-left: 24px; 443 | padding-right: 24px; 444 | 445 | &.identity-providers { 446 | .mat-button, 447 | .mat-raised-button { 448 | width: 100%; 449 | margin: 0 0 10px; 450 | padding: 0; 451 | } 452 | } 453 | } 454 | 455 | @media screen and (max-width: $break-small) { 456 | max-width: 100%; 457 | margin: 16px; 458 | 459 | .btn-back { 460 | left: -30px; 461 | top: 3px; 462 | .mat-icon { 463 | font-size: 24px; 464 | } 465 | } 466 | 467 | .mat-card-header { 468 | padding: 12px; 469 | 470 | .mat-card-title { 471 | font-size: 14px; 472 | } 473 | } 474 | 475 | .mat-card-content { 476 | padding: 0; 477 | 478 | p { 479 | font-size: 12px; 480 | padding: 12px; 481 | } 482 | } 483 | 484 | .mat-card-actions { 485 | padding-left: 8px; 486 | padding-right: 8px; 487 | 488 | .mat-button, 489 | .mat-raised-button { 490 | padding: 0 8px; 491 | margin: 0 6px; 492 | font-size: 12px; 493 | } 494 | } 495 | 496 | .mat-list, 497 | .mat-nav-list, 498 | .mat-selection-list { 499 | .mat-list-item { 500 | font-size: 12px; 501 | } 502 | } 503 | } 504 | } 505 | 506 | .mat-list { 507 | @media screen and (max-width: $break-small) { 508 | .mat-list-item { 509 | .mat-list-icon { 510 | font-size: 18px; 511 | height: 18px; 512 | width: 18px; 513 | } 514 | } 515 | } 516 | 517 | &.collaborators-list { 518 | .mat-list-item-avatar { 519 | .mat-list-icon { 520 | text-align: center; 521 | width: 32px; 522 | } 523 | } 524 | @media screen and (max-width: $break-small) { 525 | .mat-list-item-avatar { 526 | .mat-list-icon { 527 | font-size: 20px; 528 | height: 20px; 529 | padding: 0; 530 | width: 40px; 531 | } 532 | } 533 | } 534 | } 535 | 536 | .note-item-description { 537 | height: 80px; 538 | 539 | .mat-list-icon { 540 | align-self: flex-start; 541 | } 542 | } 543 | 544 | .mat-list-item:not(.form-field-item) { 545 | &:hover { 546 | background: rgba(0, 0, 0, 0.04); 547 | } 548 | } 549 | 550 | .mat-list-item-content { 551 | .mat-form-field { 552 | width: 100%; 553 | 554 | .mat-form-field-infix { 555 | top: 4px; 556 | } 557 | 558 | .mat-form-field-underline { 559 | background-color: transparent; 560 | } 561 | } 562 | } 563 | } 564 | } --------------------------------------------------------------------------------