Skip to content

Commit

Permalink
feat(badges): new badges and manual add
Browse files Browse the repository at this point in the history
  • Loading branch information
uatisdeproblem committed Apr 14, 2023
1 parent 9b1c656 commit a3688fc
Show file tree
Hide file tree
Showing 19 changed files with 1,260 additions and 32 deletions.
10 changes: 9 additions & 1 deletion back-end/deploy/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ const tables: { [tableName: string]: DDBTable } = {
},
questionsUpvotes: {
PK: { name: 'questionId', type: DDB.AttributeType.STRING },
SK: { name: 'userId', type: DDB.AttributeType.STRING }
SK: { name: 'userId', type: DDB.AttributeType.STRING },
indexes: [
{
indexName: 'inverted-index',
partitionKey: { name: 'userId', type: DDB.AttributeType.STRING },
sortKey: { name: 'questionId', type: DDB.AttributeType.STRING },
projectionType: DDB.ProjectionType.ALL
}
]
},
usersBadges: {
PK: { name: 'userId', type: DDB.AttributeType.STRING },
Expand Down
53 changes: 37 additions & 16 deletions back-end/src/handlers/badges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ class BadgesRC extends ResourceController {
this.galaxyUser = new User(event.requestContext.authorizer.lambda.user);
}

protected async checkAuthBeforeRequest(): Promise<void> {
if (!this.resourceId) return;
protected async getResources(): Promise<UserBadge[]> {
const userId =
this.queryParams.userId && this.galaxyUser.isAdministrator ? this.queryParams.userId : this.galaxyUser.userId;
let usersBadges: UserBadge[] = await ddb.query({
TableName: DDB_TABLES.usersBadges,
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': userId }
});
usersBadges = usersBadges.map(x => new UserBadge(x));
return usersBadges.sort((a, b): number => b.earnedAt.localeCompare(a.earnedAt));
}

protected async getResource(): Promise<UserBadge> {
try {
this.userBadge = new UserBadge(
await ddb.get({
Expand All @@ -42,19 +52,7 @@ class BadgesRC extends ResourceController {
} catch (err) {
throw new RCError('Badge not found');
}
}

protected async getResources(): Promise<UserBadge[]> {
let usersBadges: UserBadge[] = await ddb.query({
TableName: DDB_TABLES.usersBadges,
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': this.galaxyUser.userId }
});
usersBadges = usersBadges.map(x => new UserBadge(x));
return usersBadges.sort((a, b): number => b.earnedAt.localeCompare(a.earnedAt));
}

protected async getResource(): Promise<UserBadge> {
if (!this.userBadge.firstSeenAt) {
this.userBadge.firstSeenAt = new Date().toISOString();
await ddb.update({
Expand All @@ -66,11 +64,34 @@ class BadgesRC extends ResourceController {
}
return this.userBadge;
}

protected async postResource(): Promise<void> {
if (!this.galaxyUser.isAdministrator) throw new RCError('Unauthorized');

const { userId } = this.queryParams;
const badge = this.resourceId as Badges;

if (!userId) throw new RCError('No target user');
if (!badge || !Object.values(Badges).includes(badge)) throw new RCError('Invalid badge');

await addBadgeToUser(ddb, userId, badge);
}

protected async deleteResource(): Promise<void> {
if (!this.galaxyUser.isAdministrator) throw new RCError('Unauthorized');

const { userId } = this.queryParams;
const badge = this.resourceId as Badges;

if (!userId) throw new RCError('No target user');

await ddb.delete({ TableName: DDB_TABLES.usersBadges, Key: { userId, badge } });
}
}

export const addBadgeToUser = async (ddb: DynamoDB, user: User, badge: Badges): Promise<void> => {
export const addBadgeToUser = async (ddb: DynamoDB, userId: string, badge: Badges): Promise<void> => {
try {
const userBadge = new UserBadge({ userId: user.userId, badge });
const userBadge = new UserBadge({ userId, badge });
await ddb.put({
TableName: DDB_TABLES.usersBadges,
Item: userBadge,
Expand Down
26 changes: 22 additions & 4 deletions back-end/src/handlers/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ class Questions extends ResourceController {

await this.sendNotificationToTopicSubjects(this.topic, this.question);

await addBadgeToUser(ddb, this.galaxyUser, Badges.FIRST_QUESTION);
await addBadgeToUser(ddb, this.galaxyUser.userId, Badges.FIRST_QUESTION);
if ((await this.getNumQuestionsMadeByUser()) >= 10)
await addBadgeToUser(ddb, this.galaxyUser, Badges.QUESTIONS_MASTER);
if (toISODate(new Date()) === '2023-04-14') await addBadgeToUser(ddb, this.galaxyUser, Badges.PEER_PRESSURE_MINHO);
await addBadgeToUser(ddb, this.galaxyUser.userId, Badges.QUESTIONS_MASTER);
if (toISODate(new Date()) === '2023-04-14')
await addBadgeToUser(ddb, this.galaxyUser.userId, Badges.PEER_PRESSURE_MINHO);

return this.question;
}
Expand Down Expand Up @@ -155,7 +156,11 @@ class Questions extends ResourceController {
this.question.numOfUpvotes = await this.getLiveNumUpvotes();
await ddb.put({ TableName: DDB_TABLES.questions, Item: this.question });

await addBadgeToUser(ddb, this.galaxyUser, Badges.NEWCOMER);
if (!cancel) {
await addBadgeToUser(ddb, this.galaxyUser.userId, Badges.NEWCOMER);
if ((await this.getNumQuestionsUpvotedByUser()) >= 10)
await addBadgeToUser(ddb, this.galaxyUser.userId, Badges.LOVE_GIVER);
}

return this.question;
}
Expand Down Expand Up @@ -243,4 +248,17 @@ class Questions extends ResourceController {
return 0;
}
}
private async getNumQuestionsUpvotedByUser(): Promise<number> {
try {
const upvotes = await ddb.query({
TableName: DDB_TABLES.questionsUpvotes,
IndexName: 'inverted-index',
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': this.galaxyUser.userId }
});
return upvotes.length;
} catch (error) {
return 0;
}
}
}
13 changes: 11 additions & 2 deletions back-end/src/handlers/topics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

import { DynamoDB, RCError, ResourceController, S3 } from 'idea-aws';
import { SignedURL } from 'idea-toolbox';

import { addBadgeToUser } from './badges';

import { TopicCategoryAttached } from '../models/category.model';
import { TopicEventAttached } from '../models/event.model';

import { RelatedTopic, Topic } from '../models/topic.model';
import { User } from '../models/user.model';
import { Badges } from '../models/userBadge.model';
import { SubjectTypes } from '../models/subject.model';

///
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
Expand Down Expand Up @@ -103,7 +107,12 @@ class Topics extends ResourceController {
this.topic = new Topic(this.body);
this.topic.topicId = await ddb.IUNID(PROJECT);

return await this.putSafeResource({ noOverwrite: true });
await this.putSafeResource({ noOverwrite: true });

const userSubjects = this.topic.subjects.filter(s => s.type === SubjectTypes.USER);
for (const user of userSubjects) await addBadgeToUser(ddb, user.id, Badges.RISING_STAR);

return this.topic;
}

protected async patchResources(): Promise<SignedURL> {
Expand Down
6 changes: 5 additions & 1 deletion back-end/src/models/userBadge.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,9 @@ export enum Badges {
PEER_PRESSURE_MINHO = 'PEER_PRESSURE_MINHO',
FIRST_QUESTION = 'FIRST_QUESTION',
QUESTIONS_MASTER = 'QUESTIONS_MASTER',
NEWCOMER = 'NEWCOMER'
NEWCOMER = 'NEWCOMER',
KNOW_IT_ALL = 'KNOW_IT_ALL',
LOVE_GIVER = 'LOVE_GIVER',
POKEMON_HUNTER = 'POKEMON_HUNTER',
RISING_STAR = 'RISING_STAR'
}
48 changes: 48 additions & 0 deletions back-end/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,54 @@ paths:
responses:
200:
$ref: '#/components/responses/Badge'
post:
summary: Add the badge to a user
description: Requires to be Administrator
tags: [Badges]
security:
- AuthFunction: []
parameters:
- name: badge
in: path
description: Badge
required: true
schema:
type: string
- name: userId
in: query
description: Target User ID
required: true
schema:
type: string
responses:
200:
$ref: '#/components/responses/Badge'
400:
$ref: '#/components/responses/BadParameters'
delete:
summary: Remove the badge from a user
description: Requires to be Administrator
tags: [Badges]
security:
- AuthFunction: []
parameters:
- name: badge
in: path
description: Badge
required: true
schema:
type: string
- name: userId
in: query
description: Target User ID
required: true
schema:
type: string
responses:
200:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'

components:
schemas:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<ion-header>
<ion-toolbar color="ideaToolbar">
<ion-buttons slot="start">
<ion-button [title]="'COMMON.CLOSE' | translate" (click)="close()">
<ion-icon icon="close-circle-outline" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>{{ 'CONFIGURATIONS.GIVE_A_BADGE' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list class="aList">
<ion-item>
<ion-label position="stacked">{{ 'CONFIGURATIONS.USER_ID' | translate }}</ion-label>
<ion-input [(ngModel)]="userId" [readonly]="!!badges"></ion-input>
<ion-button
slot="end"
class="ion-margin-top"
*ngIf="!badges"
[disabled]="!userId"
(click)="getUserBadges(userId)"
>
<ion-icon icon="search" slot="icon-only"></ion-icon>
</ion-button>
<ion-button slot="end" class="ion-margin-top" fill="clear" color="medium" *ngIf="badges" (click)="badges = null">
<ion-icon icon="arrow-undo" slot="icon-only"></ion-icon>
</ion-button>
</ion-item>
<ion-grid class="ion-margin-top badgesGrid" *ngIf="badges">
<ion-row class="ion-justify-content-center ion-align-items-center">
<ion-col class="ion-text-center" *ngFor="let badge of badges">
<ion-img [src]="_badges.getBadgeImage(badge)" (click)="_badges.openBadgeDetails(badge)"></ion-img>
<ion-button fill="clear" color="danger" (click)="removeBadgeFromUser(userId, badge)">
<ion-icon slot="icon-only" icon="trash"></ion-icon>
</ion-button>
</ion-col>
<ion-col class="ion-text-center">
<ion-button shape="round" (click)="addBadgeToUser(userId)">
<ion-icon slot="icon-only" icon="add"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-list>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ion-grid.badgesGrid {
ion-img {
margin: 0 auto;
width: 100px;
height: 100px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Component } from '@angular/core';
import { AlertController, ModalController } from '@ionic/angular';
import { IDEALoadingService, IDEAMessageService, IDEATranslationsService } from '@idea-ionic/common';

import { AppService } from '@app/app.service';
import { BadgesService } from '../../profile/badges/badges.service';

import { Badges, UserBadge } from '@models/userBadge.model';

@Component({
selector: 'app-give-badges',
templateUrl: 'giveBadges.component.html',
styleUrls: ['giveBadges.component.scss']
})
export class GiveBadgesComponent {
userId: string;
badges: UserBadge[];

constructor(
private modalCtrl: ModalController,
private alertCtrl: AlertController,
private loading: IDEALoadingService,
private message: IDEAMessageService,
private t: IDEATranslationsService,
public _badges: BadgesService,
public app: AppService
) {}

async getUserBadges(userId: string): Promise<void> {
if (!userId) return;
this.badges = null;
try {
await this.loading.show();
this.badges = await this._badges.getList({ userId, force: true });
} catch (error) {
this.message.error('COMMON.SOMETHING_WENT_WRONG');
} finally {
this.loading.hide();
}
}

async removeBadgeFromUser(userId: string, userBadge: UserBadge): Promise<void> {
const doRemove = async (): Promise<void> => {
try {
await this.loading.show();
await this._badges.removeBadgeFromUser(userId, userBadge.badge);
this.badges.splice(this.badges.indexOf(userBadge), 1);
this.message.success('COMMON.OPERATION_COMPLETED');
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
this.loading.hide();
}
};

const header = this.t._('COMMON.ARE_YOU_SURE');
const buttons = [{ text: this.t._('COMMON.CANCEL') }, { text: this.t._('COMMON.CONFIRM'), handler: doRemove }];
const alert = await this.alertCtrl.create({ header, buttons });
await alert.present();
}

async addBadgeToUser(userId: string): Promise<void> {
const header = this.t._('CONFIGURATIONS.GIVE_A_BADGE');
const subHeader = userId;
const inputs: any[] = Object.values(Badges).map(badge => ({
type: 'radio',
value: badge,
label: this.t._('PROFILE.BADGES.'.concat(badge))
}));

const doAdd = async (badge: Badges): Promise<void> => {
try {
await this.loading.show();
await this._badges.addBadgeToUser(userId, badge);
this.badges.unshift(new UserBadge({ userId, badge }));
this.message.success('COMMON.OPERATION_COMPLETED');
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
this.loading.hide();
}
};
const buttons = [
{ text: this.t._('COMMON.CANCEL'), role: 'cancel' },
{ text: this.t._('COMMON.CONFIRM'), handler: doAdd }
];

const alert = await this.alertCtrl.create({ header, subHeader, inputs, buttons });
alert.present();
}

close(): void {
this.modalCtrl.dismiss();
}
}
Loading

0 comments on commit a3688fc

Please sign in to comment.