Compare commits

..

3 commits

13 changed files with 466 additions and 28 deletions

View file

@ -1,9 +1,160 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { Channel } from './channel';
import { SupaService } from './supa.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ChannelService { export class ChannelService {
channelMap = {};
channels: BehaviorSubject<any> = new BehaviorSubject([]);
isListening: boolean = false;
constructor() { } constructor(
private supa: SupaService,
) { }
/**
* Get Channels from local store.
* Requests data if store is emtpy.
* @returns Observable<Channel[]>
*/
getChannels(): Observable<Channel[]> {
if (Object.values(this.channelMap).length === 0) {
console.log('getChannels - REFRESH')
return this.refreshChannels();
} else {
console.log('getChannels - LOCAL')
return this.channels.asObservable();
}
}
/**
* Listen to realtime events from channel db.
*/
subscribeToChannels() {
if (!this.isListening) {
this.isListening = true;
this.supa.client.from<Channel>('channel').on('*', payload => {
console.log('subscribeToChannels - REALTIME EVENT', payload);
if ((payload.eventType === 'INSERT') || (payload.eventType === 'UPDATE')) {
this.channelMap[payload.new.id] = payload.new;
} else {
delete this.channelMap[payload.old.id];
}
this.next();
}).subscribe();
}
}
/**
* Requests up to date data from API.
* Returns the local copy.
* @returns Observable<Channel>
*/
refreshChannels(): Observable<Channel[]> {
this.subscribeToChannels();
this.supa.client.from<Channel>('channel').select()
.then(channels => this.updateStore(channels.body))
.catch(error => console.log('Error: ', error));
return this.channels.asObservable();
}
/**
* Update the local store with provided channels
* @param channels
*/
updateStore(channels: Channel[]) {
channels.forEach(e => {
this.channelMap[e.id] = e;
});
this.next();
}
/**
* Emits local store.
*/
next = () => this.channels.next(Object.values(this.channelMap));
/**
* Retrieve channel from local store.
* @param id
*/
getOne(id: number) {
if (this.channelMap[id]) {
return of(this.channelMap[id]);
} else {
const subject: Subject<Channel> = new Subject();
this.supa.client.from<Channel>('channel').select('id, name, description')
.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();
}
}
/**
*
* @param channel
*/
updateOne(channel: Channel): Observable<Channel> {
const subject: Subject<Channel> = new Subject();
this.supa.client.from<Channel>('channel').update(channel)
.match({ id: channel.id })
.then(data => {
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
/**
* Removes one channel from db.
* @param channel
*/
deleteOne(channel: Channel): Observable<Channel> {
const subject: Subject<Channel> = new Subject();
this.supa.client.from<Channel>('channel').delete()
.match({ id: channel.id })
.then(data => {
subject.next(channel);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
/**
* Creates a channel on the db.
* @param channel
*/
addOne(channel: Channel): Observable<Channel> {
const subject: Subject<Channel> = new Subject();
this.supa.client.from<Channel>('channel').insert(channel)
.then(data => {
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
} }

View file

@ -0,0 +1,13 @@
export class Channel {
id: number;
name: string;
description: string;
inserted_at: string | Date;
created_by: string;
constructor(created_by: string, name: string, description?: string) {
this.name = name;
this.description = description;
this.created_by = created_by;
}
}

View file

@ -0,0 +1,3 @@
export class Message {
}

View file

@ -7,7 +7,6 @@ import { SupaService } from './supa.service';
providedIn: 'root' providedIn: 'root'
}) })
export class StandupService { export class StandupService {
// standupMap: Map<number, StandUp> = new Map();
standupMap = {}; standupMap = {};
standups: BehaviorSubject<any> = new BehaviorSubject([]); standups: BehaviorSubject<any> = new BehaviorSubject([]);
isListening: boolean = false; isListening: boolean = false;
@ -68,7 +67,6 @@ export class StandupService {
*/ */
updateStore(standups: StandUp[]) { updateStore(standups: StandUp[]) {
standups.forEach(e => { standups.forEach(e => {
// this.standupMap.set(e.id, e);
this.standupMap[e.id] = e; this.standupMap[e.id] = e;
}); });
this.next(); this.next();

View file

@ -1 +1,106 @@
<p>channel works!</p> <ng-container *ngIf="channel">
<div class="row headerRow">
<div class="col-7 col-sm-10">
<strong>#</strong><strong #name contenteditable="true" (click)="dataUpdated=true">{{ channel.name }}</strong>
</div>
<div class="col-5 col-sm-2">
<div class="text-right">
<button class="btn btn-sm btn-primary mb-1" [disabled]="!dataUpdated" (click)="updateChannel(name.innerText, desc.innerText)">Save</button>
<button class="btn btn-sm btn-danger ml-1 mb-1" (click)="deleteChannel()">Delete</button>
</div>
</div>
<div class="col-12">
<span #desc contenteditable="true" (click)="dataUpdated=true">{{ channel.description }}</span>
</div>
</div>
<div class="row messageRow">
<div class="col-12 p-0">
<ul class="list-group list-group-flush">
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
<li class="list-group-item">Cras justo odio</li>
<li class="list-group-item">Dapibus ac facilisis in</li>
<li class="list-group-item">Morbi leo risus</li>
<li class="list-group-item">Porta ac consectetur ac</li>
<li class="list-group-item">Vestibulum at eros</li>
</ul>
</div>
</div>
<!-- send msg -->
<div class="row inputRow">
<div class="card w-100 m-3 d-flex flex-row">
<input [(ngModel)]="messageInput" type="text" class="input flex-grow-1">
<div class="optionRow">
<button class="btn btn-sm btn-primary btnSend ml-1">Send</button>
</div>
</div>
</div>
</ng-container>

View file

@ -0,0 +1,35 @@
.headerRow {
margin-top: -5px;
padding-bottom: 10px;
border-bottom: 1px dashed #343a40;
}
.messageRow {
margin-bottom: 60px;
max-height: calc(100vh - 200px);
overflow: hidden scroll;
}
.inputRow {
position: fixed;
bottom: 0;
// width: calc(100vw - 300px);
.input {
border: 0;
// width: 100%;
padding-left: 5px;
padding-right: 5px;
}
.optionRow {
// background-color: lightgray;
// width: 100%;
height: 40px;
padding: 5px;
.btnSend {
float: right;
background-color: lightseagreen;
border-color: lightseagreen;
&:hover {
border-color: seagreen;
}
}
}
}

View file

@ -1,4 +1,15 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from "@angular/core";
import { DomSanitizer } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { Channel } from '../api/supabase/channel';
import { ChannelService } from '../api/supabase/channel.service';
import { Message } from '../api/supabase/message';
import { SupaService } from '../api/supabase/supa.service';
import { User } from '../api/supabase/user';
import { UserService } from '../api/supabase/user.service';
@Component({ @Component({
selector: 'app-channel', selector: 'app-channel',
@ -6,10 +17,95 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./channel.component.scss'] styleUrls: ['./channel.component.scss']
}) })
export class ChannelComponent implements OnInit { export class ChannelComponent implements OnInit {
channel: Channel;
member: User[] = [];
messages: Message[] = [];
$destroy: Subject<boolean> = new Subject();
dataUpdated: boolean = false;
messageInput: string = '';
constructor() { } constructor(
private channelService: ChannelService,
private modalService: NgbModal,
private supaService: SupaService,
private route: ActivatedRoute,
private sanitizer: DomSanitizer,
private userService: UserService,
private router: Router,
) { }
ngOnInit(): void { ngOnInit() {
this.route.params.subscribe(
params => {
this.reset();
if (params.id) {
this.channelService.getOne(params.id).subscribe(
data => {
console.log('got channel', data);
this.channel = data;
// this.loadMessages(data.id).subscribe(messages => {
// this.messages = messages;
// });
},
error => {
console.error(error);
this.router.navigateByUrl('/dashboard');
}
)
}
}
)
}
ngOnDestroy() {
this.$destroy.next(true);
this.$destroy.complete();
}
reset() {
delete this.channel;
this.member = [];
this.messages = [];
this.messageInput = '';
this.$destroy.next(true);
}
loadChannel(id: number) {
return this.channelService.getOne(id).pipe(takeUntil(this.$destroy.asObservable()));
}
loadMessages(channel_id: number) {
}
loadUser(user_id: string) {
}
updateChannel(name:string, desc:string) {
this.channel.name = name;
this.channel.description = desc;
this.channelService.updateOne(this.channel).pipe(take(1)).subscribe(
data => {
console.log('Success', data);
},
error => {
console.error('Failed', error);
}
);
}
deleteChannel() {
const check = confirm('Do you want to delete this channel?');
if (check == true) {
this.channelService.deleteOne(this.channel).subscribe(
data => {
console.log('Deleted channel', data);
this.router.navigateByUrl('/dashboard');
},
error => console.error(error)
);
}
} }
} }

View file

@ -5,8 +5,8 @@
</div> </div>
<div class="col-12 col-sm-3 order-0 oder-sm-1"> <div class="col-12 col-sm-3 order-0 oder-sm-1">
<div class="float-right"> <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-sm 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> <button class="btn btn-sm btn-danger ml-1 mr-1" (click)="deleteStandUp()">Delete</button>
</div> </div>
</div> </div>
<div class="col-12 order-2"> <div class="col-12 order-2">

View file

@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap"; import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
import { SupabaseAuthUser } from '@supabase/supabase-js'; import { SupabaseAuthUser } from '@supabase/supabase-js';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -32,6 +32,7 @@ export class HuddleComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private userService: UserService, private userService: UserService,
private router: Router,
) {} ) {}
ngOnInit() { ngOnInit() {
@ -49,6 +50,7 @@ export class HuddleComponent implements OnInit, OnDestroy {
}, },
error => { error => {
console.error(error); console.error(error);
this.router.navigateByUrl('/dashboard');
} }
) )
} }
@ -122,19 +124,23 @@ export class HuddleComponent implements OnInit, OnDestroy {
error => { error => {
console.error('Failed', error); console.error('Failed', error);
} }
) );
} }
deleteStandUp() { deleteStandUp() {
const check = confirm('Do you want to delete this standup?');
if (check == true) {
this.standupService.deleteOne(this.standup).pipe(take(1)).subscribe( this.standupService.deleteOne(this.standup).pipe(take(1)).subscribe(
data => { data => {
console.log('Success', data); console.log('Success', data);
this.router.navigateByUrl('/dashboard')
}, },
error => { error => {
console.error('Failed', error); console.error('Failed', error);
} }
); );
} }
}
async uploadStory(src: string) { async uploadStory(src: string) {
console.log('uploadStory', event); console.log('uploadStory', event);

View file

@ -1,18 +1,18 @@
<form> <form>
<div class="row mt-3 justify-content-md-center"> <div class="row mt-3 justify-content-md-center">
<div class="form-group col-12 col-sm-4"> <div class="form-group col-12 col-md-4">
<label for="email">Email</label> <label for="email">Email</label>
<input #email class="form-control" type="email"> <input #email class="form-control" type="email">
</div> </div>
</div> </div>
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
<div class="form-group col-12 col-sm-4"> <div class="form-group col-12 col-md-4">
<label for="password">Password</label> <label for="password">Password</label>
<input #password class="form-control" type="password"> <input #password class="form-control" type="password">
</div> </div>
</div> </div>
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
<div class="col-12 col-sm-4"> <div class="col-12 col-md-4">
<button class="btn btn-primary text-uppercase" type="submit" (click)="login(email.value, password.value)"> <button class="btn btn-primary text-uppercase" type="submit" (click)="login(email.value, password.value)">
Log in Log in
</button> or </button> or

View file

@ -1,14 +1,13 @@
<div class="list bg-dark text-white p-3"> <div class="list bg-dark text-white p-3">
<p><strong>Standup</strong> <strong class="newBtn" (click)="createStandUp()">+</strong></p> <p><strong>Standup</strong> <strong class="newBtn" (click)="createStandUp()" style="vertical-align: text-bottom;">+</strong></p>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li *ngFor="let s of standups" [title]="s.description" [routerLink]="'/huddle/'+s.id">{{s.name}}</li> <li *ngFor="let s of standups" [title]="s.description" [routerLink]="'/huddle/'+s.id">{{s.name}}</li>
<li *ngIf="standups && (standups.length === 0)"><small>There are no standups yet.</small></li>
</ul> </ul>
<p><strong>Channel</strong> <strong class="newBtn">+</strong></p> <p><strong>Channel</strong> <strong class="newBtn" (click)="createChannel()" style="vertical-align: text-bottom;">+</strong></p>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>...</li> <li *ngFor="let c of channels" [title]="c.description" [routerLink]="'/channel/'+c.id"><span class="channelHash">#</span>{{c.name}}</li>
<li>...</li> <li *ngIf="channels && (channels.length === 0)"><small>There are no channels yet.</small></li>
<li>...</li>
<li>...</li>
</ul> </ul>
<p><strong>Settings</strong></p> <p><strong>Settings</strong></p>
<ul class="list-unstyled"> <ul class="list-unstyled">

View file

@ -19,4 +19,11 @@
color: lightseagreen; color: lightseagreen;
} }
} }
.channelHash {
padding: 3px 6.5px;
background: rgba(255,255,255,0.1);
border-radius: 3px;
margin-right: 5px;
vertical-align: middle;
}
} }

