Compare commits
11 commits
412a542e03
...
4cc60b3755
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cc60b3755 | |||
| d64dce0680 | |||
| 6880d9c033 | |||
| 99439ee7f6 | |||
| 40a376daa3 | |||
| 2827a5656f | |||
| 1a79068215 | |||
| 7f10bf776e | |||
| 0d01e55b6e | |||
| ef07db5818 | |||
| ccde75f72a |
27 changed files with 622 additions and 179 deletions
9
src/app/api/supabase/channel.service.ts
Normal file
9
src/app/api/supabase/channel.service.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChannelService {
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Story } from './story';
|
||||
import { SupaService } from './supa.service';
|
||||
|
||||
|
|
@ -27,25 +28,20 @@ export class StoryService {
|
|||
if (!this.subscribedStandUpIds.includes(standup_id)) {
|
||||
this.subscribeToStories(standup_id);
|
||||
console.log('getStories - REFRESH', standup_id)
|
||||
const subject: Subject<Story[]> = new Subject();
|
||||
this.supa.client.from<Story>('story').select()
|
||||
.filter(<never>'standup_id', 'eq', standup_id)
|
||||
.then(data => {
|
||||
this.updateStore(data.body);
|
||||
subject.next(data.body);
|
||||
subject.complete();
|
||||
})
|
||||
.catch(error => {
|
||||
subject.error(error);
|
||||
subject.complete();
|
||||
console.error(error);
|
||||
});
|
||||
return subject.asObservable();
|
||||
} else {
|
||||
console.log('getStories - LOCAL', standup_id)
|
||||
const stories = Object.values<Story>(this.storyMap).filter(e => e.standup_id === standup_id)
|
||||
console.log('filteredStories', stories)
|
||||
return of(stories);
|
||||
}
|
||||
return this.stories.asObservable().pipe(
|
||||
map(stories => stories.filter(e => e.standup_id === standup_id))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,13 +85,17 @@ export class StoryService {
|
|||
// this.storyMap.set(e.id, e);
|
||||
this.storyMap[e.id] = e;
|
||||
});
|
||||
console.log('update stories', this.storyMap)
|
||||
this.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits local store.
|
||||
*/
|
||||
next = () => this.stories.next(Object.values(this.storyMap));
|
||||
next = () => {
|
||||
console.log('next', Object.values(this.storyMap))
|
||||
this.stories.next(Object.values(this.storyMap))
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve story from local store.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { User } from './user';
|
||||
|
||||
export class Story {
|
||||
id: number;
|
||||
id: string;
|
||||
standup_id: number;
|
||||
user_id: string
|
||||
base64: string;
|
||||
user_id: string;
|
||||
user?: User;
|
||||
src: string;
|
||||
created_at: string | Date;
|
||||
|
||||
constructor(user_id: string, standup_id: number, base64: string) {
|
||||
constructor(user_id: string, standup_id: number, src: string) {
|
||||
this.user_id = user_id;
|
||||
this.standup_id = standup_id;
|
||||
this.base64 = base64;
|
||||
this.src = src;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { createClient, SupabaseAuthUser, SupabaseClient } from '@supabase/supabase-js'
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Subject } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { User } from './user';
|
||||
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -9,7 +10,9 @@ import { environment } from '../../../environments/environment'
|
|||
})
|
||||
export class SupaService {
|
||||
client: SupabaseClient;
|
||||
user: BehaviorSubject<SupabaseAuthUser> = new BehaviorSubject(null);
|
||||
user: Subject<SupabaseAuthUser> = new Subject();
|
||||
supabaseUser: SupabaseAuthUser;
|
||||
userProfile: User;
|
||||
|
||||
constructor() {
|
||||
// Create a single supabase client for interacting with your database
|
||||
|
|
@ -20,9 +23,39 @@ export class SupaService {
|
|||
async getUser() {
|
||||
const user = await this.client.auth.user();
|
||||
console.log('user', user);
|
||||
this.supabaseUser = user;
|
||||
this.getUserProfile();
|
||||
this.user.next(user);
|
||||
}
|
||||
|
||||
getUserProfile(user_id: string = this.supabaseUser.id) {
|
||||
const subject: Subject<User> = new Subject();
|
||||
if (!this.userProfile) {
|
||||
this.client.from<User>('user').select().match({id: <never>user_id})
|
||||
.then(data => {
|
||||
console.log('getUserProfile', data)
|
||||
if (data.body.length === 0) {
|
||||
// create default user profile
|
||||
this.client.from<User>('user').insert(new User(user_id, this.supabaseUser.email.split('@')[0]))
|
||||
.then(data => {
|
||||
console.log('created UserProfile', data.body[0]);
|
||||
this.userProfile = data.body[0];
|
||||
subject.next(this.userProfile);
|
||||
})
|
||||
.catch(error => console.error('Error creating UserProfile', error))
|
||||
} else {
|
||||
console.log('loaded UserProfile', data.body[0]);
|
||||
this.userProfile = data.body[0];
|
||||
subject.next(this.userProfile);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('getUserProfile', error))
|
||||
} else {
|
||||
setTimeout(() =>subject.next(this.userProfile), 100);
|
||||
}
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
async login(email: string, password: string): Promise<SupabaseAuthUser> {
|
||||
try {
|
||||
const res = await this.client.auth.login(
|
||||
|
|
@ -30,6 +63,7 @@ export class SupaService {
|
|||
password
|
||||
);
|
||||
this.user.next(res.body.user);
|
||||
this.supabaseUser = res.body.user;
|
||||
return res.body.user;
|
||||
} catch (e) {
|
||||
console.error('Login', e);
|
||||
|
|
@ -49,6 +83,7 @@ export class SupaService {
|
|||
password
|
||||
);
|
||||
this.user.next(res.body.user);
|
||||
this.supabaseUser = res.body.user;
|
||||
return res.body.user;
|
||||
} catch (e) {
|
||||
console.error('Signup', e);
|
||||
|
|
|
|||
16
src/app/api/supabase/user.service.spec.ts
Normal file
16
src/app/api/supabase/user.service.spec.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UserService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
161
src/app/api/supabase/user.service.ts
Normal file
161
src/app/api/supabase/user.service.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
|
||||
import { map } from 'rxjs/internal/operators/map';
|
||||
import { SupaService } from './supa.service';
|
||||
import { User } from './user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserService {
|
||||
userMap = {};
|
||||
users: BehaviorSubject<User[]> = new BehaviorSubject(Object.values(this.userMap));
|
||||
isListening: boolean = true;
|
||||
|
||||
constructor(
|
||||
private supa: SupaService,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Get Users from local store.
|
||||
* Requests data if store is emtpy.
|
||||
* @returns Observable<User[]>
|
||||
*/
|
||||
getStories(): Observable<User[]> {
|
||||
if (Object.values(this.userMap).length === 0) {
|
||||
this.subscribeToUsers();
|
||||
console.log('getUsers- REFRESH')
|
||||
this.supa.client.from<User>('user').select()
|
||||
.then(data => {
|
||||
this.updateStore(data.body);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
return this.users.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to realtime events from users db.
|
||||
*/
|
||||
subscribeToUsers() {
|
||||
if (!this.isListening) {
|
||||
this.isListening = true;
|
||||
this.supa.client.from<User>('user').on('*', payload => {
|
||||
console.log('subscribeToStories - REALTIME EVENT', payload)
|
||||
if ((payload.eventType === 'INSERT') || (payload.eventType === 'UPDATE')) {
|
||||
this.userMap[payload.new.id] = payload.new;
|
||||
} else {
|
||||
delete this.userMap[payload.old.id];
|
||||
}
|
||||
this.next();
|
||||
}).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the local store with provided users
|
||||
* @param users
|
||||
*/
|
||||
updateStore(users: User[]) {
|
||||
users.forEach(e => {
|
||||
// this.userMap.set(e.id, e);
|
||||
this.userMap[e.id] = e;
|
||||
});
|
||||
console.log('update users', this.userMap)
|
||||
this.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits local store.
|
||||
*/
|
||||
next = () => {
|
||||
console.log('next', Object.values(this.userMap))
|
||||
this.users.next(Object.values(this.userMap))
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve users from local store.
|
||||
* @param id
|
||||
*/
|
||||
getOne(id: string) {
|
||||
if (this.userMap[id]) {
|
||||
return of(this.userMap[id]);
|
||||
} else {
|
||||
const subject: Subject<User> = new Subject();
|
||||
this.supa.client.from<User>('user').select()
|
||||
.filter(<never>'id', 'eq', id)
|
||||
.then(data => {
|
||||
this.updateStore([data.body[0]]);
|
||||
subject.next(data.body[0]);
|
||||
subject.complete();
|
||||
})
|
||||
.catch(error => {
|
||||
subject.error(error);
|
||||
subject.complete();
|
||||
});
|
||||
return subject.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a users data.
|
||||
* @param users
|
||||
*/
|
||||
updateOne(user: User): Observable<User> {
|
||||
const subject: Subject<User> = new Subject();
|
||||
this.supa.client.from<User>('user').update(user)
|
||||
.match({ id: user.id })
|
||||
.then(data => {
|
||||
subject.next(data.body[0]);
|
||||
this.updateStore([data.body[0]]);
|
||||
subject.complete();
|
||||
})
|
||||
.catch(error => {
|
||||
subject.error(error);
|
||||
subject.complete();
|
||||
});
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes one user from db.
|
||||
* @param user
|
||||
*/
|
||||
deleteOne(user: User): Observable<User> {
|
||||
const subject: Subject<User> = new Subject();
|
||||
this.supa.client.from<User>('user').delete()
|
||||
.match({ id: user.id })
|
||||
.then(data => {
|
||||
subject.next(user);
|
||||
subject.complete();
|
||||
})
|
||||
.catch(error => {
|
||||
subject.error(error);
|
||||
subject.complete();
|
||||
});
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a user on the db.
|
||||
* @param users
|
||||
*/
|
||||
addOne(user: User): Observable<User> {
|
||||
const subject: Subject<User> = new Subject();
|
||||
this.supa.client.from<User>('user').insert(user)
|
||||
.then(data => {
|
||||
subject.next(data.body[0]);
|
||||
this.updateStore([data.body[0]]);
|
||||
subject.complete();
|
||||
})
|
||||
.catch(error => {
|
||||
subject.error(error);
|
||||
subject.complete();
|
||||
});
|
||||
return subject.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
11
src/app/api/supabase/user.ts
Normal file
11
src/app/api/supabase/user.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export class User {
|
||||
id: string;
|
||||
status: 'OFFLINE' |'ONLINE';
|
||||
username: string;
|
||||
|
||||
constructor(id: string, username: string, status: 'OFFLINE' | 'ONLINE' = 'ONLINE') {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="container-fluid p-0">
|
||||
<div class="myContainer d-flex">
|
||||
<div *ngIf="supa.user | async" class="menu">
|
||||
<app-channel-list></app-channel-list>
|
||||
<app-sidebar></app-sidebar>
|
||||
</div>
|
||||
<div class="container content">
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { FormsModule } from '@angular/forms';
|
|||
import { SupaService } from './api/supabase/supa.service';
|
||||
import { StandupService } from './api/supabase/standup.service';
|
||||
import { StoryService } from './api/supabase/story.service';
|
||||
import { UserService } from './api/supabase/user.service';
|
||||
import { AuthGuard } from './authguard.service';
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
|
|
@ -17,8 +18,9 @@ import { SignupComponent } from './signup/signup.component';
|
|||
import { HuddleComponent } from './huddle/huddle.component';
|
||||
import { ProfileComponent } from './profile/profile.component';
|
||||
import { ChannelComponent } from './channel/channel.component';
|
||||
import { ChannelListComponent } from './channel-list/channel-list.component';
|
||||
import { SidebarComponent } from './sidebar/sidebar.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { InputManagerComponent } from './input-manager/input-manager.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
|
@ -29,8 +31,9 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||
SignupComponent,
|
||||
ProfileComponent,
|
||||
ChannelComponent,
|
||||
ChannelListComponent,
|
||||
DashboardComponent
|
||||
SidebarComponent,
|
||||
DashboardComponent,
|
||||
InputManagerComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
|
@ -42,6 +45,7 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||
SupaService,
|
||||
StandupService,
|
||||
StoryService,
|
||||
UserService,
|
||||
AuthGuard,
|
||||
],
|
||||
bootstrap: [
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
<div *ngIf="standup" class="row">
|
||||
<div class="col-12 col-sm-9">
|
||||
<div class="col-12 col-sm-9 order-1 order-sm-0">
|
||||
<legend #name contenteditable="true" (click)="dataUpdated=true">{{ standup.name }}</legend>
|
||||
<p #desc contenteditable="true" (click)="dataUpdated=true">{{ standup.description }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-3">
|
||||
<div class="col-12 col-sm-3 order-0 oder-sm-1">
|
||||
<div class="float-right">
|
||||
<button class="btn btn-primary ml-1 mr-1" [disabled]="!dataUpdated" (click)="updateStandUp(name.innerText, desc.innerText)">Save</button>
|
||||
<button class="btn btn-danger ml-1 mr-1" (click)="deleteStandUp()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="col-12 order-2">
|
||||
<ah-recorder (recordingEnded)="uploadStory($event)"></ah-recorder>
|
||||
</div>
|
||||
<div *ngFor="let user of users" class="col p-3">
|
||||
<div class="text-center" (click)="playStory(user)">
|
||||
<div class="storyImage" style="background-image: url({{user.image}})"
|
||||
[ngClass]="{'hasStory': user.story_link && user.submit_time}"></div>
|
||||
<p class="mt-2 mb-0"><strong>{{user.name}}</strong></p>
|
||||
<p><small>{{user.submit_time | date :'dd.MM. HH:mm' }}</small></p>
|
||||
<div *ngFor="let story of stories" class="col p-3 order-3">
|
||||
<div class="text-center" (click)="playStory(story)">
|
||||
<div class="storyImage" style="background-image: url({{'https://dummyimage.com/256x256/000/fff&text='+(story.user ? story.user.username: '...')}})" [ngClass]="{'hasStory': story.id && story.src}"></div>
|
||||
<p class="mt-2 mb-0"><strong>{{story.user ? story.user.username : '...'}}</strong></p>
|
||||
<p><small>{{story.created_at | date :'dd.MM. HH:mm' }}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -46,9 +45,9 @@
|
|||
</button>
|
||||
</div>
|
||||
<p class="text-center m-0">
|
||||
<strong>{{selectedUser.name}} -
|
||||
<small>{{selectedUser.submit_time | date :'dd.MM. HH:mm' }}</small></strong>
|
||||
<strong>{{selectedStory.user ? selectedStory.user.username : '...'}} - <small>{{selectedStory.created_at | date :'dd.MM. HH:mm' }}</small></strong>
|
||||
</p>
|
||||
<video [src]="selectedUser.story_link" (ended)="nextUser(modal)" autoplay></video>
|
||||
<video [src]="base64ToSafeURL(selectedStory.src)" (ended)="nextUser(modal)" autoplay>
|
||||
</video>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
}
|
||||
|
||||
.modal-body {
|
||||
height: 90vh;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.modal-body video {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,28 @@
|
|||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
|
||||
import { SupabaseAuthUser } from '@supabase/supabase-js';
|
||||
import { Subject } from 'rxjs';
|
||||
import { take, takeUntil, map } from 'rxjs/operators';
|
||||
import { StandUp } from '../api/supabase/standup';
|
||||
import { StandupService } from '../api/supabase/standup.service';
|
||||
import { Story } from '../api/supabase/story';
|
||||
import { StoryService } from '../api/supabase/story.service';
|
||||
import { SupaService } from '../api/supabase/supa.service';
|
||||
|
||||
import { UserService } from '../api/supabase/user.service';
|
||||
@Component({
|
||||
selector: 'app-huddle',
|
||||
templateUrl: './huddle.component.html',
|
||||
styleUrls: ['./huddle.component.scss']
|
||||
})
|
||||
export class HuddleComponent implements OnInit {
|
||||
export class HuddleComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("content") content;
|
||||
@ViewChild("activeStory") video;
|
||||
users = [
|
||||
{
|
||||
name: "Jan",
|
||||
image: "https://www.supercardating.com/doc/image.rhtm/profile-pic2.jpg",
|
||||
submit_time: 1601655386668,
|
||||
story_link: "https://erjb.s3.nl-ams.scw.cloud/ttk_beagle.mp4"
|
||||
},
|
||||
{
|
||||
name: "Bob",
|
||||
image:
|
||||
"https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Ftse1.mm.bing.net%2Fth%3Fid%3DOIP.4oYqJqInuQd2TAlPPdggLgHaHa%26pid%3DApi&f=1",
|
||||
submit_time: null,
|
||||
story_link: "https://erjb.s3.nl-ams.scw.cloud/ttk_golden.mp4"
|
||||
},
|
||||
{
|
||||
name: "Angela",
|
||||
image:
|
||||
"https://writestylesonline.com/wp-content/uploads/2019/01/What-To-Wear-For-Your-Professional-Profile-Picture-or-Headshot.jpg",
|
||||
submit_time: 1601655386668,
|
||||
story_link: "https://erjb.s3.nl-ams.scw.cloud/ttk_frenchie.mp4"
|
||||
}
|
||||
];
|
||||
selectedUser;
|
||||
standup: StandUp;
|
||||
selectedStory: Story;
|
||||
stories: Story[] = [];
|
||||
unsubscribe: Subject<boolean> = new Subject();
|
||||
|
||||
constructor(
|
||||
private modalService: NgbModal,
|
||||
|
|
@ -48,17 +30,22 @@ export class HuddleComponent implements OnInit {
|
|||
private storyService: StoryService,
|
||||
private supaService: SupaService,
|
||||
private route: ActivatedRoute,
|
||||
private sanitizer: DomSanitizer,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe(
|
||||
params => {
|
||||
this.reset();
|
||||
if (params.id) {
|
||||
this.standupService.getOne(params.id).subscribe(
|
||||
data => {
|
||||
console.log(data);
|
||||
this.standup = data;
|
||||
this.loadStories(data.id).subscribe(stories => this.stories = stories);
|
||||
this.loadStories(data.id).subscribe(stories => {
|
||||
this.stories = stories;
|
||||
});
|
||||
},
|
||||
error => {
|
||||
console.error(error);
|
||||
|
|
@ -69,60 +56,66 @@ export class HuddleComponent implements OnInit {
|
|||
)
|
||||
}
|
||||
|
||||
playStory(user) {
|
||||
if (!user.submit_time || !user.story_link) {
|
||||
ngOnDestroy() {
|
||||
this.unsubscribe.next(true);
|
||||
this.unsubscribe.complete();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.stories = [];
|
||||
delete this.standup;
|
||||
delete this.selectedStory;
|
||||
this.unsubscribe.next(true);
|
||||
}
|
||||
|
||||
playStory(story) {
|
||||
if (!story.id || !story.src) {
|
||||
return;
|
||||
}
|
||||
this.modalService.open(this.content, { centered: true });
|
||||
this.selectedUser = user;
|
||||
this.selectedStory = story;
|
||||
}
|
||||
|
||||
prevUser(modal: NgbModalRef) {
|
||||
let index: number = this.users.findIndex(
|
||||
(u) => u.name === this.selectedUser.name
|
||||
let index: number = this.stories.findIndex(
|
||||
(u) => u.id === this.selectedStory.id
|
||||
);
|
||||
if (index < 1) {
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
while (
|
||||
!this.users[index - 1].story_link ||
|
||||
!this.users[index - 1].submit_time
|
||||
) {
|
||||
while (!this.stories[index - 1].id) {
|
||||
index--;
|
||||
if (index === 0) {
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.selectedUser = this.users[index - 1];
|
||||
this.selectedStory = this.stories[index - 1];
|
||||
}
|
||||
|
||||
nextUser(modal: NgbModalRef) {
|
||||
let index: number = this.users.findIndex(
|
||||
(u) => u.name === this.selectedUser.name
|
||||
let index: number = this.stories.findIndex(
|
||||
(u) => u.id === this.selectedStory.id
|
||||
);
|
||||
if (index === -1 || index === this.users.length - 1) {
|
||||
if (index === -1 || index === this.stories.length - 1) {
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
while (
|
||||
!this.users[index + 1].story_link ||
|
||||
!this.users[index + 1].submit_time
|
||||
) {
|
||||
if (index === -1 || index === this.users.length - 1) {
|
||||
while (!this.stories[index + 1].id) {
|
||||
if (index === -1 || index === this.stories.length - 1) {
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
this.selectedUser = this.users[index + 1];
|
||||
this.selectedStory = this.stories[index + 1];
|
||||
}
|
||||
|
||||
updateStandUp(name:string, desc:string) {
|
||||
this.standup.name = name;
|
||||
this.standup.description = desc;
|
||||
this.standupService.updateOne(this.standup).subscribe(
|
||||
this.standupService.updateOne(this.standup).pipe(take(1)).subscribe(
|
||||
data => {
|
||||
console.log('Success', data);
|
||||
},
|
||||
|
|
@ -133,7 +126,7 @@ export class HuddleComponent implements OnInit {
|
|||
}
|
||||
|
||||
deleteStandUp() {
|
||||
this.standupService.deleteOne(this.standup).subscribe(
|
||||
this.standupService.deleteOne(this.standup).pipe(take(1)).subscribe(
|
||||
data => {
|
||||
console.log('Success', data);
|
||||
},
|
||||
|
|
@ -143,12 +136,11 @@ export class HuddleComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
async uploadStory(base64: string) {
|
||||
async uploadStory(src: string) {
|
||||
console.log('uploadStory', event);
|
||||
const user: SupabaseAuthUser = await this.supaService.client.auth.user();
|
||||
// TODO ID muss in DB noch autoincremented werden.
|
||||
const story = new Story(user.id, this.standup.id, base64);
|
||||
this.storyService.addOne(story).subscribe(
|
||||
const story = new Story(user.id, this.standup.id, src);
|
||||
this.storyService.addOne(story).pipe(take(1)).subscribe(
|
||||
data => {
|
||||
console.log('Success', data);
|
||||
},
|
||||
|
|
@ -159,7 +151,36 @@ export class HuddleComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadStories(id: number) {
|
||||
return this.storyService.getStories(id);
|
||||
return this.storyService.getStories(id).pipe(
|
||||
takeUntil(this.unsubscribe.asObservable()),
|
||||
map(stories => {
|
||||
stories.forEach(story => {
|
||||
this.userService.getOne(story.user_id).subscribe(user => story.user = user)
|
||||
})
|
||||
return stories;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
base64ToSafeURL(b64Data: string, contentType: string ='video/webm', sliceSize: number=512): SafeUrl {
|
||||
// https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript#16245768
|
||||
const byteCharacters = atob(b64Data.split('base64,')[1]);
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
const blob = new Blob(byteArrays, {type: contentType});
|
||||
return this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
21
src/app/input-manager/input-manager.component.html
Normal file
21
src/app/input-manager/input-manager.component.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<video id="localVideo" autoplay muted></video>
|
||||
|
||||
|
||||
<details [open]="detailsOpen">
|
||||
<summary>
|
||||
Settings:
|
||||
</summary>
|
||||
<div class="form-group">
|
||||
<label for="audioSource">Audio input source: </label>
|
||||
<select class="form-control" id="audioSource" (change)="start()"></select>
|
||||
</div>
|
||||
|
||||
<!-- <div class="select">
|
||||
<label for="audioOutput">Audio output destination: </label><select id="audioOutput" (change)="changeAudioDestination()"></select>
|
||||
</div> -->
|
||||
|
||||
<div class="form-group">
|
||||
<label for="videoSource">Video source: </label>
|
||||
<select class="form-control" id="videoSource" (change)="start()"></select>
|
||||
</div>
|
||||
</details>
|
||||
4
src/app/input-manager/input-manager.component.scss
Normal file
4
src/app/input-manager/input-manager.component.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChannelListComponent } from './channel-list.component';
|
||||
import { InputManagerComponent } from './input-manager.component';
|
||||
|
||||
describe('ChannelListComponent', () => {
|
||||
let component: ChannelListComponent;
|
||||
let fixture: ComponentFixture<ChannelListComponent>;
|
||||
describe('InputManagerComponent', () => {
|
||||
let component: InputManagerComponent;
|
||||
let fixture: ComponentFixture<InputManagerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ChannelListComponent ]
|
||||
declarations: [ InputManagerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChannelListComponent);
|
||||
fixture = TestBed.createComponent(InputManagerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
143
src/app/input-manager/input-manager.component.ts
Normal file
143
src/app/input-manager/input-manager.component.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-manager',
|
||||
templateUrl: './input-manager.component.html',
|
||||
styleUrls: ['./input-manager.component.scss']
|
||||
})
|
||||
export class InputManagerComponent implements AfterViewInit {
|
||||
@Input() detailsOpen: boolean = true;
|
||||
@Output() streamOutput: EventEmitter<MediaStream> = new EventEmitter();
|
||||
stream: MediaStream;
|
||||
videoElement:any = document.querySelector('#localVideo');
|
||||
audioInputSelect:any = document.querySelector('select#audioSource');
|
||||
// audioOutputSelect:any = document.querySelector('select#audioOutput');
|
||||
videoSelect:any = document.querySelector('select#videoSource');
|
||||
// selectors = [this.audioInputSelect, this.audioOutputSelect, this.videoSelect];
|
||||
selectors = [this.audioInputSelect, this.videoSelect];
|
||||
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.videoElement = document.querySelector('#localVideo');
|
||||
this.videoElement.muted = true;
|
||||
this.audioInputSelect = document.querySelector('select#audioSource');
|
||||
// this.audioOutputSelect = document.querySelector('select#audioOutput');
|
||||
this.videoSelect = document.querySelector('select#videoSource');
|
||||
// this.selectors = [this.audioInputSelect, this.audioOutputSelect, this.videoSelect];
|
||||
this.selectors = [this.audioInputSelect, this.videoSelect];
|
||||
// console.log(this.videoSelect, this.selectors)
|
||||
// this.audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);
|
||||
navigator.mediaDevices.enumerateDevices().then(devices => this.gotDevices(devices)).catch(error => this.handleError(error));
|
||||
this.start();
|
||||
}
|
||||
|
||||
// Attach audio output device to video element using device/sink ID.
|
||||
attachSinkId(element, sinkId) {
|
||||
if (typeof element.sinkId !== 'undefined') {
|
||||
element.setSinkId(sinkId)
|
||||
.then(() => {
|
||||
// console.log(`Success, audio output device attached: ${sinkId}`);
|
||||
})
|
||||
.catch(error => {
|
||||
let errorMessage = error;
|
||||
if (error.name === 'SecurityError') {
|
||||
errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`;
|
||||
}
|
||||
// console.error(errorMessage);
|
||||
// Jump back to first output device in the list as it's the default.
|
||||
// this.audioOutputSelect.selectedIndex = 0;
|
||||
});
|
||||
} else {
|
||||
// console.warn('Browser does not support output device selection.');
|
||||
}
|
||||
}
|
||||
|
||||
// changeAudioDestination() {
|
||||
// const audioDestination = this.audioOutputSelect.value;
|
||||
// this.attachSinkId(this.videoElement, audioDestination);
|
||||
// }
|
||||
|
||||
gotStream(stream: MediaStream) {
|
||||
this.stream = stream; // make stream available to // console
|
||||
this.videoElement.srcObject = stream;
|
||||
this.videoElement.muted = true;
|
||||
this.streamOutput.next(stream);
|
||||
// Refresh button list in case labels have become available
|
||||
return navigator.mediaDevices.enumerateDevices();
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);
|
||||
}
|
||||
|
||||
gotDevices(deviceInfos) {
|
||||
// Handles being called several times to update labels. Preserve values.
|
||||
const values = this.selectors.map(select => select.value);
|
||||
this.selectors.forEach(select => {
|
||||
while (select.firstChild) {
|
||||
select.removeChild(select.firstChild);
|
||||
}
|
||||
});
|
||||
for (let i = 0; i !== deviceInfos.length; ++i) {
|
||||
const deviceInfo = deviceInfos[i];
|
||||
const option = document.createElement('option');
|
||||
option.value = deviceInfo.deviceId;
|
||||
if (deviceInfo.kind === 'audioinput') {
|
||||
option.text = deviceInfo.label || `microphone ${this.audioInputSelect.length + 1}`;
|
||||
this.audioInputSelect.appendChild(option);
|
||||
// } else if (deviceInfo.kind === 'audiooutput') {
|
||||
// option.text = deviceInfo.label || `speaker ${this.audioOutputSelect.length + 1}`;
|
||||
// this.audioOutputSelect.appendChild(option);
|
||||
} else if (deviceInfo.kind === 'videoinput') {
|
||||
option.text = deviceInfo.label || `camera ${this.videoSelect.length + 1}`;
|
||||
this.videoSelect.appendChild(option);
|
||||
} else {
|
||||
// console.log('Some other kind of source/device: ', deviceInfo);
|
||||
}
|
||||
}
|
||||
this.selectors.forEach((select, selectorIndex) => {
|
||||
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
|
||||
select.value = values[selectorIndex];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
let audioSource = this.audioInputSelect.value;
|
||||
const savedAudioSource = localStorage.getItem('async-huddle-selected-mic');
|
||||
if (audioSource) {
|
||||
localStorage.setItem('async-huddle-selected-mic', audioSource);
|
||||
} else if (savedAudioSource) {
|
||||
audioSource = savedAudioSource;
|
||||
setTimeout(() => {
|
||||
const e:any = document.querySelector('[value="'+savedAudioSource+'"]')
|
||||
if (e) e.selected = true;
|
||||
}, 200);
|
||||
}
|
||||
console.warn('Selecting audio:', audioSource);
|
||||
let videoSource = this.videoSelect.value;
|
||||
const savedVideoSource = localStorage.getItem('async-huddle-selected-video-device');
|
||||
if (videoSource) {
|
||||
localStorage.setItem('async-huddle-selected-video-device', videoSource);
|
||||
} else if (savedVideoSource) {
|
||||
videoSource = savedVideoSource;
|
||||
setTimeout(() => {
|
||||
const e:any = document.querySelector('[value="'+savedVideoSource+'"]')
|
||||
if (e) e.selected = true;
|
||||
}, 200);
|
||||
}
|
||||
console.warn('Selecting video:', videoSource);
|
||||
const constraints = {
|
||||
audio: {deviceId: audioSource ? {exact: audioSource} : undefined},
|
||||
video: {deviceId: videoSource ? {exact: videoSource} : undefined}
|
||||
};
|
||||
navigator.mediaDevices.getUserMedia(constraints).then((stream) => this.gotStream(stream)).then(devices => this.gotDevices(devices)).catch(error => this.handleError(error));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,24 @@
|
|||
<div class="row mt-3 justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="email">Email</label>
|
||||
<input #email class="form-control" type="email">
|
||||
<form>
|
||||
<div class="row mt-3 justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="email">Email</label>
|
||||
<input #email class="form-control" type="email">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="password">Password</label>
|
||||
<input #password class="form-control" type="password">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="password">Password</label>
|
||||
<input #password class="form-control" type="password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-12 col-sm-4">
|
||||
<button class="btn btn-primary text-uppercase" type="button" (click)="login(email.value, password.value)">
|
||||
Log in
|
||||
</button> or
|
||||
<a class="btn btn-secondary text-uppercase" [routerLink]="'/signup'">
|
||||
signup
|
||||
</a>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-12 col-sm-4">
|
||||
<button class="btn btn-primary text-uppercase" type="submit" (click)="login(email.value, password.value)">
|
||||
Log in
|
||||
</button> or
|
||||
<a class="btn btn-secondary text-uppercase" (click)="router.navigate(['/signup'])">
|
||||
signup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -10,7 +10,7 @@ import { SupaService } from '../api/supabase/supa.service';
|
|||
export class LoginComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
public router: Router,
|
||||
private supa: SupaService,
|
||||
) { }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
<legend>Profile</legend>
|
||||
<pre>
|
||||
<code *ngIf="user">
|
||||
{{ user | json }}
|
||||
</code>
|
||||
</pre>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-3">
|
||||
<legend>Profile</legend>
|
||||
<form *ngIf="user">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" name="username" aria-describedby="Input for username." [(ngModel)]="user.username">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" (submit)="updateUser()" (click)="updateUser()">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6">
|
||||
<legend>Media Settings</legend>
|
||||
<app-input-manager></app-input-manager>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { SupabaseAuthUser } from '@supabase/supabase-js';
|
||||
import { SupaService } from '../api/supabase/supa.service';
|
||||
import { User } from '../api/supabase/user';
|
||||
import { UserService } from '../api/supabase/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
|
|
@ -8,17 +10,25 @@ import { SupaService } from '../api/supabase/supa.service';
|
|||
styleUrls: ['./profile.component.scss']
|
||||
})
|
||||
export class ProfileComponent implements OnInit {
|
||||
user: SupabaseAuthUser;
|
||||
user: User;
|
||||
|
||||
constructor(
|
||||
public supa: SupaService,
|
||||
private supaService: SupaService,
|
||||
private userService: UserService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.supa.client.auth.user().then(user => {
|
||||
console.log('user', user)
|
||||
this.supaService.getUserProfile().subscribe((user:User) => {
|
||||
console.log('user', user);
|
||||
this.user = user;
|
||||
});
|
||||
}
|
||||
|
||||
updateUser() {
|
||||
this.userService.updateOne(this.user).subscribe(
|
||||
data => console.log('Success', data),
|
||||
error => console.error(error)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="modal-body d-flex flex-column">
|
||||
<video *ngIf="stream" [id]="id" [srcObject]="stream" class="video-recorder" autoplay muted volume="0"></video>
|
||||
<button *ngIf="!isRecording" class="btn btn-success" (click)="startRecording()">
|
||||
<app-input-manager (streamOutput)="handleStreamChange($event)" [detailsOpen]="false"></app-input-manager>
|
||||
<button *ngIf="!isRecording" class="btn btn-success mt-2" (click)="startRecording()">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||
|
|
@ -35,6 +35,6 @@
|
|||
</svg>
|
||||
Start recording
|
||||
</button>
|
||||
<button *ngIf="isRecording" class="btn btn-danger" (click)="stopRecording()">Stop</button>
|
||||
<button *ngIf="isRecording" class="btn btn-danger mt-2" (click)="stopRecording()">Stop</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
@ -27,21 +27,20 @@ export class RecorderComponent implements OnInit {
|
|||
this.modal = this.modalService.open(this.content, {
|
||||
centered: true
|
||||
});
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
const video:any = document.getElementById(this.id);
|
||||
video.volume = 0;
|
||||
video.muted = true;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
startRecording() {
|
||||
handleStreamChange(stream: MediaStream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
startRecording(stream: MediaStream = this.stream) {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
this.isRecording = true;
|
||||
this.recorder = new rrtc.RecordRTCPromisesHandler(this.stream, {
|
||||
this.recorder = new rrtc.RecordRTCPromisesHandler(stream, {
|
||||
type: 'video',
|
||||
mimeType: 'video/webm;codecs=vp8',
|
||||
});
|
||||
this.recorder.startRecording();
|
||||
}
|
||||
|
|
@ -50,21 +49,10 @@ export class RecorderComponent implements OnInit {
|
|||
const that = this;
|
||||
this.isRecording = false;
|
||||
await this.recorder.stopRecording();
|
||||
let blob = await this.recorder.getBlob();
|
||||
// read as b64
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = function() {
|
||||
const base64data:string = String(reader.result);
|
||||
console.log(base64data);
|
||||
that.recordingEnded.emit(base64data);
|
||||
that.modal.close();
|
||||
}
|
||||
const handleError = (error) => {
|
||||
console.error(error);
|
||||
}
|
||||
reader.onerror = handleError;
|
||||
reader.onabort = handleError;
|
||||
let blob = await this.recorder.getDataURL();
|
||||
that.recordingEnded.emit(blob);
|
||||
that.modal.close();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,4 +14,5 @@
|
|||
<ul class="list-unstyled">
|
||||
<li [routerLink]="'/profile'">Profile</li>
|
||||
</ul>
|
||||
<p style="position: fixed; bottom: 5px; text-align: center;" class="text-muted"><small>v{{environment.version}}</small></p>
|
||||
</div>
|
||||
|
|
@ -2,14 +2,16 @@ import { Component, OnInit } from '@angular/core';
|
|||
import { Observable, throwError } from 'rxjs';
|
||||
import { StandUp } from '../api/supabase/standup';
|
||||
import { StandupService } from '../api/supabase/standup.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channel-list',
|
||||
templateUrl: './channel-list.component.html',
|
||||
styleUrls: ['./channel-list.component.scss']
|
||||
selector: 'app-sidebar',
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrls: ['./sidebar.component.scss']
|
||||
})
|
||||
export class ChannelListComponent implements OnInit {
|
||||
export class SidebarComponent implements OnInit {
|
||||
standups: StandUp[] = [];
|
||||
environment = environment;
|
||||
|
||||
constructor(
|
||||
private standupSevice: StandupService,
|
||||
|
|
@ -1,25 +1,27 @@
|
|||
<div class="row mt-3 justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="email">Email</label>
|
||||
<input [(ngModel)]="email" class="form-control" type="email">
|
||||
<form>
|
||||
<div class="row mt-3 justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="email">Email</label>
|
||||
<input [(ngModel)]="email" name="email" class="form-control" type="email">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="password">Password</label>
|
||||
<input [(ngModel)]="password" class="form-control" type="password">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="password">Password</label>
|
||||
<input [(ngModel)]="password" name="password" class="form-control" type="password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="password">Repeat password</label>
|
||||
<input [(ngModel)]="repeat" class="form-control" type="password">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="form-group col-12 col-sm-4">
|
||||
<label for="password">Repeat password</label>
|
||||
<input [(ngModel)]="repeat" name="repeat" class="form-control" type="password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-12 col-sm-4">
|
||||
<button type="button" class="btn btn-secondary text-uppercase" (click)="signup(email, password, repeat)">
|
||||
signup
|
||||
</button>
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-12 col-sm-4">
|
||||
<button type="button" class="btn btn-secondary text-uppercase" (click)="signup(email, password, repeat)">
|
||||
signup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -6,5 +6,6 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
supa_url: 'https://wdklwjgycvhxpbbnrpto.supabase.co',
|
||||
supa_key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYwMzQ2OTMyOCwiZXhwIjoxOTE5MDQ1MzI4fQ.KunzEloX8UoOAK8fGbiZtAi9bMxB_Ap8GVcJFWyPx0Q'
|
||||
supa_key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYwMzQ2OTMyOCwiZXhwIjoxOTE5MDQ1MzI4fQ.KunzEloX8UoOAK8fGbiZtAi9bMxB_Ap8GVcJFWyPx0Q',
|
||||
version: '0.0.1'
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue