3
\$\begingroup\$

In my endeavor to learn Angular, I've created a flashcard site with an F#/Mongo backend. As I'm new to Angular, I'm going to post everything; any and all feedback is welcome.

First, some pictures of it in action:

This is the home page. The dropdown on the left (or top) allows you to filter the displayed decks by all categories. The settings gear brings you to the edit page, and the name is a link to the viewer component. home page

This is the deck-viewer component. Hovering on the card makes it flip over. viewing a card in a deck

This is the create/edit component. creating or editing a deck

Next, my file structure:

- Flashcard.fsproj
  - /ClientApp
    - index.html
    - styles.scss
    - /src
      - /app
        - app-routing.module.ts
        - app.component.ts
        - app.component.html
        - app.module.ts
      - /create
        - create.component.ts
        - create.component.html
        - create.component.scss
        - create.module.ts
        - tag-dialog.component.ts
        - tag-dialog.component.html
      - /deck-finder
        - deck-finder.component.ts
        - deck-finder.component.html
        - deck-finder.component.scss
     - /deck-viewer
        - deck-viewer.component.ts
        - deck-viewer.component.html
        - deck-viewer.component.scss
        - deck-viewer.module.ts
     - /home
        - home.component.ts
        - home.component.html
        - home.component.scss
        - home.module.ts
     - /shared
       - /services
         - flashcardDeck.service.ts
       - /models
         - flashcard.ts
        - flashcardDeck.ts
  - /Controllers
    - FlashcardDeckController.cs

Finally, the code. In the interest of brevity, I've left out any unchanged files, such as app.component.ts. This is built with Angular 8, so don't worry about recommending any newly-introduced features.

styles.scss:

/* You can add global styles to this file, and also import other style files */

html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

@import '~@angular/material/theming';
// Plus imports for other components in your app.

// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat-core();

// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$fcd-primary: mat-palette($mat-teal);
$fcd-accent: mat-palette($mat-brown, A200, A100, A400);

// The warn palette is optional (defaults to red).
$fcd-warn: mat-palette($mat-red);

// Create the theme object (a Sass map containing all of the palettes).
$fcd-theme: mat-light-theme($fcd-primary, $fcd-accent, $fcd-warn);

// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($fcd-theme);

app-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

const routes: Routes = [
    {
        path: 'create',
        loadChildren: () => import('./create/create.module').then(m => m.CreateModule)
    },
    {
        path: 'edit/deck/:id',
        loadChildren: () => import('./create/create.module').then(m => m.CreateModule)
    },
    {
        path: 'deck/:id',
        loadChildren: () => import('./deck-viewer/deck-viewer.module').then(m => m.DeckViewerModule)
    },
    {
        path: '**',
        loadChildren: () => import('./home/home.module').then(m => m.HomeModule)
    },
];

@NgModule({
    imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
    exports: [RouterModule]
})
export class AppRoutingModule { }

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { FlexLayoutModule } from '@angular/flex-layout';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        MatButtonModule,
        MatIconModule,
        MatToolbarModule,
        AppRoutingModule,
        NoopAnimationsModule,
        FlexLayoutModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

app.component.html:

<div fxLayout="row">
  <div fxFlex="15%"></div>
  <div fxFlex>
    <mat-toolbar>
      <button mat-button [routerLink]="['']">Flashcard</button>
      <button mat-button [routerLink]="['/create']">Create</button>
    </mat-toolbar>
    <router-outlet></router-outlet>
  </div>
  <div fxFlex="15%"></div>
</div>

create.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CreateComponent } from './create.component';
import { TagDialog } from "./tag-dialog.component";
import { RouterModule } from '@angular/router';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FlashcardDeckService } from '../shared/services/flashcardDeck.service';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms'
import { CKEditorModule } from '@ckeditor/ckeditor5-angular';

@NgModule({
    declarations: [CreateComponent, TagDialog],
    imports: [
        CommonModule,
        MatButtonToggleModule,
        MatButtonModule,
        MatIconModule,
        MatCardModule,
        MatInputModule,
        MatSelectModule,
        MatDialogModule,
        FlexLayoutModule,
        HttpClientModule,
        ReactiveFormsModule,
        CKEditorModule,
        RouterModule.forChild([
            { path: '', component: CreateComponent }
        ])
    ],
    providers: [FlashcardDeckService, MatDialog],
    entryComponents: [TagDialog]
})
export class CreateModule { }

create.component.ts:

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FlashcardDeckService } from '../shared/services/flashcardDeck.service';
import { includes, trim } from 'lodash';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, FormArray } from '@angular/forms';
import * as ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import { TagDialog } from './tag-dialog.component';

@Component({
    selector: 'fcd-create',
    templateUrl: './create.component.html',
    styleUrls: ['./create.component.scss'],
    encapsulation: ViewEncapsulation.None
})
export class CreateComponent implements OnInit {
    public Editor = ClassicEditor;
    public allTags: string[] = [];
    public selectedCardId: number = 0;
    public form: FormGroup = this.fb.group({
        id: 0,
        title: '',
        cards: this.fb.array([]),
        tags: this.fb.control([])
    });

    public get cardControls() {
        return this.form.get('cards') as FormArray;
    }

    public addTagDialog(): void {
        const dialogRef = this.dialog.open(TagDialog);

        dialogRef.afterClosed().subscribe(tag => {
            let sanitizedTag = trim(tag);

            if (sanitizedTag !== undefined && sanitizedTag !== '' && !includes(this.allTags, sanitizedTag)) {
                this.allTags.push(sanitizedTag);
            }
        });
    }

    constructor(private readonly fb: FormBuilder,
        private readonly flashcardDeckService: FlashcardDeckService,
        private readonly dialog: MatDialog,
        private readonly route: ActivatedRoute) {
    }

    public setSelectedCard(ev: any) {
        this.selectedCardId = parseInt(ev.value);
    }

    public addCard() {
        let cards = this.form.get('cards') as FormArray;
        cards.push(this.fb.group({
            id: 0,
            label: '(New Card)',
            front: '',
            back: ''
        }));

        this.selectedCardId = cards.length - 1;
    }

    public deleteCard() {
        let cards = this.form.get('cards') as FormArray;
        cards.removeAt(this.selectedCardId);

        if (cards.length === 0) {
            this.selectedCardId = 0;
        } else {
            this.selectedCardId = this.selectedCardId === 0
                ? 0
                : this.selectedCardId - 1;
        }
    }

    public saveDeck() {
        this.flashcardDeckService.save(this.form.value).subscribe(data => {
            this.form = this.fb.group({
                id: data.id,
                title: data.title,
                cards: this.fb.array(data.cards.map(card => this.fb.group({
                    id: card.id,
                    label: card.label,
                    front: card.front,
                    back: card.back
                }))),
                tags: this.fb.control(data.tags)
            });
            this.allTags = data.tags;
        });
    }

    ngOnInit() {
        if (!this.route.snapshot.paramMap.has('id')) {
            let deck = this.flashcardDeckService.getNew();
            this.form = this.fb.group({
                id: deck.id,
                title: deck.title,
                cards: this.fb.array(deck.cards.map(card => this.fb.group({
                    id: card.id,
                    label: card.label,
                    front: card.front,
                    back: card.back
                }))),
                tags: this.fb.control(deck.tags)
            });
        } else {
            this.flashcardDeckService
                .get(parseInt(this.route.snapshot.paramMap.get('id')))
                .subscribe(data => {
                    this.form = this.fb.group({
                        id: data.id,
                        title: data.title,
                        cards: this.fb.array(data.cards.map(card => this.fb.group({
                            id: card.id,
                            label: card.label,
                            front: card.front,
                            back: card.back
                        }))),
                        tags: this.fb.control(data.tags)
                    });
                    this.allTags = data.tags;
                    this.selectedCardId = 0;
                });
        }
    }
}

create.component.html:

<section>
  <form [formGroup]="form" (ngSubmit)="saveDeck()">
    <mat-card>
      <mat-card-content class="card-container">
        <mat-form-field>
          <input matInput placeholder="Deck Title" formControlName="title" />
        </mat-form-field>

        <div fxLayout="row" fxLayoutGap="20px">
          <div fxFlex>
            <mat-form-field>
              <mat-label>Tags</mat-label>
              <mat-select multiple [value]="allTags" formControlName="tags">
                <mat-option *ngFor="let tag of allTags" value="{{tag}}">{{tag}}</mat-option>
              </mat-select>
            </mat-form-field>
          </div>
          <div fxFlex="130px">
            <button id="add-tag" mat-raised-button color="primary" (click)="addTagDialog()" type="button">
              <mat-icon>label</mat-icon> Create Tag
            </button>
          </div>
        </div>

        <div fxLayout="row" fxLayout.sm="column" fxLayout.lt-sm="column" fxLayoutGap="20px" fxLayoutGap.sm="0" fxLayoutGap.lt-sm="0">
          <div fxFlex="250px" fxHide fxShow.gt-sm>
            <mat-card class="flashcard-list">
              <mat-card-content>
                <div fxLayout="column">
                  <div fxFlex>
                    <mat-button-toggle-group vertical [value]="selectedCardId" (change)="setSelectedCard($event)">
                      <mat-button-toggle *ngFor="let card of form.get('cards').value; index as i" [value]="i" [checked]="i === selectedCardId">
                        {{card.label}}
                      </mat-button-toggle>
                    </mat-button-toggle-group>
                  </div>
                  <div fxFlex="36px">
                    <button mat-raised-button color="primary" id="add-card" (click)="addCard()" type="button">
                      <mat-icon>add</mat-icon> Create Card
                    </button>
                  </div>
                </div>
              </mat-card-content>
            </mat-card>
          </div>

          <div fxFlex fxHide fxShow.sm fxShow.lt-sm>
            <div fxLayout="row" class="card-select-selector">
              <div fxFlex="25px" class="menu-icon">
                <mat-icon>menu</mat-icon>
              </div>
              <div fxFlex>
                <mat-form-field>
                  <mat-label>Cards</mat-label>
                  <mat-select [value]="selectedCardId" (selectionChange)="setSelectedCard($event)">
                    <mat-option *ngFor="let card of form.get('cards').value; index as i" [value]="i">{{card.label}}</mat-option>
                  </mat-select>
                </mat-form-field>
              </div>
              <div fxFlex="130px" class="create-card-button">
                <button mat-raised-button color="primary" (click)="addCard()" type="button">
                  <mat-icon>add</mat-icon> Create Card
                </button>
              </div>
            </div>
          </div>

          <div fxFlex formArrayName="cards">
            <div *ngFor="let card of cardControls.controls; index as i" [formGroupName]="i" [ngSwitch]="selectedCardId">
              <mat-card *ngSwitchCase="i">
                <mat-card-title>{{form.get('title').value}}</mat-card-title>
                <mat-card-content>
                  <mat-form-field>
                    <input matInput placeholder="Card Label" formControlName="label" />
                  </mat-form-field>
                  <label for="card-front">Card Front</label>
                  <ckeditor [editor]="Editor" formControlName="front"></ckeditor>
                  <ckeditor [editor]="Editor" formControlName="back"></ckeditor>
                  <button mat-raised-button color="primary" id="delete-card" (click)="deleteCard()" type="button">
                    <mat-icon>delete_outline</mat-icon> Delete Card
                  </button>
                </mat-card-content>
              </mat-card>
            </div>
          </div>
        </div>
      </mat-card-content>
      <mat-card-actions>
        <button mat-raised-button color="primary" type="submit">
          <mat-icon>save_alt</mat-icon> Save
        </button>
      </mat-card-actions>
    </mat-card>
  </form>
</section>

create.component.scss:

section {
  mat-card-actions {
    margin: 0;

    &:last-child {
      margin-bottom: 0;
    }
  }

  mat-form-field {
    width: 100%;
  }

  ckeditor div {
    padding-bottom: 1.25em
  }

  .flashcard-list {
    padding: 0;

    mat-button-toggle-group {
      width: 100%;
      height: 372px;
      overflow: auto;

      mat-button-toggle button {
        text-align: left;
      }
    }

    #add-card {
      width: 100%;
    }
  }

  .card-select-selector {
    .menu-icon {
      margin: auto
    }

    .create-card-button {
      margin-left: 20px
    }
  }
}

tag-dialog.component.ts:

import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
    selector: 'tag-dialog',
    templateUrl: './tag-dialog.component.html',
})
export class TagDialog {
    constructor(public dialogRef: MatDialogRef<TagDialog>) { }
}

tag-dialog.component.html:

<h1 mat-dialog-title>Create Tag</h1>
<div mat-dialog-content>
  <mat-form-field>
    <input matInput placeholder="Tag Name" #tag />
  </mat-form-field>
</div>
<div mat-dialog-actions>
  <button mat-raised-button [mat-dialog-close]="tag.value" color="primary">Create</button>
</div>

deck-finder.component.ts:

import { Component, OnInit } from '@angular/core';
import { FlashcardDeckService } from '../shared/services/flashcardDeck.service';
import { HttpClient } from '@angular/common/http';
import { FlashcardDeck } from '../shared/models/flashcardDeck';

@Component({
    selector: 'fcd-deck-finder',
    templateUrl: './deck-finder.component.html',
    styleUrls: ['./deck-finder.component.scss']
})
export class DeckFinderComponent implements OnInit {

    constructor(private readonly flashcardDeckService: FlashcardDeckService, private readonly http: HttpClient) {
    }

    private _decks: FlashcardDeck[];
    public get decks() {
        return this._decks;
    }
    public set decks(value) {
        this._decks = value;
    }

    public categories: string[];

    public filterDecks(category: any) {
        this.flashcardDeckService.getAll(category).subscribe(data => {
            this.decks = data;
        });
    }

    ngOnInit() {
        this.flashcardDeckService.getAll().subscribe(data => {
            this.decks = data;
        });

        this.http.get<string[]>(`/FlashcardDeck/getAllCategories`).subscribe(data => {
            this.categories = data;
        });
    }
}

deck-finder.component.html:

<section>
  <mat-card>
    <mat-card-content>
      <div fxLayout="row" fxLayout.lt-sm="column" fxLayoutGap="20px" fxLayoutGap.lt-sm>
        <div fxFlex="250px" fxFlex.lt-sm>
          <mat-form-field>
            <mat-label>Categories</mat-label>
            <mat-select (selectionChange)="filterDecks($event.value)">
              <mat-option class="unfiltered">(Unfiltered)</mat-option>
              <mat-option *ngFor="let category of categories" [value]="category">
                {{category}}
              </mat-option>
            </mat-select>
          </mat-form-field>
        </div>
        <div fxLayout="row wrap" fxFlex>
          <mat-card class="deck" *ngFor="let deck of decks">
            <mat-card-content>
              <div>
                <a id="edit-deck" mat-mini-fab [routerLink]="['/edit/deck', deck.id]">
                  <mat-icon>settings</mat-icon>
                </a>
              </div>
              <div class="view-deck-container">
                <a class="view-deck" [routerLink]="['/deck', deck.id]">{{deck.title}}</a>
              </div>
            </mat-card-content>
          </mat-card>
        </div>
      </div>
    </mat-card-content>
  </mat-card>
</section>

deck-finder.component.scss:

section {
  mat-card.deck {
    height: 150px;
    width: 225px;
    margin: 5px;
    background-color: whitesmoke;

    #edit-deck {
      float: right !important;
    }

    .view-deck-container {
      text-align: center;
      padding-top: 40px;
    }

    .view-deck {
      font-weight: bold;
      font-size: 200%;
      color: sienna;
      text-decoration: underline;
    }
  }
}

deck-viewer.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { FlexLayoutModule } from '@angular/flex-layout';
import { HttpClientModule } from '@angular/common/http';

import { DeckViewerComponent } from './deck-viewer.component';
import { FlashcardDeckService } from '../shared/services/flashcardDeck.service';


@NgModule({
    declarations: [DeckViewerComponent],
    imports: [
        CommonModule,
        MatButtonModule,
        MatIconModule,
        MatCardModule,
        FlexLayoutModule,
        HttpClientModule,
        RouterModule.forChild([
            { path: '', component: DeckViewerComponent }
        ])
    ],
    providers: [FlashcardDeckService],
})
export class DeckViewerModule { }

deck-viewer.component.ts:

import { Component, OnInit } from '@angular/core';
import { FlashcardDeckService } from '../shared/services/flashcardDeck.service';
import { ActivatedRoute } from '@angular/router';
import { FlashcardDeck } from '../shared/models/flashcardDeck';
import { Flashcard } from '../shared/models/flashcard';

@Component({
    selector: 'fcd-deck-viewer',
    templateUrl: './deck-viewer.component.html',
    styleUrls: ['./deck-viewer.component.scss']
})
export class DeckViewerComponent implements OnInit {

    private _flashcardDeck: FlashcardDeck;
    public get flashcardDeck() {
        return this._flashcardDeck;
    }
    public set flashcardDeck(value) {
        this._flashcardDeck = value;
    }

    private _selectedCard: Flashcard;
    public get selectedCard() {
        return this._selectedCard;
    }
    public set selectedCard(value) {
        this._selectedCard = value;
    }

    constructor(private flashcardDeckService: FlashcardDeckService, private route: ActivatedRoute) {
    }

    public previousCard() {
        if (this.selectedCard.id === 0) {
            return;
        }

        document.getElementsByClassName('flip-container').item(0).classList.remove('hover');
        this.selectedCard = this.flashcardDeck.cards[this.selectedCard.id - 1];
    }

    public nextCard() {
        if (this.selectedCard.id === this.flashcardDeck.cards.length - 1) {
            return;
        }

        document.getElementsByClassName('flip-container').item(0).classList.remove('hover');
        this.selectedCard = this.flashcardDeck.cards[this.selectedCard.id + 1];
    }

    ngOnInit() {
        this.flashcardDeckService
            .get(parseInt(this.route.snapshot.paramMap.get('id')))
            .subscribe(data => {
                this.flashcardDeck = data;
                this.selectedCard = this.flashcardDeck.cards[0];
            });
    }
}

deck-viewer.component.html:

<section>
  <mat-card>
    <mat-card-title>{{flashcardDeck?.title}}</mat-card-title>
    <mat-card-content>
      <div class="flip-container" onclick="this.classList.toggle('hover');">
        <div class="flipper">
          <mat-card class="front" fxLayout="column">
            <div fxFlex></div>
            <div fxFlex [innerHTML]="selectedCard?.front"></div>
            <div fxFlex></div>
          </mat-card>
          <mat-card class="back" fxLayout="column">
            <div fxFlex></div>
            <div fxFlex [innerHTML]="selectedCard?.back"></div>
            <div fxFlex></div>
          </mat-card>
        </div>
      </div>
    </mat-card-content>
    <mat-card-actions>
      <button mat-raised-button color="primary" (click)="previousCard()" class="nav-prev" [disabled]="selectedCard?.id === 0">
        <mat-icon>navigate_before</mat-icon> Previous
      </button>
      <button mat-raised-button color="primary" (click)="nextCard()" class="nav-next" [disabled]="selectedCard?.id === flashcardDeck?.cards.length - 1">
        <mat-icon>navigate_next</mat-icon> Next
      </button>
    </mat-card-actions>
  </mat-card>
</section>

deck-viewer.component.scss:

section {
  mat-card-title {
    text-align: center;
  }

  mat-card-content {
    .flip-container {
      perspective: 1000px;
      margin: auto;
      margin-bottom: 0;

      .flipper {
        transition: 0.6s;
        transform-style: preserve-3d;
        position: relative;

        .front, .back {
          backface-visibility: hidden;
          position: absolute;
          top: 0;
          left: 0;
          text-align: center;
          vertical-align: middle;
          padding: 10px;
          background-color: whitesmoke;
        }

        .front {
          z-index: 2;
          transform: rotateY(0deg);
        }

        .back {
          transform: rotateY(180deg);
        }
      }
    }

    .flip-container:hover .flipper, .flip-container.hover .flipper {
      transform: rotateY(180deg);
    }

    .flip-container, .front, .back {
      width: 320px;
      height: 200px;
    }
  }

  mat-card-actions {
    width: 320px;
    margin: auto;
    padding-top: 0;

    .nav-next {
      float: right !important;
    }
  }
}

home.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeComponent } from './home.component';
import { RouterModule } from '@angular/router';
import { DeckFinderComponent } from '../deck-finder/deck-finder.component';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list'
import { MatSelectModule } from '@angular/material/select';
import { FlashcardDeckService } from '../shared/services/flashcardDeck.service';
import { HttpClientModule } from '@angular/common/http';
import { FlexLayoutModule } from '@angular/flex-layout';

@NgModule({
    declarations: [HomeComponent, DeckFinderComponent],
    imports: [
        CommonModule,
        MatCardModule,
        MatButtonModule,
        MatIconModule,
        MatGridListModule,
        MatSelectModule,
        HttpClientModule,
        FlexLayoutModule,
        RouterModule.forChild([
            { path: '', component: HomeComponent }
        ])
    ],
    providers: [FlashcardDeckService]
})
export class HomeModule { }

home.component.html:

<fcd-deck-finder></fcd-deck-finder>

flashcardDeck.ts and flashcard.ts:

import { Flashcard } from "./Flashcard";
export interface FlashcardDeck {
    id: number;
    title: string;
    cards: Flashcard[];
    tags: string[];
}
export interface Flashcard {
    id: number;
    label: string;
    front: string;
    back: string;
}

flashcardDeck.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; 
import { Observable } from 'rxjs';
import { FlashcardDeck } from '../models/flashcardDeck';

@Injectable({
    providedIn: 'root'
})
export class FlashcardDeckService {
    constructor(private http: HttpClient) { }

    getNew(): FlashcardDeck {
        return {
            id: 0,
            title: '(New Deck)',
            cards: [],
            tags: []
        }
    }

    get(id: number): Observable<FlashcardDeck> {
        return this.http.get<FlashcardDeck>(`/FlashcardDeck/getDeck/${id}`);
    }

    getAll(category?: string): Observable<FlashcardDeck[]> {
        return category === undefined
            ? this.http.get<FlashcardDeck[]>(`/FlashcardDeck/getAllDecks`)
            : this.http.get<FlashcardDeck[]>(`/FlashcardDeck/getAllDecks?category=${category}`);
    }

    save(flashcardDeck: FlashcardDeck) {
        return this.http.post<FlashcardDeck>('/FlashcardDeck/SaveDeck', flashcardDeck);
    }
}

FlashcardDeckController.fs:

namespace Flashcard.Controllers

open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Configuration
open FlashcardTypes
open MongoDB.Driver
open System.Linq

type FlashcardDeckController (configuration : IConfiguration) =
    inherit ControllerBase()

    [<HttpPost>]
    member __.SaveDeck([<FromBody>] flashcardDeck : FlashcardDeck) =
        Array.iteri (fun index (item:Flashcard) -> item.Id <- index) flashcardDeck.Cards

        let client = new MongoClient(configuration.GetConnectionString("Database"))
        let database = client.GetDatabase("Flashcards")
        let collection = database.GetCollection<FlashcardDeck>("Decks")

        if flashcardDeck.Id = 0 then
            let nextId =
                if flashcardDeck.Id = 0 && (collection.Find(Builders<FlashcardDeck>.Filter.Empty).Any() |> not) then
                    1
                else
                    collection.Find(Builders<FlashcardDeck>.Filter.Empty)
                        .SortByDescending(fun i -> (i.Id :> obj))
                        .FirstOrDefault().Id + 1

            flashcardDeck.Id <- nextId

            async {
                collection.InsertOneAsync(flashcardDeck) |> ignore
            } |> Async.Start

        else
            async {
                collection.FindOneAndReplaceAsync(Builders<FlashcardDeck>.Filter.Eq((fun deck -> deck.Id), flashcardDeck.Id), flashcardDeck) |> ignore
            } |> Async.Start

        flashcardDeck

    [<HttpGet>]
    member __.GetDeck(id : int) : FlashcardDeck =
        let client = new MongoClient(configuration.GetConnectionString("Database"))
        let database = client.GetDatabase("Flashcards")
        let collection = database.GetCollection<FlashcardDeck>("Decks")

        collection.Find(Builders<FlashcardDeck>.Filter.Eq((fun deck -> deck.Id), id)).Single()

    [<HttpGet>]
    member __.GetAllDecks(category : string) : FlashcardDeck[] =
        let client = new MongoClient(configuration.GetConnectionString("Database"))
        let database = client.GetDatabase("Flashcards")
        let collection = database.GetCollection<FlashcardDeck>("Decks")

        let filter =
            if System.String.IsNullOrEmpty(category) then
                Builders<FlashcardDeck>.Filter.Empty
            else
                Builders<FlashcardDeck>.Filter.Where(fun deck -> deck.Tags.Contains(category))

        collection.Find(filter).ToList()
        |> Seq.toArray

    [<HttpGet>]
    member __.GetAllCategories() : string[] =
        let client = new MongoClient(configuration.GetConnectionString("Database"))
        let database = client.GetDatabase("Flashcards")
        let collection = database.GetCollection<FlashcardDeck>("Decks")

        collection
            .Find(Builders<FlashcardDeck>.Filter.Empty)
            .Project(Builders<FlashcardDeck>.Projection.Expression(fun s -> s.Tags))
            .ToList()
        |> Seq.collect (fun s -> s)
        |> Seq.distinct
        |> Seq.sortBy (fun s -> s)
        |> Seq.toArray
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy