Développement d’applications d’entreprise 2

Combinez vos apprentissages des outils Angular pour ajouter les fonctionnalités ci-dessous au projet de démonstration Housing.

HTTP

Il est fréquent pour les applications de type SPA de communiquer avec un API HTTP pour accéder aux fonctionnalités serveurs et services de traitement des données. Vous devez utilisez les ressources Martha ci-dessous pour intégrer la gestion CRUDL des bâtiments(Locations).

  • Vous devez intégrer judicieusement ces fonctionnalités en considérant l'expérience pour un utilisateur connecté ET un utilisateur public, pour les items qu'il possède ou pas.
  • Assurez-vous qu'un utilisateurs ne peut pas afficher le formulaire d'un item inexistant ou ne lui appartenant pas.
  • Encadrez la saisie des données avec les validations pertinentes.

Votre travail se situe au niveau du Front-end, vous n'avez pas à modifier l'implémentation SQL sur Martha.

Ressources
angular
Formulaire
copier
import { Component } from '@angular/core';

import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatButtonModule} from '@angular/material/button';
import {MatCheckboxModule} from '@angular/material/checkbox';

@Component({
selector: 'app-location-form',
imports: [
MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule, MatCheckboxModule
],
template: `
<h1>Location</h1>
<form>
<div id="layout">
<div id="inputs">
<mat-form-field appearance="outline">
<mat-label>Name</mat-label>
<input matInput>
</mat-form-field>

<mat-form-field appearance="outline">
<mat-label>City</mat-label>
<input matInput>
</mat-form-field>

<mat-form-field appearance="outline">
<mat-label>State</mat-label>
<input matInput>
</mat-form-field>

<div class="small-inputs">
<mat-form-field appearance="outline">
<mat-label>Units</mat-label>
<input matInput type="number">
</mat-form-field>

<mat-checkbox>Wifi</mat-checkbox>

<mat-checkbox>Laundry</mat-checkbox>
</div>

<div class="buttons-all">

<button mat-button color="danger">Delete</button>

<div class="buttons-right">
<button mat-button>Cancel</button>
<button mat-flat-button>Save</button>
</div>

</div>
</div>

<div id="photo">
<img src="https://angular.dev/assets/images/tutorials/common/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg">

<mat-form-field appearance="outline">
<mat-label>Image URL</mat-label>
<input matInput type="text">
</mat-form-field>
</div>
</div>
</form>

`
,
styles: `
#layout {
display: flex;
gap: 32px;
justify-content: space-between;
}

#inputs {
flex-grow: 1;
display: flex;
flex-direction: column;
}

#photo {
display: flex;
flex-direction: column;
gap: 16px;
height: 600px;
width: 50%;
}

#photo img {
object-fit: cover;
border-radius: 4px;
width: 100%;
height: 100%;
}

.small-inputs {
display: flex;
align-items: baseline;
justify-content: space-between;
}

.mdc-label {
color: black;
}

.buttons-all {
display: flex;
justify-content: space-between;
}
.buttons-right {
display: flex;
justify-content: center;
gap: 32px;
}

@media (max-width: 890px) {
#layout {
flex-direction: column-reverse;
gap: 0px;
}

#photo {
width: 100%;
height: 300px;
}
}
`

})
export class LocationFormPage {

}
angular
Liste
copier
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';

import {MatTableModule} from '@angular/material/table';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';

@Component({
selector: 'app-locations',
imports: [
RouterModule,
MatIconModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatTableModule,
],
template: `
<div id="header">
<h1>Locations</h1>

<a mat-flat-button routerLink="/locations/new">
<mat-icon>add</mat-icon>
</a>
</div>

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>

<ng-container matColumnDef="city">
<th mat-header-cell *matHeaderCellDef> City </th>
<td mat-cell *matCellDef="let element"> {{element.city}} </td>
</ng-container>

<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> State </th>
<td mat-cell *matCellDef="let element"> {{element.state}} </td>
</ng-container>

<ng-container matColumnDef="availableUnits">
<th mat-header-cell *matHeaderCellDef> Units </th>
<td mat-cell *matCellDef="let element"> {{element.availableUnits}} </td>
</ng-container>

<ng-container matColumnDef="wifi">
<th mat-header-cell *matHeaderCellDef>Wifi</th>
<td mat-cell *matCellDef="let element">
<mat-icon class="availability" [attr.available]="element.wifi">{{ availabilityIcon(element.wifi) }}</mat-icon>
</td>
</ng-container>

<ng-container matColumnDef="laundry">
<th mat-header-cell *matHeaderCellDef> Laundry </th>
<td mat-cell *matCellDef="let element">
<mat-icon class="availability" [attr.available]="element.laundry">{{ availabilityIcon(element.laundry) }}</mat-icon>
</td>
</ng-container>

<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">
<button class="edit" mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
`
,
styles: `
#header {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 16px;
}

#header mat-icon {
margin: 0;
}

.mat-mdc-row {
background-color: white;
color: var(--mat-on-sys-secondary);
}

.mat-mdc-header-row {
color: var(--mat-on-sys-secondary);
background-color: var(--mat-sys-secondary);
}

.mat-mdc-row:hover .mat-mdc-cell {
background-color: color-mix(in lab, var(--mat-sys-secondary) 30%, transparent 100%);
}

.edit {
--color: rgb(255, 165, 0);

color: var(--color);
background-color: color-mix(in lab, var(--mat-sys-secondary) 30%, transparent 100%);

/* https://www.fusonic.net/en/blog/angular-material-customization */
--mat-icon-button-ripple-color: color-mix(in lab, var(--color) 10%, transparent 100%);
--mat-icon-button-hover-state-layer-opacity: .8;
--mat-icon-button-state-layer-color: color-mix(in lab, var(--mat-sys-secondary) 100%, transparent 100%);
}

.mat-column-actions, .mat-column-laundry, .mat-column-wifi, mat-column-availableUnits, mat-column-state {
width: 0px;
}

.mat-column-availableUnits, .mat-column-laundry, .mat-column-wifi, .mat-column-state {
text-align: center
}

.availability[available=true] {
color: green;
}

.availability[available=false] {
color: red;
}
`

})
export class LocationsPage {
displayedColumns: string[] = ['name', 'city', 'state', 'availableUnits', 'wifi', 'laundry', 'actions'];

readonly baseUrl = 'https://angular.dev/assets/images/tutorials/common';
dataSource = [
{
id: 0,
name: 'Acme Fresh Start Housing',
city: 'Chicago',
state: 'IL',
photo: `${this.baseUrl}/bernard-hermant-CLKGGwIBTaY-unsplash.jpg`,
availableUnits: 4,
wifi: true,
laundry: true,
},
{
id: 1,
name: 'A113 Transitional Housing',
city: 'Santa Monica',
state: 'CA',
photo: `${this.baseUrl}/brandon-griggs-wR11KBaB86U-unsplash.jpg`,
availableUnits: 0,
wifi: false,
laundry: true,
},
{
id: 2,
name: 'Warm Beds Housing Support',
city: 'Juneau',
state: 'AK',
photo: `${this.baseUrl}/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg`,
availableUnits: 1,
wifi: false,
laundry: false,
},
]

availabilityIcon(available: boolean | undefined): string | undefined {

if( available != undefined ) {
return available ? 'check' : 'close'
}

return undefined
}

}
sql
copier
create or replace table locations (
id int unsigned auto_increment key,
name varchar(250) not null,
city varchar(250) not null,
state char(2) not null,
photo varchar(500) not null,
available_units int unsigned,
wifi boolean,
laundry boolean,

user_email varchar(50) not null,

foreign key (user_email) references users(email)
);

insert into locations (name, city, state, photo, available_units, wifi, laundry, user_email)
values
(
'Acme Fresh Start Housing',
'Chicago',
'IL',
'https://angular.dev/assets/images/tutorials/common/bernard-hermant-CLKGGwIBTaY-unsplash.jpg',
4,
true,
true,
'a@a'
),
(
'A113 Transitional Housing',
'Santa Monica',
'CA',
'https://angular.dev/assets/images/tutorials/common/brandon-griggs-wR11KBaB86U-unsplash.jpg',
0,
false,
true,
'a@a'
),
(
'Warm Beds Housing Support',
'Juneau',
'AK',
'https://angular.dev/assets/images/tutorials/common/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg',
1,
false,
false,
'a@a'
),
(
'Homesteady Housing',
'Chicago',
'IL',
'https://angular.dev/assets/images/tutorials/common/ian-macdonald-W8z6aiwfi1E-unsplash.jpg',
1,
true,
false,
'a@a'
),
(
'Happy Homes Group',
'Gary',
'IN',
'https://angular.dev/assets/images/tutorials/common/krzysztof-hepner-978RAXoXnH4-unsplash.jpg',
1,
true,
false,
'a@a'
),
(
'Hopeful Apartment Group',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/r-architecture-JvQ0Q5IkeMM-unsplash.jpg',
2,
true,
true,
'b@b'
),
(
'Seriously Safe Towns',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/phil-hearing-IYfp2Ixe9nM-unsplash.jpg',
5,
true,
true,
'b@b'
),
(
'Hopeful Housing Solutions',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/r-architecture-GGupkreKwxA-unsplash.jpg',
2,
true,
true,
'b@b'
),
(
'Seriously Safe Towns',
'Oakland',
'CA',
'https://angular.dev/assets/images/tutorials/common/saru-robert-9rP3mxf8qWI-unsplash.jpg',
10,
false,
false,
'c@c'
),
(
'Capital Safe Towns',
'Portland',
'OR',
'https://angular.dev/assets/images/tutorials/common/webaliser-_TPTXZd9mOo-unsplash.jpg',
6,
true,
true,
'c@c'
);
json
Queries
copier
[
{
"name": "users-login",
"string": "select email from users where email = '?email' && passwd = sha2('?password', 256);",
"type": 0
},
{
"name": "users-signup",
"string": "insert into users(email, passwd) values ('?email', sha2('?password', 256));",
"type": 1
},
{
"name": "select-locations",
"string": "select * from locations where coalesce(?email, user_email) = user_email;",
"type": 0
},
{
"name": "select-location",
"string": "select * from locations where id = ?id && coalesce(?email, user_email) = user_email;",
"type": 0
},
{
"name": "delete-location",
"string": "delete from locations \r\nwhere id = ?id && user_email = \"?email\"\r\nreturning id;",
"type": 0
},
{
"name": "DO_NOT_USE_TEST_RESET",
"string": "create or replace table locations (\r\n id int unsigned auto_increment key,\r\n name varchar(250) not null,\r\n city varchar(250) not null,\r\n state char(2) not null,\r\n photo varchar(500) not null,\r\n available_units int unsigned,\r\n wifi boolean,\r\n laundry boolean,\r\n\r\n user_email varchar(50) not null,\r\n\r\n foreign key (user_email) references users(email)\r\n);\r\n\r\ninsert into locations (name, city, state, photo, available_units, wifi, laundry, user_email)\r\nvalues\r\n (\r\n 'Acme Fresh Start Housing',\r\n 'Chicago',\r\n 'IL',\r\n 'https://angular.dev/assets/images/tutorials/common/bernard-hermant-CLKGGwIBTaY-unsplash.jpg',\r\n 4,\r\n true,\r\n true,\r\n 'a@a'\r\n ),\r\n (\r\n 'A113 Transitional Housing',\r\n 'Santa Monica',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/brandon-griggs-wR11KBaB86U-unsplash.jpg',\r\n 0,\r\n false,\r\n true,\r\n 'a@a'\r\n ),\r\n (\r\n 'Warm Beds Housing Support',\r\n 'Juneau',\r\n 'AK',\r\n 'https://angular.dev/assets/images/tutorials/common/i-do-nothing-but-love-lAyXdl1-Wmc-unsplash.jpg',\r\n 1,\r\n false,\r\n false,\r\n 'a@a'\r\n ),\r\n (\r\n 'Homesteady Housing',\r\n 'Chicago',\r\n 'IL',\r\n 'https://angular.dev/assets/images/tutorials/common/ian-macdonald-W8z6aiwfi1E-unsplash.jpg',\r\n 1,\r\n true,\r\n false,\r\n 'a@a'\r\n ),\r\n (\r\n 'Happy Homes Group',\r\n 'Gary',\r\n 'IN',\r\n 'https://angular.dev/assets/images/tutorials/common/krzysztof-hepner-978RAXoXnH4-unsplash.jpg',\r\n 1,\r\n true,\r\n false,\r\n 'a@a'\r\n ),\r\n (\r\n 'Hopeful Apartment Group',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/r-architecture-JvQ0Q5IkeMM-unsplash.jpg',\r\n 2,\r\n true,\r\n true,\r\n 'b@b'\r\n ),\r\n (\r\n 'Seriously Safe Towns',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/phil-hearing-IYfp2Ixe9nM-unsplash.jpg',\r\n 5,\r\n true,\r\n true,\r\n 'b@b'\r\n ),\r\n (\r\n 'Hopeful Housing Solutions',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/r-architecture-GGupkreKwxA-unsplash.jpg',\r\n 2,\r\n true,\r\n true,\r\n 'b@b'\r\n ),\r\n (\r\n 'Seriously Safe Towns',\r\n 'Oakland',\r\n 'CA',\r\n 'https://angular.dev/assets/images/tutorials/common/saru-robert-9rP3mxf8qWI-unsplash.jpg',\r\n 10,\r\n false,\r\n false,\r\n 'c@c'\r\n ),\r\n (\r\n 'Capital Safe Towns',\r\n 'Portland',\r\n 'OR',\r\n 'https://angular.dev/assets/images/tutorials/common/webaliser-_TPTXZd9mOo-unsplash.jpg',\r\n 6,\r\n true,\r\n true,\r\n 'c@c'\r\n );",
"type": 1
},
{
"name": "insert-location",
"string": "insert into locations (name, city, state, photo, available_units, wifi, laundry, user_email) \r\nvalues (\"?name\", \"?city\", \"?state\", \"?photo\", ?units, ?wifi, ?laundry, \"?email\")\r\nreturning id;",
"type": 0
},
{
"name": "update-location",
"string": "update locations set \r\n\tname = \"?name\",\r\n\tcity = \"?city\",\r\n\tstate = \"?state\",\r\n\tphoto = \"?photo\",\r\n\tavailable_units = ?units,\r\n\twifi = ?wifi,\r\n\tlaundry = ?laundry\r\nwhere id = ?id && user_email = \"?email\";",
"type": 0
}
]
rb
Exemples queries
copier
# USAGE
# MARTHA_USERNAME='...' MARTHA_PASSWORD='...' ruby locations_martha_tests.rb

# Vim remap
# :nnoremap ;; :w <bar> :terminal ruby %<cr>
# set fallback variables at line 23

ENV["APP_ENV"] = "test"

require "bundler/inline"
require "minitest/autorun"
require "rack/test"
require "base64"

gemfile do
source "https://rubygems.org"

gem "faraday"
end

require "faraday"

USERNAME = ENV["MARTHA_USERNAME"] || "..."
PASSWORD = ENV["MARTHA_PASSWORD"] || "..."

class Test < Minitest::Test

def setup
options = { headers: { "auth" => Base64.encode64("#{USERNAME}:#{PASSWORD}") } }

@martha = Faraday.new("http://martha.jh.shawinigan.info", options) do |config|
config.request :json
config.response :json, **{ parser_options: { symbolize_names: true } }
end

q "DO_NOT_USE_TEST_RESET"
end

def q(query, params = nil)
@martha.post("/queries/#{query}/execute", params).body
end

##
### LIST
##

def test_select_all_public
r = q "select-locations", { email: nil }

assert r[:data].length == 10
end

def test_select_all_owner_a
r = q "select-locations", { email: "'a@a'" } # !!! Nested string, ' inside "

assert r[:data].length == 5
end

def test_select_all_owner_b
r = q "select-locations", { email: "'b@b'" }

assert r[:data].length == 3
end

def test_select_all_owner_invalid
r = q "select-locations", { email: "'z@z'" }

assert r[:data].length == 0
end

##
### READ
##

def test_select_one_public
r = q "select-location", { id: 1, email: nil }

assert r[:data].length == 1
end

def test_select_one_a
r = q "select-location", { id: 2, email: "'a@a'" }

assert r[:data].length == 1
end

def test_select_one_not_a
r = q "select-location", { id: 10, email: "'a@a'" }

assert r[:data].length == 0
end

def test_select_one_public_invalid_id
r = q "select-location", { id: 999, email: nil }

assert r[:data].length == 0
end

##
### CREATE
##

def test_insert_a
r = q "insert-location", { name: "new a", city: "city a", state: "SA", photo: "https://cdn-icons-png.flaticon.com/128/619/619153.png", units: 2, wifi: 1, laundry: 0, email: "a@a" }

assert r[:data].length == 1

r = q "select-locations", { email: "'a@a'" }

assert r[:data].length == 6
end

##
### UPDATE
##

def test_update_a
r = q "update-location", { id: 2, name: "new a2", city: "city a2", state: "A2", photo: "https://cdn-icons-png.flaticon.com/128/619/619153.png", units: 4, wifi: 0, laundry: 0, email: "a@a" }

assert r[:success]

r = q "select-location", { id: 2, email: nil }

assert r[:data].first[:name] == "new a2"
end

##
### DELETE
##

def test_delete_1_a
r = q "delete-location", { id: 1, email: "a@a" }

assert r[:data].length == 1
end

def test_delete_10_not_a
r = q "delete-location", { id: 10, email: "a@a" }

assert r[:data].length == 0
end

def test_delete_1_invalid_email
r = q "delete-location", { id: 1, email: "z@z" }

assert r[:data].length == 0
end

end

Routing

Solidifiez les mécanismes de navigation en ajoutant

  • Une route wildcard qui affiche une page représentant une ressource inexistante
  • Les guards pertinents pour empêcher un utilisateur connecté de retourner à la connexion ET l'inscription. Également restreindre l'accès aux pages de gestion des Locations aux utilisateurs authentifiés.

Internationalisation

Exploitez la librairie ngx-translate pour implémenter la traduction en 2 langues(FR et EN) du formulaire Location.

  • Offrez un mécanisme à l'utilisateur pour changer de langue
  • Bien qu'une seule page sera traduite, intégrez le mécanisme de façon judicieuse et persistente entre les rechargements de page.

Déploiement

Mettre en ligne la version production de votre application sur votre micro-serveur.

Remise

Vous devez présenter une démonstration fonctionnelle de votre implémentation avant vendredi 31 janvier @ 15h

Développement d’applications d’entreprise 2

Nom:

Appréciation générale
UI 0     -0.5     -1     -2
UX 0     -0.5     -1     -2
Code 0     -0.5     -1     -2
Séparations judicieuses des responsabilités 0     -0.5     -1     -2
HTTP
Intégration UI/UX, Navigation, Connecté vs Public, Liste, Formulaire 2     1.5     1     0
C   (R)U   D   L   Locations 3     2.5     2     1     0
Validations, restrictions 2     1.5     1     0
Routes
Wildcard 1     0.5     0
Guards 2     1.5     1     0
Traduction
Intégration ngx-translate exhaustive, Formulaire 2     1.5     1     0
Persistente 1     0.5     0
Déploiement
Production en ligne 1     0.5     0