diff --git a/functions/src/database/groups/index.ts b/functions/src/database/groups/index.ts new file mode 100644 index 00000000..474e22c8 --- /dev/null +++ b/functions/src/database/groups/index.ts @@ -0,0 +1,108 @@ +/*********************************************************************************************** +* Nonprofit Social Networking Platform: Allowing Users and Organizations to Collaborate. +* Copyright (C) 2023 ASCENDynamics NFP +* +* This file is part of Nonprofit Social Networking Platform. +* +* Nonprofit Social Networking Platform is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Nonprofit Social Networking Platform is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with Nonprofit Social Networking Platform. If not, see . +***********************************************************************************************/ +import * as functions from "firebase-functions"; +import * as admin from "firebase-admin"; +import * as logger from "firebase-functions/logger"; + +const db = admin.firestore(); + +export const onGroupCreation = functions.firestore + .document("groups/{groupId}") + .onCreate(handleGroupCreation); + +/** + * Asynchronously handles the creation of groups in the database. + * This function logs the group creation, and depending on the status of the new group, + * creates a corresponding relationship in the database. + * @param {Object} snapshot - The snapshot of the group document that was created. + * @param {Object} context - The context in which the function is run, including event context. + * @return {Promise} - Returns a promise that resolves when the operation is complete + */ +async function handleGroupCreation(snapshot: any, context: any) { + logger.info("snapshot: ", snapshot); + logger.info("context: ", context); + const newGroup = snapshot.data(); + if (newGroup) { + logger.info("newGroup: ", newGroup); + // Use a transaction to ensure atomic update + return db + .runTransaction(async (transaction) => { + const userDataSnapshot = await fetchUser( + transaction, + newGroup.createdBy, + ); + logger.info("userDataSnapshot: ", userDataSnapshot); + if (userDataSnapshot.exists) { + const userData = userDataSnapshot.data(); + logger.info("userData: ", userData); + const relationshipData = { + createdAt: admin.firestore.FieldValue.serverTimestamp(), + createdBy: newGroup.createdBy, + id: null, + senderId: newGroup.createdBy, + receiverId: context.params.groupId, + type: "member", + status: "accepted", + membershipRole: "admin", + receiverRelationship: "group", + senderRelationship: "user", + senderName: userData?.displayName, // Assuming the user data has a 'displayName' field + senderImage: userData?.profilePicture, // Assuming the user data has an 'profilePicture' field + senderTagline: userData?.tagline, // Assuming the user data has a 'tagline' field + receiverName: newGroup.name, // Assuming the group data has a 'name' field + receiverImage: newGroup.logoImage, // Assuming the group data has an 'logoImage' field + receiverTagline: newGroup.tagline, // Assuming the group data has a 'tagline' field + lastModifiedAt: admin.firestore.FieldValue.serverTimestamp(), + lastModifiedBy: newGroup.createdBy, + }; + + await transaction.set( + db.collection("relationships").doc(), + relationshipData, + ); + } + }) + .catch((error) => { + logger.error("Transaction failed: ", error); + throw error; + }); + } +} + +/** + * Fetches a user document based on the provided userId + * @param {admin.firestore.Transaction} transaction - The Firestore transaction + * @param {string} userId - The ID of the user to fetch + * @return {Promise} A promise that resolves with the fetched user document + */ +async function fetchUser( + transaction: admin.firestore.Transaction, + userId: string, +) { + const userRef = db.collection("users").doc(userId); + const userDoc = await transaction.get(userRef); + + if (!userDoc.exists) { + logger.error("User not found"); + throw new Error("User not found"); + } + + return userDoc; +} diff --git a/functions/src/database/relationships/index.ts b/functions/src/database/relationships/index.ts index 21013285..e5ea34b4 100644 --- a/functions/src/database/relationships/index.ts +++ b/functions/src/database/relationships/index.ts @@ -1,3 +1,22 @@ +/*********************************************************************************************** +* Nonprofit Social Networking Platform: Allowing Users and Organizations to Collaborate. +* Copyright (C) 2023 ASCENDynamics NFP +* +* This file is part of Nonprofit Social Networking Platform. +* +* Nonprofit Social Networking Platform is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Nonprofit Social Networking Platform is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with Nonprofit Social Networking Platform. If not, see . +***********************************************************************************************/ import * as functions from "firebase-functions"; import * as admin from "firebase-admin"; import * as logger from "firebase-functions/logger"; diff --git a/functions/src/index.ts b/functions/src/index.ts index 1e6e56e7..1b59bc19 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,3 +1,23 @@ +/*********************************************************************************************** +* Nonprofit Social Networking Platform: Allowing Users and Organizations to Collaborate. +* Copyright (C) 2023 ASCENDynamics NFP +* +* This file is part of Nonprofit Social Networking Platform. +* +* Nonprofit Social Networking Platform is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Nonprofit Social Networking Platform is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with Nonprofit Social Networking Platform. If not, see . +***********************************************************************************************/ + /** * Import function triggers from their respective submodules: * @@ -17,10 +37,5 @@ // logger.info("Hello logs!", {structuredData: true}); // response.send("Hello from Firebase!"); // }); -import { - onRelationshipCreation, - onRelationshipDeletion, - onRelationshipUpdate, -} from "./database/relationships"; // relationships triggers - -export {onRelationshipCreation, onRelationshipDeletion, onRelationshipUpdate}; // relationships triggers +export * from "./database/relationships"; // triggers +export * from "./database/groups"; // triggers diff --git a/src/app/core/services/groups.service.ts b/src/app/core/services/groups.service.ts index ce02e3c1..77239f75 100644 --- a/src/app/core/services/groups.service.ts +++ b/src/app/core/services/groups.service.ts @@ -58,12 +58,19 @@ export class GroupsService { private successHandler: SuccessHandlerService, ) {} - async createGroup(group: AppGroup): Promise { + async createGroup(group: Partial): Promise { const loading = await this.loadingController.create(); await loading.present(); + const userId = this.authService.getCurrentUser()?.uid; + if (userId) { + group.admins = [userId]; + group.members = [userId]; + } + group.logoImage = "assets/icon/favicon.png"; + group.heroImage = "assets/image/orghero.png"; return await addDoc( collection(this.firestoreService.firestore, this.collectionName), - prepareDataForCreate(group, this.authService.getCurrentUser()?.uid), + prepareDataForCreate(group, userId), ) .then((docRef) => { this.successHandler.handleSuccess("Request sent successfully!"); diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts index 3ec1380f..e072cebc 100644 --- a/src/app/models/user.model.ts +++ b/src/app/models/user.model.ts @@ -40,6 +40,7 @@ export interface AppUser { email: string; // Email address emailVerified: boolean; // Whether the user's email is verified name: string; // First and last name + heroImage: string; // base64 string profilePicture: string; // base64 string dateOfBirth: Timestamp; // Birthday language: string; // User's language diff --git a/src/app/modules/group/components/create-group-modal/create-group-modal.component.html b/src/app/modules/group/components/create-group-modal/create-group-modal.component.html new file mode 100644 index 00000000..22fc7145 --- /dev/null +++ b/src/app/modules/group/components/create-group-modal/create-group-modal.component.html @@ -0,0 +1,56 @@ + + + + Create New Group + + Close + + + + + +
+ + Group Name * + + + + + Group Description * + + + + + Group Tagline * + + + + + Create Group + +
+
diff --git a/src/app/modules/group/components/create-group-modal/create-group-modal.component.scss b/src/app/modules/group/components/create-group-modal/create-group-modal.component.scss new file mode 100644 index 00000000..641816b5 --- /dev/null +++ b/src/app/modules/group/components/create-group-modal/create-group-modal.component.scss @@ -0,0 +1,19 @@ +/*********************************************************************************************** +* Nonprofit Social Networking Platform: Allowing Users and Organizations to Collaborate. +* Copyright (C) 2023 ASCENDynamics NFP +* +* This file is part of Nonprofit Social Networking Platform. +* +* Nonprofit Social Networking Platform is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Nonprofit Social Networking Platform is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with Nonprofit Social Networking Platform. If not, see . +***********************************************************************************************/ diff --git a/src/app/modules/group/components/create-group-modal/create-group-modal.component.spec.ts b/src/app/modules/group/components/create-group-modal/create-group-modal.component.spec.ts new file mode 100644 index 00000000..e55f1df5 --- /dev/null +++ b/src/app/modules/group/components/create-group-modal/create-group-modal.component.spec.ts @@ -0,0 +1,42 @@ +/*********************************************************************************************** +* Nonprofit Social Networking Platform: Allowing Users and Organizations to Collaborate. +* Copyright (C) 2023 ASCENDynamics NFP +* +* This file is part of Nonprofit Social Networking Platform. +* +* Nonprofit Social Networking Platform is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Nonprofit Social Networking Platform is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with Nonprofit Social Networking Platform. If not, see . +***********************************************************************************************/ +import {ComponentFixture, TestBed, waitForAsync} from "@angular/core/testing"; +import {IonicModule} from "@ionic/angular"; + +import {CreateGroupModalComponent} from "./create-group-modal.component"; + +describe("CreateGroupModalComponent", () => { + let component: CreateGroupModalComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IonicModule.forRoot()], + }).compileComponents(); + + fixture = TestBed.createComponent(CreateGroupModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/group/components/create-group-modal/create-group-modal.component.ts b/src/app/modules/group/components/create-group-modal/create-group-modal.component.ts new file mode 100644 index 00000000..9f070de0 --- /dev/null +++ b/src/app/modules/group/components/create-group-modal/create-group-modal.component.ts @@ -0,0 +1,67 @@ +/*********************************************************************************************** +* Nonprofit Social Networking Platform: Allowing Users and Organizations to Collaborate. +* Copyright (C) 2023 ASCENDynamics NFP +* +* This file is part of Nonprofit Social Networking Platform. +* +* Nonprofit Social Networking Platform is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* Nonprofit Social Networking Platform is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with Nonprofit Social Networking Platform. If not, see . +***********************************************************************************************/ +import {Component} from "@angular/core"; +import {IonicModule, ModalController} from "@ionic/angular"; +import {CommonModule} from "@angular/common"; +import { + FormBuilder, + FormsModule, + ReactiveFormsModule, + Validators, +} from "@angular/forms"; +import {GroupsService} from "../../../../core/services/groups.service"; +import {AppGroup} from "../../../../models/group.model"; + +@Component({ + selector: "app-create-group-modal", + templateUrl: "./create-group-modal.component.html", + styleUrls: ["./create-group-modal.component.scss"], + standalone: true, + imports: [CommonModule, IonicModule, FormsModule, ReactiveFormsModule], +}) +export class CreateGroupModalComponent { + groupForm = this.fb.group({ + name: ["", Validators.required], + description: ["", Validators.required], + tagline: ["", Validators.required], + }); + + constructor( + private modalCtrl: ModalController, + private fb: FormBuilder, + private groupsService: GroupsService, + ) {} + + cancel() { + return this.modalCtrl.dismiss(null, "cancel"); + } + + confirm() { + return this.modalCtrl.dismiss(null, "confirm"); + } + + onSubmit() { + this.groupsService + .createGroup(this.groupForm.value as Partial) + .then((groupId) => { + return this.modalCtrl.dismiss({groupId: groupId}, "confirm"); + }); + } +} diff --git a/src/app/modules/group/pages/group-list/group-list.page.html b/src/app/modules/group/pages/group-list/group-list.page.html index b991bb06..5bbbbeaf 100644 --- a/src/app/modules/group/pages/group-list/group-list.page.html +++ b/src/app/modules/group/pages/group-list/group-list.page.html @@ -51,9 +51,9 @@ > diff --git a/src/app/modules/group/pages/group-profile/components/hero/hero.component.html b/src/app/modules/group/pages/group-profile/components/hero/hero.component.html index 23964de3..8de2b158 100644 --- a/src/app/modules/group/pages/group-profile/components/hero/hero.component.html +++ b/src/app/modules/group/pages/group-profile/components/hero/hero.component.html @@ -29,6 +29,9 @@

{{ group.tagline }}

### Hours Volunteer
+ diff --git a/src/app/modules/group/pages/group-profile/components/hero/hero.component.ts b/src/app/modules/group/pages/group-profile/components/hero/hero.component.ts index 408f7be6..33f824a7 100644 --- a/src/app/modules/group/pages/group-profile/components/hero/hero.component.ts +++ b/src/app/modules/group/pages/group-profile/components/hero/hero.component.ts @@ -31,7 +31,8 @@ import {AppGroup} from "../../../../../../models/group.model"; }) export class HeroComponent implements OnInit { @Input() group: Partial | null = null; // define your user here - + @Input() isMember: boolean = false; + @Input() isPendingMember: boolean = false; constructor() {} ngOnInit() {} diff --git a/src/app/modules/group/pages/group-profile/group-profile.page.html b/src/app/modules/group/pages/group-profile/group-profile.page.html index 87988738..889e12ae 100644 --- a/src/app/modules/group/pages/group-profile/group-profile.page.html +++ b/src/app/modules/group/pages/group-profile/group-profile.page.html @@ -35,7 +35,12 @@ - + @@ -45,7 +50,7 @@ diff --git a/src/app/modules/group/pages/group-profile/group-profile.page.ts b/src/app/modules/group/pages/group-profile/group-profile.page.ts index e84b6399..0572e75b 100644 --- a/src/app/modules/group/pages/group-profile/group-profile.page.ts +++ b/src/app/modules/group/pages/group-profile/group-profile.page.ts @@ -49,23 +49,23 @@ import {GroupListComponent} from "./components/group-list/group-list.component"; ], }) export class GroupProfilePage implements OnInit { - groupId: string | null; + groupId: string | null = ""; group: Partial | null = {}; - user: any; memberList: AppRelationship[] = []; groupList: AppRelationship[] = []; - canEdit: boolean = false; + isAdmin: boolean = false; + isMember: boolean = false; + isPendingMember: boolean = false; constructor( private authService: AuthService, private route: ActivatedRoute, private menuService: MenuService, private groupsService: GroupsService, private relationshipsCollectionService: RelationshipsCollectionService, - ) { - this.groupId = this.route.snapshot.paramMap.get("groupId"); - } + ) {} ngOnInit() { + this.groupId = this.route.snapshot.paramMap.get("groupId"); this.getGroup(); this.relationshipsCollectionService .getRelationships(this.groupId) @@ -97,10 +97,17 @@ export class GroupProfilePage implements OnInit { .getGroupById(this.groupId) .then((group) => { this.group = group; - let userId = this.authService?.getCurrentUser()?.uid; - this.canEdit = userId + let user = this.authService.getCurrentUser(); + let userId = user?.uid ? user.uid : ""; + this.isAdmin = userId ? this.group?.admins?.includes(userId) || false : false; + this.isMember = userId + ? this.group?.members?.includes(userId) || false + : false; + this.isPendingMember = userId + ? this.group?.pendingMembers?.includes(userId) || false + : false; }) .catch((error) => { console.log(error); diff --git a/src/app/modules/user/pages/user-profile/components/hero/hero.component.html b/src/app/modules/user/pages/user-profile/components/hero/hero.component.html index 0331f8d8..00d9a2db 100644 --- a/src/app/modules/user/pages/user-profile/components/hero/hero.component.html +++ b/src/app/modules/user/pages/user-profile/components/hero/hero.component.html @@ -18,11 +18,7 @@ along with Nonprofit Social Networking Platform. If not, see . --> - Silhouette of mountains + {{ user.displayName }} 101 Hours Volunteer diff --git a/src/app/modules/user/pages/user-profile/components/hero/hero.component.ts b/src/app/modules/user/pages/user-profile/components/hero/hero.component.ts index ba60d5a3..d652175b 100644 --- a/src/app/modules/user/pages/user-profile/components/hero/hero.component.ts +++ b/src/app/modules/user/pages/user-profile/components/hero/hero.component.ts @@ -20,7 +20,7 @@ import {CommonModule} from "@angular/common"; import {Component, Input, OnInit} from "@angular/core"; import {IonicModule} from "@ionic/angular"; -import {User} from "firebase/auth"; +import {AppUser} from "../../../../../../models/user.model"; // import your user model @Component({ selector: "app-hero", @@ -30,7 +30,7 @@ import {User} from "firebase/auth"; imports: [IonicModule, CommonModule], }) export class HeroComponent implements OnInit { - @Input() user: User | null = null; // define your user here + @Input() user: AppUser | null = null; // define your user here constructor() {} diff --git a/src/app/shared/components/menu/menu.component.html b/src/app/shared/components/menu/menu.component.html index 2554dc3f..4a3b1a67 100644 --- a/src/app/shared/components/menu/menu.component.html +++ b/src/app/shared/components/menu/menu.component.html @@ -30,13 +30,13 @@ - + - + + User + {{ p.title }} + {{ p.buttonText }} + + @@ -87,11 +112,11 @@ - + - + + User + {{ p.title }} + + {{ p.title }} - --> + + + ASCENDynamics NFP Information + + + + {{ p.title }} + +