From: Chocobozzz Date: Fri, 3 Jun 2016 20:08:03 +0000 (+0200) Subject: First draft to use webpack instead of systemjs X-Git-Tag: v0.0.1-alpha~882 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=4a6995be18b15de1834a39c8921a0e4109671bb6;p=oweals%2Fpeertube.git First draft to use webpack instead of systemjs --- diff --git a/client/.bootstraprc b/client/.bootstraprc new file mode 100644 index 000000000..a4d4fe689 --- /dev/null +++ b/client/.bootstraprc @@ -0,0 +1,125 @@ +--- +# Output debugging info +# loglevel: debug + +# Major version of Bootstrap: 3 or 4 +bootstrapVersion: 3 + +# If Bootstrap version 3 is used - turn on/off custom icon font path +useCustomIconFontPath: true + +# Webpack loaders, order matters +styleLoaders: + - style + - css + - sass + +# Extract styles to stand-alone css file +# Different settings for different environments can be used, +# It depends on value of NODE_ENV environment variable +# This param can also be set in webpack config: +# entry: 'bootstrap-loader/extractStyles' +extractStyles: false +# env: +# development: +# extractStyles: false +# production: +# extractStyles: true + +# Customize Bootstrap variables that get imported before the original Bootstrap variables. +# Thus original Bootstrap variables can depend on values from here. All the bootstrap +# variables are configured with !default, and thus, if you define the variable here, then +# that value is used, rather than the default. However, many bootstrap variables are derived +# from other bootstrap variables, and thus, you want to set this up before we load the +# official bootstrap versions. +# For example, _variables.scss contains: +# $input-color: $gray !default; +# This means you can define $input-color before we load _variables.scss +preBootstrapCustomizations: ./src/sass/pre-customizations.scss + +# This gets loaded after bootstrap/variables is loaded and before bootstrap is loaded. +# A good example of this is when you want to override a bootstrap variable to be based +# on the default value of bootstrap. This is pretty specialized case. Thus, you normally +# just override bootrap variables in preBootstrapCustomizations so that derived +# variables will use your definition. +# +# For example, in _variables.scss: +# $input-height: (($font-size-base * $line-height) + ($input-padding-y * 2) + ($border-width * 2)) !default; +# This means that you could define this yourself in preBootstrapCustomizations. Or you can do +# this in bootstrapCustomizations to make the input height 10% bigger than the default calculation. +# Thus you can leverage the default calculations. +# $input-height: $input-height * 1.10; +# bootstrapCustomizations: ./app/styles/bootstrap/customizations.scss + +# Import your custom styles here. You have access to all the bootstrap variables. If you require +# your sass files separately, you will not have access to the bootstrap variables, mixins, clases, etc. +# Usually this endpoint-file contains list of @imports of your application styles. +appStyles: ./src/sass/application.scss + +### Bootstrap styles +styles: + + # Mixins + mixins: true + + # Reset and dependencies + normalize: true + print: true + glyphicons: true + + # Core CSS + scaffolding: true + type: true + code: false + grid: true + tables: true + forms: true + buttons: true + + # Components + component-animations: false + dropdowns: true + button-groups: true + input-groups: true + navs: false + navbar: false + breadcrumbs: false + pagination: true + pager: false + labels: false + badges: false + jumbotron: false + thumbnails: true + alerts: false + progress-bars: true + media: true + list-group: false + panels: false + wells: false + responsive-embed: false + close: false + + # Components w/ JavaScript + modals: false + tooltip: false + popovers: false + carousel: false + + # Utility classes + utilities: true + responsive-utilities: true + +### Bootstrap scripts +scripts: + transition: false + alert: false + button: false + carousel: false + collapse: false + dropdown: false + modal: false + tooltip: false + popover: false + scrollspy: false + tab: false + affix: false diff --git a/client/.gitignore b/client/.gitignore index 81e4a1cf7..b20541002 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,8 +1,2 @@ -typings -app/**/*.js -app/**/*.map -app/**/*.css -stylesheets/index.css -bundles -main.js -main.js.map +dist/ +typings/ diff --git a/client/app/app.component.html b/client/app/app.component.html deleted file mode 100644 index 48e97d523..000000000 --- a/client/app/app.component.html +++ /dev/null @@ -1,60 +0,0 @@ -
- -
-
-

PeerTube

-
- -
- -
-
- - -
- - -
-
- - Login - Logout -
-
- -
-
- - Get videos -
- - -
- -
-
- - Make friends -
- -
- - Quit friends -
-
-
- -
- -
- -
- - -
- PeerTube, CopyLeft 2015-2016 -
-
diff --git a/client/app/app.component.scss b/client/app/app.component.scss deleted file mode 100644 index e02c2d5b0..000000000 --- a/client/app/app.component.scss +++ /dev/null @@ -1,32 +0,0 @@ -header div { - line-height: 25px; - margin-bottom: 30px; -} - -menu { - min-height: 600px; - margin-right: 20px; - border-right: 1px solid rgba(0, 0, 0, 0.2); - - .panel_button { - margin: 8px; - cursor: pointer; - transition: margin 0.2s; - - &:hover { - margin-left: 15px; - } - - a { - color: #333333; - } - } - - .glyphicon { - margin: 5px; - } -} - -.panel_block:not(:last-child) { - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} diff --git a/client/app/app.component.ts b/client/app/app.component.ts deleted file mode 100644 index 94924a47a..000000000 --- a/client/app/app.component.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Component } from '@angular/core'; -import { HTTP_PROVIDERS } from '@angular/http'; -import { RouteConfig, Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated'; - -import { FriendService } from './friends/index'; -import { LoginComponent } from './login/index'; -import { - AuthService, - AuthStatus, - Search, - SearchComponent -} from './shared/index'; -import { - VideoAddComponent, - VideoListComponent, - VideoWatchComponent, - VideoService -} from './videos/index'; - -@RouteConfig([ - { - path: '/users/login', - name: 'UserLogin', - component: LoginComponent - }, - { - path: '/videos/list', - name: 'VideosList', - component: VideoListComponent, - useAsDefault: true - }, - { - path: '/videos/watch/:id', - name: 'VideosWatch', - component: VideoWatchComponent - }, - { - path: '/videos/add', - name: 'VideosAdd', - component: VideoAddComponent - } -]) - -@Component({ - selector: 'my-app', - templateUrl: 'client/app/app.component.html', - styleUrls: [ 'client/app/app.component.css' ], - directives: [ ROUTER_DIRECTIVES, SearchComponent ], - providers: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService ] -}) - -export class AppComponent { - choices = []; - isLoggedIn: boolean; - - constructor( - private authService: AuthService, - private friendService: FriendService, - private router: Router - ) { - this.isLoggedIn = this.authService.isLoggedIn(); - - this.authService.loginChangedSource.subscribe( - status => { - if (status === AuthStatus.LoggedIn) { - this.isLoggedIn = true; - } - } - ); - } - - onSearch(search: Search) { - if (search.value !== '') { - const params = { - field: search.field, - search: search.value - }; - this.router.navigate(['VideosList', params]); - } else { - this.router.navigate(['VideosList']); - } - } - - logout() { - // this._authService.logout(); - } - - makeFriends() { - this.friendService.makeFriends().subscribe( - status => { - if (status === 409) { - alert('Already made friends!'); - } else { - alert('Made friends!'); - } - }, - error => alert(error) - ); - } - - quitFriends() { - this.friendService.quitFriends().subscribe( - status => { - alert('Quit friends!'); - }, - error => alert(error) - ); - } -} diff --git a/client/app/friends/friend.service.ts b/client/app/friends/friend.service.ts deleted file mode 100644 index d3684f08d..000000000 --- a/client/app/friends/friend.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Http, Response } from '@angular/http'; -import { Observable } from 'rxjs/Rx'; - -import { AuthService } from '../shared/index'; - -@Injectable() -export class FriendService { - private static BASE_FRIEND_URL: string = '/api/v1/pods/'; - - constructor (private http: Http, private authService: AuthService) {} - - makeFriends() { - const headers = this.authService.getRequestHeader(); - return this.http.get(FriendService.BASE_FRIEND_URL + 'makefriends', { headers }) - .map(res => res.status) - .catch(this.handleError); - } - - quitFriends() { - const headers = this.authService.getRequestHeader(); - return this.http.get(FriendService.BASE_FRIEND_URL + 'quitfriends', { headers }) - .map(res => res.status) - .catch(this.handleError); - } - - private handleError (error: Response): Observable { - console.error(error); - return Observable.throw(error.json().error || 'Server error'); - } -} diff --git a/client/app/friends/index.ts b/client/app/friends/index.ts deleted file mode 100644 index 0adc256c4..000000000 --- a/client/app/friends/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './friend.service'; diff --git a/client/app/login/index.ts b/client/app/login/index.ts deleted file mode 100644 index 69c16441f..000000000 --- a/client/app/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './login.component'; diff --git a/client/app/login/login.component.html b/client/app/login/login.component.html deleted file mode 100644 index 940694515..000000000 --- a/client/app/login/login.component.html +++ /dev/null @@ -1,14 +0,0 @@ -

Login

-
-
- - -
- -
- - -
- - -
diff --git a/client/app/login/login.component.ts b/client/app/login/login.component.ts deleted file mode 100644 index 50f598d92..000000000 --- a/client/app/login/login.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router-deprecated'; - -import { AuthService, AuthStatus, User } from '../shared/index'; - -@Component({ - selector: 'my-login', - templateUrl: 'client/app/login/login.component.html' -}) - -export class LoginComponent { - constructor( - private authService: AuthService, - private router: Router - ) {} - - login(username: string, password: string) { - this.authService.login(username, password).subscribe( - result => { - const user = new User(username, result); - user.save(); - - this.authService.setStatus(AuthStatus.LoggedIn); - - this.router.navigate(['VideosList']); - }, - error => { - if (error.error === 'invalid_grant') { - alert('Credentials are invalid.'); - } else { - alert(`${error.error}: ${error.error_description}`); - } - } - ); - } -} diff --git a/client/app/shared/index.ts b/client/app/shared/index.ts deleted file mode 100644 index ad3ee0098..000000000 --- a/client/app/shared/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './search/index'; -export * from './users/index' diff --git a/client/app/shared/search/index.ts b/client/app/shared/search/index.ts deleted file mode 100644 index a49a4f1a9..000000000 --- a/client/app/shared/search/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './search-field.type'; -export * from './search.component'; -export * from './search.model'; diff --git a/client/app/shared/search/search-field.type.ts b/client/app/shared/search/search-field.type.ts deleted file mode 100644 index 846236290..000000000 --- a/client/app/shared/search/search-field.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type SearchField = "name" | "author" | "podUrl" | "magnetUri"; diff --git a/client/app/shared/search/search.component.html b/client/app/shared/search/search.component.html deleted file mode 100644 index fb13ac72e..000000000 --- a/client/app/shared/search/search.component.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
- - -
- - -
diff --git a/client/app/shared/search/search.component.ts b/client/app/shared/search/search.component.ts deleted file mode 100644 index d541cd0d6..000000000 --- a/client/app/shared/search/search.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, EventEmitter, Output } from '@angular/core'; - -import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown'; - -import { Search } from './search.model'; -import { SearchField } from './search-field.type'; - -@Component({ - selector: 'my-search', - templateUrl: 'client/app/shared/search/search.component.html', - directives: [ DROPDOWN_DIRECTIVES ] -}) - -export class SearchComponent { - @Output() search = new EventEmitter(); - - fieldChoices = { - name: 'Name', - author: 'Author', - podUrl: 'Pod Url', - magnetUri: 'Magnet Uri' - }; - searchCriterias: Search = { - field: 'name', - value: '' - }; - - get choiceKeys() { - return Object.keys(this.fieldChoices); - } - - choose($event: MouseEvent, choice: SearchField) { - $event.preventDefault(); - $event.stopPropagation(); - - this.searchCriterias.field = choice; - } - - doSearch() { - this.search.emit(this.searchCriterias); - } - - getStringChoice(choiceKey: SearchField) { - return this.fieldChoices[choiceKey]; - } -} diff --git a/client/app/shared/search/search.model.ts b/client/app/shared/search/search.model.ts deleted file mode 100644 index 932a6566c..000000000 --- a/client/app/shared/search/search.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SearchField } from './search-field.type'; - -export interface Search { - field: SearchField; - value: string; -} diff --git a/client/app/shared/users/auth-status.model.ts b/client/app/shared/users/auth-status.model.ts deleted file mode 100644 index f646bd4cf..000000000 --- a/client/app/shared/users/auth-status.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AuthStatus { - LoggedIn, - LoggedOut -} diff --git a/client/app/shared/users/auth.service.ts b/client/app/shared/users/auth.service.ts deleted file mode 100644 index d63fe38f3..000000000 --- a/client/app/shared/users/auth.service.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Headers, Http, RequestOptions, Response, URLSearchParams } from '@angular/http'; -import { Observable, Subject } from 'rxjs/Rx'; - -import { AuthStatus } from './auth-status.model'; -import { User } from './user.model'; - -@Injectable() -export class AuthService { - private static BASE_CLIENT_URL = '/api/v1/users/client'; - private static BASE_LOGIN_URL = '/api/v1/users/token'; - - loginChangedSource: Observable; - - private clientId: string; - private clientSecret: string; - private loginChanged: Subject; - - constructor(private http: Http) { - this.loginChanged = new Subject(); - this.loginChangedSource = this.loginChanged.asObservable(); - - // Fetch the client_id/client_secret - // FIXME: save in local storage? - this.http.get(AuthService.BASE_CLIENT_URL) - .map(res => res.json()) - .catch(this.handleError) - .subscribe( - result => { - this.clientId = result.client_id; - this.clientSecret = result.client_secret; - console.log('Client credentials loaded.'); - }, - error => { - alert(error); - } - ); - } - - getAuthRequestOptions(): RequestOptions { - return new RequestOptions({ headers: this.getRequestHeader() }); - } - - getRequestHeader() { - return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` }); - } - - getToken() { - return localStorage.getItem('access_token'); - } - - getTokenType() { - return localStorage.getItem('token_type'); - } - - getUser(): User { - if (this.isLoggedIn() === false) { - return null; - } - - const user = User.load(); - - return user; - } - - isLoggedIn() { - if (this.getToken()) { - return true; - } else { - return false; - } - } - - login(username: string, password: string) { - let body = new URLSearchParams(); - body.set('client_id', this.clientId); - body.set('client_secret', this.clientSecret); - body.set('response_type', 'code'); - body.set('grant_type', 'password'); - body.set('scope', 'upload'); - body.set('username', username); - body.set('password', password); - - let headers = new Headers(); - headers.append('Content-Type', 'application/x-www-form-urlencoded'); - - let options = { - headers: headers - }; - - return this.http.post(AuthService.BASE_LOGIN_URL, body.toString(), options) - .map(res => res.json()) - .catch(this.handleError); - } - - logout() { - // TODO make HTTP request - } - - setStatus(status: AuthStatus) { - this.loginChanged.next(status); - } - - private handleError (error: Response) { - console.error(error); - return Observable.throw(error.json() || { error: 'Server error' }); - } -} diff --git a/client/app/shared/users/index.ts b/client/app/shared/users/index.ts deleted file mode 100644 index c6816b3c6..000000000 --- a/client/app/shared/users/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './auth-status.model'; -export * from './auth.service'; -export * from './token.model'; -export * from './user.model'; diff --git a/client/app/shared/users/token.model.ts b/client/app/shared/users/token.model.ts deleted file mode 100644 index 021c83fad..000000000 --- a/client/app/shared/users/token.model.ts +++ /dev/null @@ -1,32 +0,0 @@ -export class Token { - access_token: string; - refresh_token: string; - token_type: string; - - static load() { - return new Token({ - access_token: localStorage.getItem('access_token'), - refresh_token: localStorage.getItem('refresh_token'), - token_type: localStorage.getItem('token_type') - }); - } - - constructor(hash?: any) { - if (hash) { - this.access_token = hash.access_token; - this.refresh_token = hash.refresh_token; - - if (hash.token_type === 'bearer') { - this.token_type = 'Bearer'; - } else { - this.token_type = hash.token_type; - } - } - } - - save() { - localStorage.setItem('access_token', this.access_token); - localStorage.setItem('refresh_token', this.refresh_token); - localStorage.setItem('token_type', this.token_type); - } -} diff --git a/client/app/shared/users/user.model.ts b/client/app/shared/users/user.model.ts deleted file mode 100644 index ca0a5f26c..000000000 --- a/client/app/shared/users/user.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Token } from './token.model'; - -export class User { - username: string; - token: Token; - - static load() { - return new User(localStorage.getItem('username'), Token.load()); - } - - constructor(username: string, hash_token: any) { - this.username = username; - this.token = new Token(hash_token); - } - - save() { - localStorage.setItem('username', this.username); - this.token.save(); - } -} diff --git a/client/app/videos/index.ts b/client/app/videos/index.ts deleted file mode 100644 index 1c80ac5e5..000000000 --- a/client/app/videos/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './shared/index'; -export * from './video-add/index'; -export * from './video-list/index'; -export * from './video-watch/index'; diff --git a/client/app/videos/shared/index.ts b/client/app/videos/shared/index.ts deleted file mode 100644 index c535c46fc..000000000 --- a/client/app/videos/shared/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './loader/index'; -export * from './pagination.model'; -export * from './sort-field.type'; -export * from './video.model'; -export * from './video.service'; diff --git a/client/app/videos/shared/loader/index.ts b/client/app/videos/shared/loader/index.ts deleted file mode 100644 index ab22584e4..000000000 --- a/client/app/videos/shared/loader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './loader.component'; diff --git a/client/app/videos/shared/loader/loader.component.html b/client/app/videos/shared/loader/loader.component.html deleted file mode 100644 index d02296a2d..000000000 --- a/client/app/videos/shared/loader/loader.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
diff --git a/client/app/videos/shared/loader/loader.component.scss b/client/app/videos/shared/loader/loader.component.scss deleted file mode 100644 index 454195811..000000000 --- a/client/app/videos/shared/loader/loader.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -div { - margin-top: 150px; -} - -// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d -.glyphicon-refresh-animate { - -animation: spin .7s infinite linear; - -ms-animation: spin .7s infinite linear; - -webkit-animation: spinw .7s infinite linear; - -moz-animation: spinm .7s infinite linear; -} - -@keyframes spin { - from { transform: scale(1) rotate(0deg);} - to { transform: scale(1) rotate(360deg);} -} - -@-webkit-keyframes spinw { - from { -webkit-transform: rotate(0deg);} - to { -webkit-transform: rotate(360deg);} -} - -@-moz-keyframes spinm { - from { -moz-transform: rotate(0deg);} - to { -moz-transform: rotate(360deg);} -} diff --git a/client/app/videos/shared/loader/loader.component.ts b/client/app/videos/shared/loader/loader.component.ts deleted file mode 100644 index 666d43bc3..000000000 --- a/client/app/videos/shared/loader/loader.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, Input } from '@angular/core'; - -@Component({ - selector: 'my-loader', - styleUrls: [ 'client/app/videos/shared/loader/loader.component.css' ], - templateUrl: 'client/app/videos/shared/loader/loader.component.html' -}) - -export class LoaderComponent { - @Input() loading: boolean; -} diff --git a/client/app/videos/shared/pagination.model.ts b/client/app/videos/shared/pagination.model.ts deleted file mode 100644 index 06f7a7875..000000000 --- a/client/app/videos/shared/pagination.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Pagination { - currentPage: number; - itemsPerPage: number; - total: number; -} diff --git a/client/app/videos/shared/sort-field.type.ts b/client/app/videos/shared/sort-field.type.ts deleted file mode 100644 index 6e8cc7936..000000000 --- a/client/app/videos/shared/sort-field.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type SortField = "name" | "-name" - | "duration" | "-duration" - | "createdDate" | "-createdDate"; diff --git a/client/app/videos/shared/video.model.ts b/client/app/videos/shared/video.model.ts deleted file mode 100644 index 614403d79..000000000 --- a/client/app/videos/shared/video.model.ts +++ /dev/null @@ -1,64 +0,0 @@ -export class Video { - author: string; - by: string; - createdDate: Date; - description: string; - duration: string; - id: string; - isLocal: boolean; - magnetUri: string; - name: string; - podUrl: string; - thumbnailPath: string; - - private static createByString(author: string, podUrl: string) { - let [ host, port ] = podUrl.replace(/^https?:\/\//, '').split(':'); - - if (port === '80' || port === '443') { - port = ''; - } else { - port = ':' + port; - } - - return author + '@' + host + port; - } - - private static createDurationString(duration: number) { - const minutes = Math.floor(duration / 60); - const seconds = duration % 60; - const minutes_padding = minutes >= 10 ? '' : '0'; - const seconds_padding = seconds >= 10 ? '' : '0'; - - return minutes_padding + minutes.toString() + ':' + seconds_padding + seconds.toString(); - } - - constructor(hash: { - author: string, - createdDate: string, - description: string, - duration: number; - id: string, - isLocal: boolean, - magnetUri: string, - name: string, - podUrl: string, - thumbnailPath: string - }) { - this.author = hash.author; - this.createdDate = new Date(hash.createdDate); - this.description = hash.description; - this.duration = Video.createDurationString(hash.duration); - this.id = hash.id; - this.isLocal = hash.isLocal; - this.magnetUri = hash.magnetUri; - this.name = hash.name; - this.podUrl = hash.podUrl; - this.thumbnailPath = hash.thumbnailPath; - - this.by = Video.createByString(hash.author, hash.podUrl); - } - - isRemovableBy(user) { - return this.isLocal === true && user && this.author === user.username; - } -} diff --git a/client/app/videos/shared/video.service.ts b/client/app/videos/shared/video.service.ts deleted file mode 100644 index a786b2ab2..000000000 --- a/client/app/videos/shared/video.service.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Http, Response, URLSearchParams } from '@angular/http'; -import { Observable } from 'rxjs/Rx'; - -import { Pagination } from './pagination.model'; -import { Search } from '../../shared/index'; -import { SortField } from './sort-field.type'; -import { AuthService } from '../../shared/index'; -import { Video } from './video.model'; - -@Injectable() -export class VideoService { - private static BASE_VIDEO_URL = '/api/v1/videos/'; - - constructor( - private authService: AuthService, - private http: Http - ) {} - - getVideo(id: string) { - return this.http.get(VideoService.BASE_VIDEO_URL + id) - .map(res =>