View file

@ -3,6 +3,10 @@ import { Observable, throwError } from 'rxjs';
import { StandUp } from '../api/supabase/standup'; import { StandUp } from '../api/supabase/standup';
import { StandupService } from '../api/supabase/standup.service'; import { StandupService } from '../api/supabase/standup.service';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { Channel } from '../api/supabase/channel';
import { ChannelService } from '../api/supabase/channel.service';
import { SupaService } from '../api/supabase/supa.service';
import { User } from '../api/supabase/user';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
@ -11,10 +15,13 @@ import { environment } from '../../environments/environment';
}) })
export class SidebarComponent implements OnInit { export class SidebarComponent implements OnInit {
standups: StandUp[] = []; standups: StandUp[] = [];
channels: Channel[] = [];
environment = environment; environment = environment;
constructor( constructor(
private supaService: SupaService,
private standupSevice: StandupService, private standupSevice: StandupService,
private channelService: ChannelService,
) { } ) { }
ngOnInit() { ngOnInit() {
@ -22,12 +29,20 @@ export class SidebarComponent implements OnInit {
console.log('ChannelListComponent - StandUps', standups); console.log('ChannelListComponent - StandUps', standups);
this.standups = standups; this.standups = standups;
}); });
this.loadChannels().subscribe(channels => {
console.log('ChannelListComponent - Channels', channels);
this.channels = channels;
});
} }
loadStandUps(): Observable<StandUp[]> { loadStandUps(): Observable<StandUp[]> {
return this.standupSevice.getStandUps(); return this.standupSevice.getStandUps();
} }
loadChannels(): Observable<Channel[]> {
return this.channelService.getChannels();
}
createStandUp() { createStandUp() {
this.standupSevice.addOne(new StandUp('test', 'test')).subscribe( this.standupSevice.addOne(new StandUp('test', 'test')).subscribe(
data => console.log(data), data => console.log(data),
@ -35,4 +50,14 @@ export class SidebarComponent implements OnInit {
) )
} }
createChannel() {
if (!this.supaService.userProfile.id) {
return;
}
this.channelService.addOne(new Channel(this.supaService.userProfile.id, 'test', 'test')).subscribe(
data => console.log(data),
error => console.error(error)
)
}
} }