1
0

Update interface for simplify view and publish to Tramontana Website

This commit is contained in:
Siroco 2019-07-20 18:57:52 +02:00
parent 5e510cb1df
commit d057e11dac
14 changed files with 203 additions and 33 deletions

View File

@ -21,6 +21,7 @@
"src/favicon.ico" "src/favicon.ico"
], ],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css" "src/styles.css"
], ],
"scripts": [] "scripts": []
@ -71,6 +72,7 @@
"tsConfig": "src/tsconfig.spec.json", "tsConfig": "src/tsconfig.spec.json",
"scripts": [], "scripts": [],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css" "src/styles.css"
], ],
"assets": [ "assets": [

30
package-lock.json generated
View File

@ -236,6 +236,23 @@
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }
}, },
"@angular/cdk": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-7.3.7.tgz",
"integrity": "sha512-xbXxhHHKGkVuW6K7pzPmvpJXIwpl0ykBnvA2g+/7Sgy5Pd35wCC+UtHD9RYczDM/mkygNxMQtagyCErwFnDtQA==",
"requires": {
"parse5": "^5.0.0",
"tslib": "^1.7.1"
},
"dependencies": {
"parse5": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz",
"integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==",
"optional": true
}
}
},
"@angular/cli": { "@angular/cli": {
"version": "7.3.9", "version": "7.3.9",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-7.3.9.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-7.3.9.tgz",
@ -370,6 +387,14 @@
"integrity": "sha512-Ig5Jr7mnDelaZvSbUd9YhI5am3q1ku9xelAuwvtyDKvQJeKQj3BtTagcOgWrnQBfrJ/FsA/M5Zo48ncSsV0tqQ==", "integrity": "sha512-Ig5Jr7mnDelaZvSbUd9YhI5am3q1ku9xelAuwvtyDKvQJeKQj3BtTagcOgWrnQBfrJ/FsA/M5Zo48ncSsV0tqQ==",
"dev": true "dev": true
}, },
"@angular/material": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-7.3.7.tgz",
"integrity": "sha512-Eq+7frkeNGkLOfEtmkmJgR+AgoWajOipXZWWfCSamNfpCcPof82DwvGOpAmgGni9FuN2XFQdqP5MoaffQzIvUA==",
"requires": {
"tslib": "^1.7.1"
}
},
"@angular/platform-browser": { "@angular/platform-browser": {
"version": "7.2.15", "version": "7.2.15",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.15.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.15.tgz",
@ -4388,6 +4413,11 @@
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
"dev": true "dev": true
}, },
"hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE="
},
"handle-thing": { "handle-thing": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",

View File

@ -12,16 +12,19 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "7.2.15", "@angular/animations": "^7.2.15",
"@angular/cdk": "^7.3.7",
"@angular/common": "7.2.15", "@angular/common": "7.2.15",
"@angular/compiler": "7.2.15", "@angular/compiler": "7.2.15",
"@angular/core": "7.2.15", "@angular/core": "7.2.15",
"@angular/forms": "7.2.15", "@angular/forms": "7.2.15",
"@angular/http": "7.2.15", "@angular/http": "7.2.15",
"@angular/material": "^7.3.7",
"@angular/platform-browser": "7.2.15", "@angular/platform-browser": "7.2.15",
"@angular/platform-browser-dynamic": "7.2.15", "@angular/platform-browser-dynamic": "7.2.15",
"@angular/router": "7.2.15", "@angular/router": "7.2.15",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"hammerjs": "^2.0.8",
"rxjs": "^6.5.2", "rxjs": "^6.5.2",
"tslib": "^1.9.0", "tslib": "^1.9.0",
"zone.js": "^0.8.29" "zone.js": "^0.8.29"

View File

@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { MatSelectModule,MatStepperModule,MatCheckboxModule,MatPaginatorModule,MatFormFieldModule,MatTabsModule, MatDialogModule, MatProgressBarModule, MatInputModule, MatToolbarModule,MatMenuModule, MatTableModule, MatCardModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule, } from '@angular/material';
const materialModules = [
MatTabsModule,
MatToolbarModule,
MatCardModule,
MatButtonModule,
MatTableModule,
MatSidenavModule,
MatIconModule,
MatListModule,
MatMenuModule,
MatInputModule,
MatFormFieldModule,
MatProgressBarModule,
MatDialogModule,
MatPaginatorModule,
MatCheckboxModule,
MatStepperModule,
MatSelectModule,
];
@NgModule({
imports: materialModules,
exports: materialModules
})
export class AppMaterialModule { }

View File

@ -1,12 +1,13 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import {AppMaterialModule} from './app-material.module';
import {WordpressService} from './wordpress.service'; import {WordpressService} from './wordpress.service';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { GridComponent } from './grid/grid.component'; import { GridComponent } from './grid/grid.component';
import {SlicePipe } from '@angular/common'; import {SlicePipe } from '@angular/common';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
@NgModule({ @NgModule({
@ -16,7 +17,10 @@ import {SlicePipe } from '@angular/common';
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule,
HttpClientModule, HttpClientModule,
FormsModule,
AppMaterialModule,
], ],
providers: [ providers: [
WordpressService, WordpressService,

View File

@ -11,14 +11,22 @@ export class GeojsonService {
constructor(private http: HttpClient, constructor(private http: HttpClient,
private slicePipe:SlicePipe) { private slicePipe:SlicePipe) {
this.items = soinumapa.features as JSON let items = soinumapa.features as any[]
// only gizartea (sociedad)
this.items = items.filter( (i) => {
if (i.properties) {
let category = i.properties.category
if (category==="Gizartea") { return true }
}
return false
})
this.total_pages this.total_pages
let pages = Object.keys(this.items).length/this.per_page let pages = Object.keys(this.items).length/this.per_page
if (pages%1!=0) pages=Math.floor(pages)+1 if (pages%1!=0) pages=Math.floor(pages)+1
this.total_pages = pages this.total_pages = pages
} }
items:JSON items:any[]
per_page:number=10 per_page:number=10
total_pages:number=0 total_pages:number=0
@ -29,8 +37,19 @@ export class GeojsonService {
let page = feed.page let page = feed.page
let start:number = (page-1)*this.per_page let start:number = (page-1)*this.per_page
let end:number = (page-1)*this.per_page+this.per_page let end:number = (page-1)*this.per_page+this.per_page
return this.slicePipe.transform(this.items, start, end); let items:any[] = this.items
// return a.slice( start, end)
if (feed.search) {
items = this.items.filter( (i) => {
if (i.properties) {
let title = i.properties.title
if (title.toLowerCase().search(feed.search.toLowerCase())>=0) return true
return false
} else { return false }
})
}
// if (feed.search!='') items = items.filter( (i) => {i.title.search(feed.search)})
return this.slicePipe.transform(items, start, end);
} }
getCount():any { getCount():any {

View File

@ -1,17 +1,22 @@
<ul class="feeds" style="display:none;"> <!-- <ul class="feeds" style="display:none;">
<li *ngFor="let feed of feeds"> <li *ngFor="let feed of feeds">
<span class="feeder blink" [ngClass]="{'active':feed.status}"><span *ngIf="!feed.status">Loading</span> {{feed.name}} <span style="display:none">[{{feed.page}}/{{feed.total_pages}}]</span></span> <span class="feeder blink" [ngClass]="{'active':feed.status}"><span *ngIf="!feed.status">Loading</span> {{feed.name}} <span style="display:block">({{feed.page}}/{{feed.total_pages}})</span></span>
</li> </li>
</ul> </ul> -->
<ul class="counter"> <!-- <ul class="counter">
<p><span class="big-number">{{items.length}}/{{countItems}}</span></p> <p><span class="big-number">{{items.length}}/{{countItems}}</span></p>
</ul> </ul> -->
<!-- <ul><h2 style="cursor:pointer;" (click)="showCategories=!showCategories">Categories</h2></ul> <!-- <ul><h2 style="cursor:pointer;" (click)="showCategories=!showCategories">Categories</h2></ul>
<ul *ngIf="showCategories"> <ul *ngIf="showCategories">
<li *ngFor="let term of terms"><div [id]="term.id" class="term" (click)="filterByTerm(term.id)">{{term.name}}</div></li> <li *ngFor="let term of terms"><div [id]="term.id" class="term" (click)="filterByTerm(term.id)">{{term.name}}</div></li>
</ul> --> </ul> -->
<!-- <ul>
<label>Search</label>
<input type="text" name="search" [(ngModel)]="search">
<button type="button" name="search_button" value="Search" (click)="updateSearch()">Search</button>
</ul> -->
<ul> <ul>
<li *ngIf="items.length<=0">Loading</li> <li *ngIf="items.length<=0">Loading</li>
<li class="item" *ngFor="let item of items" style="cursor:pointer;" [id]="item.id" [ngClass]="item.repositorySlug" [title]="item.title"> <li class="item" *ngFor="let item of items" style="cursor:pointer;" [id]="item.id" [ngClass]="item.repositorySlug" [title]="item.title">
@ -27,7 +32,42 @@
<li class="more" (click)="showMore()">Show mores</li> <li class="more" (click)="showMore()">Show mores</li>
</ul> </ul>
<ul class="footer"> <!-- <ul class="footer">
<ul><li *ngFor="let feed of feeds" style="text-align:right;font-size:2em;" [ngClass]="feed.slug"><span class="feeder blink" [ngClass]="{'active':feed.status}"><span *ngIf="!feed.status">Loading</span> {{feed.name}} <span style="display:none">[{{feed.page}}/{{feed.total_pages}}]</span></span></li></ul> <ul><li *ngFor="let feed of feeds" style="text-align:right;font-size:2em;" [ngClass]="feed.slug"><span class="feeder blink" [ngClass]="{'active':feed.status}"><span *ngIf="!feed.status">Loading</span> {{feed.name}} <span style="display:block">({{feed.page}}/{{feed.total_pages}})</span></span></li></ul>
<p style="text-align:right;width:100%;font-size:0.8em;font-weight:bold;">Tramontana Archive - <a href="https://git.audio-lab.org/lrullo/tau_frontend">Fork me</a></p> <p style="text-align:right;width:100%;font-size:0.8em;font-weight:bold;">Tramontana Archive - <a href="https://git.audio-lab.org/lrullo/tau_frontend">Fork me</a></p>
</ul> </ul> -->
<!--
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field> -->
<div class="mat-elevation-z8">
<!-- <mat-table [dataSource]="dataSource"> -->
<!-- <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> No. </th>
<td mat-cell *matCellDef="let element"> {{element.id}} </td>
</ng-container> -->
<!-- <ng-container matColumnDef="date">
<th mat-header-cell *matHeaderCellDef> Date. </th>
<td mat-cell *matCellDef="let element"> {{element.date | date}} </td>
</ng-container> -->
<!-- <ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef> Title. </th>
<td mat-cell *matCellDef="let element"> {{element.title}} </td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef> Description. </th>
<td mat-cell *matCellDef="let element"> {{element.description}} </td>
</ng-container> -->
<!-- <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions. </th>
<td mat-cell *matCellDef="let element"> <button>Actions</button> </td>
</ng-container> -->
<!-- <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element"> {{element.title}} </td>
</ng-container> -->
<!-- <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> -->
<!-- </mat-table> -->
<!-- <mat-paginator [pageSizeOptions]="[5]"></mat-paginator> -->
</div>

View File

@ -1,9 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { WordpressService } from '../wordpress.service'; import { WordpressService } from '../wordpress.service';
import { OmekaClassicService } from '../omeka-classic.service'; import { OmekaClassicService } from '../omeka-classic.service';
import { GeojsonService } from '../geojson.service'; import { GeojsonService } from '../geojson.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Item } from '../item'; import { Item } from '../item';
import {MatPaginator} from '@angular/material/paginator';
import {MatTableDataSource} from '@angular/material/table';
// import {MatTableDataSource} from '@angular/material';
@Component({ @Component({
@ -16,30 +19,35 @@ export class GridComponent implements OnInit {
/* DEBEMOS CREAR UN PROXY CON NGINX PARA HACER LECTURAS Y EVITAR CORS ..*/ /* DEBEMOS CREAR UN PROXY CON NGINX PARA HACER LECTURAS Y EVITAR CORS ..*/
/* DEBEMOS CREAR UN ITEM TYPE BASICO PARA TODOS Y ASÍ CARGAR LOS DATOS DE MANERA MÁS CONTROLADA SEGUN EL CONTENT MANAGER */ /* DEBEMOS CREAR UN ITEM TYPE BASICO PARA TODOS Y ASÍ CARGAR LOS DATOS DE MANERA MÁS CONTROLADA SEGUN EL CONTENT MANAGER */
posts$:Observable<any[]>;
// @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatPaginator) paginator: MatPaginator;
search:string = ""
items:Item[]=[]; items:Item[]=[];
feeds = [ feeds = [
{type:"wordpress", categories:"https://www.gransassolagaich.it/wp-json/wp/v2/categories", url:"https://www.gransassolagaich.it/wp-json/wp/v2/posts?_embed",slug:'bambun',name:"Bambun",status:-1,per_page:10,page:1,total_pages:0}, {type:"wordpress", search:"", categories:"https://www.gransassolagaich.it/wp-json/wp/v2/categories", url:"https://www.gransassolagaich.it/wp-json/wp/v2/posts?_embed",slug:'bambun',name:"Bambun",status:-1,per_page:10,page:1,total_pages:0},
{type:"wordpress", categories:"https://www.archive.binauralmedia.org/wp-json/wp/v2/categories", url:"https://www.archive.binauralmedia.org/wp-json/wp/v2/avada_portfolio?_embed",slug:'binauralnodar',name:"Binaural",status:-1,per_page:10,page:1,total_pages:0}, // {type:"wordpress", search:"", categories:"https://www.archive.binauralmedia.org/wp-json/wp/v2/categories", url:"https://www.archive.binauralmedia.org/wp-json/wp/v2/avada_portfolio?_embed",slug:'binauralnodar',name:"Binaural",status:-1,per_page:10,page:1,total_pages:0},
// {type:"wordpress", url:"http://backend.industriapaisaia.eus/wp-json/wp/v2/posts?_embed",name:"Local",status:0}, // {type:"wordpress", url:"http://backend.industriapaisaia.eus/wp-json/wp/v2/posts?_embed",name:"Local",status:0},
{type:"geojson", categories:"",url:"http://www.soinumapa.net/geojson/",name:"Audiolab",slug:"soinumapa",status:0,per_page:10,page:1,total_pages:0}, {type:"geojson", search:"", categories:"",url:"http://www.soinumapa.net/geojson/",name:"Audiolab",slug:"soinumapa",status:0,per_page:20,page:1,total_pages:0},
{type:"omeka", categories:"http://oralitat", url:"https://tramontana.audio-lab.org/oralitatdegasconha/",name:"Oralitat de Gasconha",slug:'oralitatgasconha',status:-1,per_page:10,page:1,total_pages:0}, {type:"omeka", search:"", categories:"http://oralitat", url:"https://tramontana.audio-lab.org/oralitatdegasconha/",name:"Oralitat de Gasconha",slug:'oralitatgasconha',status:-1,per_page:10,page:1,total_pages:0},
] ]
// feedsCategories = [
// {type:"wordpress", url:"https://www.gransassolagaich.it/wp-json/wp/v2/categories",name:"Bambun",status:0},
// {type:"wordpress", url:"https://www.archive.binauralmedia.org/wp-json/wp/v2/categories",name:"Binaural",status:0},
// // {type:"wordpress", url:"http://backend.industriapaisaia.eus/wp-json/wp/v2/categories",name:"Local",status:0},
// {type:"omeka", url:"http://oralitat/",name:"Oralitat de Gasconha",status:0}
// ]
terms:any[]=[]; terms:any[]=[];
countItems:number = 0; countItems:number = 0;
displayedColumns: string[] = ['title','description'];
dataSource = new MatTableDataSource<Item>(this.items);
constructor( constructor(
private wService:WordpressService, private wService:WordpressService,
private oService:OmekaClassicService, private oService:OmekaClassicService,
private gService:GeojsonService, private gService:GeojsonService,
) { } ) { }
AfterViewInit() {
}
ngOnInit() { ngOnInit() {
//this.posts$ = this.wService.getPosts() //this.posts$ = this.wService.getPosts()
// .subscribe( // .subscribe(
@ -48,6 +56,7 @@ export class GridComponent implements OnInit {
// ) // )
this.loadFeeds(); this.loadFeeds();
this.loadCategories(); this.loadCategories();
this.dataSource.paginator = this.paginator;
} }
loadCategories() { loadCategories() {
@ -85,6 +94,16 @@ export class GridComponent implements OnInit {
) )
} }
updateSearch() {
this.feeds.map(
(feed) => {
feed.search = this.search
feed.total_pages = 0
}
)
this.loadFeeds()
}
showMore() { showMore() {
// nextpage // nextpage
this.feeds.map( this.feeds.map(
@ -100,6 +119,8 @@ export class GridComponent implements OnInit {
} }
loadFeeds(){ loadFeeds(){
this.items = []
this.countItems = 0
this.feeds.map( this.feeds.map(
(feed) => { this.loadFeed(feed) } (feed) => { this.loadFeed(feed) }
); );
@ -120,6 +141,8 @@ export class GridComponent implements OnInit {
feed.status = 1 feed.status = 1
res.body.map( res.body.map(
(i) => { (i) => {
let thumbnail = ""
if (i['_embedded']['wp:featuredmedia']) thumbnail = i['_embedded']['wp:featuredmedia'][0]['media_details']['sizes']['thumbnail']['source_url']
let item:Item = { let item:Item = {
id:feed.slug+"_"+i['id'], id:feed.slug+"_"+i['id'],
repository:feed.name, repository:feed.name,
@ -127,7 +150,7 @@ export class GridComponent implements OnInit {
title:i.title.rendered, title:i.title.rendered,
description:i.excerpt.rendered, description:i.excerpt.rendered,
link:i.link, link:i.link,
thumbnail:i['_embedded']['wp:featuredmedia'][0]['media_details']['sizes']['thumbnail']['source_url'] thumbnail:thumbnail
} }
this.items.push(item) this.items.push(item)
} }
@ -186,7 +209,7 @@ export class GridComponent implements OnInit {
repositorySlug:feed.slug, repositorySlug:feed.slug,
title:i['element_texts'][0]['text'], title:i['element_texts'][0]['text'],
description:subjects.toString(), description:subjects.toString(),
link:feed.url+"items/show/"+i['id'], link:"http://oralitatdegasconha.net/culturaviva/items/show/"+i['id'],
thumbnail:'http://oralitatdegasconha.net/culturaviva/files/theme_uploads/577dc3ddb797af39c4d39d324893d767.jpg' thumbnail:'http://oralitatdegasconha.net/culturaviva/files/theme_uploads/577dc3ddb797af39c4d39d324893d767.jpg'
} }
this.items.push(item) this.items.push(item)

View File

@ -23,8 +23,16 @@ export class OmekaClassicService {
}); });
} }
// TODO: Upgrade omeka to +2.5 ...
getItems(feed):Observable<HttpResponse<any[]>> { getItems(feed):Observable<HttpResponse<any[]>> {
return this.http.get<any>(feed.url+this.itemsUrl+"?per_page="+feed.per_page+"&page="+feed.page, { return this.http.get<any>(feed.url+this.itemsUrl, {
//+"?per_page="+feed.per_page+"&page="+feed.page
params: {
per_page:feed.per_page,
page:feed.page,
// search:feed.search, // v+2.5
},
observe: 'response', observe: 'response',
}) })
} }

View File

@ -12,6 +12,7 @@ export class WordpressService {
params: { params: {
per_page: feed.per_page, per_page: feed.per_page,
page: feed.page, page: feed.page,
search: feed.search,
}, },
observe: 'response', observe: 'response',
}); });

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@ -1,3 +1,4 @@
import 'hammerjs';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

View File

@ -1 +1,9 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
/* @import '~@angular/material/theming';
@include mat-core(); */
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }