From 4f6fb771c41457e37c9beac1719cecc9bde94c56 Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Wed, 17 Sep 2025 18:36:17 +0500 Subject: [PATCH 01/10] chore: firebase auth service --- firebase.ts | 2 ++ src/app/services/auth.service.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/app/services/auth.service.ts diff --git a/firebase.ts b/firebase.ts index f3c1b26..8494fec 100644 --- a/firebase.ts +++ b/firebase.ts @@ -3,6 +3,7 @@ import { initializeApp } from 'firebase/app'; import { getFirestore } from 'firebase/firestore'; import { environment } from './src/environments/environment'; import { getStorage } from 'firebase/storage'; +import { getAuth } from 'firebase/auth'; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries @@ -21,3 +22,4 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export const storage = getStorage(app); +export const auth = getAuth(app); diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..ff203ad --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,31 @@ +import { Injectable, inject } from '@angular/core'; +import { + GoogleAuthProvider, + signInWithPopup, + signInWithEmailAndPassword, + createUserWithEmailAndPassword, + signOut, + UserCredential, +} from 'firebase/auth'; + +import { auth } from '../../../firebase'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + googleSignIn(): Promise { + const provider = new GoogleAuthProvider(); + return signInWithPopup(auth, provider); + } + + emailSignIn(email: string, password: string): Promise { + return signInWithEmailAndPassword(auth, email, password); + } + + emailSignup(email: string, password: string): Promise { + return createUserWithEmailAndPassword(auth, email, password); + } + + logout(): Promise { + return signOut(auth); + } +} From ad4c86b75ffe7088a4db45678cdcbfd7dfceb349 Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Wed, 17 Sep 2025 18:36:55 +0500 Subject: [PATCH 02/10] chore: signin and signup UI with routes --- src/app/app.routes.ts | 11 ++++- src/app/components/signin/signin.css | 0 src/app/components/signin/signin.html | 61 +++++++++++++++++++++++++++ src/app/components/signup/signup.css | 0 src/app/components/signup/signup.html | 54 ++++++++++++++++++++++++ src/app/header/header.html | 15 +++++++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/app/components/signin/signin.css create mode 100644 src/app/components/signin/signin.html create mode 100644 src/app/components/signup/signup.css create mode 100644 src/app/components/signup/signup.html diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c62ab4a..26b2055 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -2,7 +2,8 @@ import { Routes } from '@angular/router'; import { Story } from './story/story'; import { Home } from './home/home'; import { DisplayStory } from './components/display-story/display-story'; - +import { Signin } from './components/signin/signin'; +import { Signup } from './components/signup/signup'; export const routes: Routes = [ { @@ -17,4 +18,12 @@ export const routes: Routes = [ path: 'viewStory', component: DisplayStory, }, + { + path: 'signin', + component: Signin, + }, + { + path: 'signup', + component: Signup, + }, ]; diff --git a/src/app/components/signin/signin.css b/src/app/components/signin/signin.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/signin/signin.html b/src/app/components/signin/signin.html new file mode 100644 index 0000000..469dc0f --- /dev/null +++ b/src/app/components/signin/signin.html @@ -0,0 +1,61 @@ +
+
+ +
+

Kidlytics

+

AI Powered Story Teller

+
+ + +
+ + + + + +
+ + +
+
+ or +
+
+ + + + + + @if(error()) { +

Could not Sign In

+ } +
+
diff --git a/src/app/components/signup/signup.css b/src/app/components/signup/signup.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/signup/signup.html b/src/app/components/signup/signup.html new file mode 100644 index 0000000..2022463 --- /dev/null +++ b/src/app/components/signup/signup.html @@ -0,0 +1,54 @@ +
+
+ +
+

Kidlytics

+

+ AI Powered Story Teller for Kids +

+
+ + +
+ + + + + + +
+ + + @if(error()) { +

Error while Sign up

+ } +
+
diff --git a/src/app/header/header.html b/src/app/header/header.html index 52a5716..70e67be 100644 --- a/src/app/header/header.html +++ b/src/app/header/header.html @@ -37,4 +37,19 @@ + + @if(authStore.isLoggedIn()){ + + + }@else{ + + + } From a5c536d71c6722f77de78757ebdd18cd62e4d105 Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Wed, 17 Sep 2025 18:37:11 +0500 Subject: [PATCH 03/10] chor: business logic and state management of auth --- src/app/components/signin/signin.ts | 60 +++++++++++++++++++++++++++++ src/app/components/signup/signup.ts | 56 +++++++++++++++++++++++++++ src/app/header/header.ts | 15 +++++++- src/app/services/auth.store.ts | 24 ++++++++++++ 4 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/app/components/signin/signin.ts create mode 100644 src/app/components/signup/signup.ts create mode 100644 src/app/services/auth.store.ts diff --git a/src/app/components/signin/signin.ts b/src/app/components/signin/signin.ts new file mode 100644 index 0000000..ef88229 --- /dev/null +++ b/src/app/components/signin/signin.ts @@ -0,0 +1,60 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + signal, +} from '@angular/core'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../../services/auth.service'; +@Component({ + selector: 'app-signin', + imports: [ReactiveFormsModule], + templateUrl: './signin.html', + styleUrl: './signin.css', +}) +export class Signin { + private readonly auth = inject(AuthService); + private readonly fb = inject(FormBuilder); + + readonly loading = signal(false); + readonly error = signal(null); + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + + readonly isValid = computed(() => this.form.valid); + + async onEmailLogin(): Promise { + if (!this.form.valid) return; + this.loading.set(true); + this.error.set(null); + + try { + const { email, password } = this.form.getRawValue(); + const user = await this.auth.emailSignIn(email, password); + console.log(user); + } catch (err: unknown) { + this.error.set((err as Error).message); + } finally { + this.loading.set(false); + } + } + + async onGoogleLogin(): Promise { + this.loading.set(true); + this.error.set(null); + + try { + const user = await this.auth.googleSignIn(); + console.log(user); + // show toast, save user object in local storage and navigate to create page + } catch (err: unknown) { + this.error.set((err as Error).message); + } finally { + this.loading.set(false); + } + } +} diff --git a/src/app/components/signup/signup.ts b/src/app/components/signup/signup.ts new file mode 100644 index 0000000..f37d707 --- /dev/null +++ b/src/app/components/signup/signup.ts @@ -0,0 +1,56 @@ +import { + ChangeDetectionStrategy, + Component, + signal, + inject, +} from '@angular/core'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../../services/auth.service'; +@Component({ + selector: 'app-signup', + imports: [ReactiveFormsModule], + templateUrl: './signup.html', + styleUrl: './signup.css', +}) +export class Signup { + private readonly auth = inject(AuthService); + private readonly fb = inject(FormBuilder); + + readonly loading = signal(false); + readonly error = signal(null); + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + + async onSignup(): Promise { + if (!this.form.valid) return; + this.loading.set(true); + this.error.set(null); + + try { + const { email, password } = this.form.getRawValue(); + const newUser = await this.auth.emailSignup(email, password); + console.log(newUser); + } catch (err: unknown) { + this.error.set((err as Error).message); + } finally { + this.loading.set(false); + } + } + async onGoogleLogin(): Promise { + this.loading.set(true); + this.error.set(null); + + try { + const user = await this.auth.googleSignIn(); + console.log(user); + // show toast, save user object in local storage and navigate to create page + } catch (err: unknown) { + this.error.set((err as Error).message); + } finally { + this.loading.set(false); + } + } +} diff --git a/src/app/header/header.ts b/src/app/header/header.ts index c83957c..0b3705e 100644 --- a/src/app/header/header.ts +++ b/src/app/header/header.ts @@ -1,10 +1,21 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { MatTooltip } from '@angular/material/tooltip'; import { RouterLink } from '@angular/router'; +import { AuthStore } from '../services/auth.store'; +import { AuthService } from '../services/auth.service'; @Component({ selector: 'app-header', imports: [MatTooltip, RouterLink], + standalone: true, templateUrl: './header.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class Header {} +export class Header { + readonly authStore = inject(AuthStore); + readonly authService = inject(AuthService); + + onLogout() { + this.authService.logout(); + } +} diff --git a/src/app/services/auth.store.ts b/src/app/services/auth.store.ts new file mode 100644 index 0000000..d6b3c35 --- /dev/null +++ b/src/app/services/auth.store.ts @@ -0,0 +1,24 @@ +import { Injectable, inject, signal, computed } from '@angular/core'; +import { onAuthStateChanged, User } from 'firebase/auth'; +import { auth } from '../../../firebase'; + +@Injectable({ providedIn: 'root' }) +export class AuthStore { + // signals + private readonly _user = signal(null); + readonly user = computed(() => this._user()); + readonly isLoggedIn = computed(() => !!this._user()); + + constructor() { + // Subscribe to Firebase auth changes + onAuthStateChanged(auth, (firebaseUser) => { + this._user.set(firebaseUser); + console.log('STate changed', this.isLoggedIn()); + }); + } + + // useful getter + get currentUser(): User | null { + return this._user(); + } +} From afb31da075b1b282970970ed76f98f626d07e1ea Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Thu, 18 Sep 2025 19:55:05 +0500 Subject: [PATCH 04/10] chore: save story service --- src/app/services/save.story.service.ts | 101 +++++++++++++++++++++++++ src/constants/limits.constants.ts | 1 + 2 files changed, 102 insertions(+) create mode 100644 src/app/services/save.story.service.ts create mode 100644 src/constants/limits.constants.ts diff --git a/src/app/services/save.story.service.ts b/src/app/services/save.story.service.ts new file mode 100644 index 0000000..7c498dd --- /dev/null +++ b/src/app/services/save.story.service.ts @@ -0,0 +1,101 @@ +import { Injectable } from '@angular/core'; +import { + doc, + updateDoc, + arrayUnion, + collection, + query, + where, + getDocs, + getDoc, + arrayRemove, +} from 'firebase/firestore'; + +import { db } from '../../../firebase'; + +@Injectable({ + providedIn: 'root', +}) +export class StorySaveService { + async saveStory( + documentId: string, + uid: string + ): Promise<{ success: boolean; message: string }> { + try { + const storiesRef = collection(db, 'stories'); + + // Step 1: Count how many stories this user has saved + const q = query(storiesRef, where('savedBy', 'array-contains', uid)); + const snapshot = await getDocs(q); + const savedCount = snapshot.size; + + if (savedCount >= 5) { + return { + success: false, + message: 'You can only save up to 5 stories.', + }; + } + + // Step 2: Check if this story already has the uid in savedBy + const storyRef = doc(db, `stories/${documentId}`); + const storySnap = await getDoc(storyRef); + + if (storySnap.exists()) { + const data = storySnap.data(); + const alreadySaved = + Array.isArray(data['savedBy']) && data['savedBy'].includes(uid); + + if (alreadySaved) { + return { + success: false, + message: 'You have already saved this story.', + }; + } + } + + // Step 3: Add uid to savedBy array + await updateDoc(storyRef, { + savedBy: arrayUnion(uid), + }); + + return { success: true, message: 'Story saved successfully.' }; + } catch (error: any) { + console.error('Error saving story:', error); + return { + success: false, + message: error.message || 'Failed to save story.', + }; + } + } + + async getUserStory(uid: string) { + const storiesRef = collection(db, 'stories'); + const q = query(storiesRef, where('savedBy', 'array-contains', uid)); + const snapshot = await getDocs(q); + console.log(snapshot.docs); + return snapshot.docs; + } + async removeSavedStory( + documentId: string, + uid: string + ): Promise<{ success: boolean; message: string }> { + try { + const storyRef = doc(db, `stories/${documentId}`); + + await updateDoc(storyRef, { + savedBy: arrayRemove(uid), + }); + + return { + success: true, + message: 'Story removed from saved list.', + }; + } catch (error: any) { + console.error('Error removing saved story:', error); + return { + success: false, + message: error.message || 'Failed to remove saved story.', + }; + } + } +} diff --git a/src/constants/limits.constants.ts b/src/constants/limits.constants.ts new file mode 100644 index 0000000..95574d7 --- /dev/null +++ b/src/constants/limits.constants.ts @@ -0,0 +1 @@ +const NO_OF_SAVED_STORIES_ALLOWED = 5; From 50262f46bf077d52b91e25e2979a391ed98e1fc2 Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Thu, 18 Sep 2025 19:55:30 +0500 Subject: [PATCH 05/10] chore: UI links and routing --- src/app/app.routes.ts | 5 +++++ src/app/components/signin/signin.ts | 4 ++++ src/app/header/header.html | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 26b2055..1bbfd67 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,6 +4,7 @@ import { Home } from './home/home'; import { DisplayStory } from './components/display-story/display-story'; import { Signin } from './components/signin/signin'; import { Signup } from './components/signup/signup'; +import { UserStoriesComponent } from './components/my-stories/my-stories'; export const routes: Routes = [ { @@ -26,4 +27,8 @@ export const routes: Routes = [ path: 'signup', component: Signup, }, + { + path: 'my-stories', + component: UserStoriesComponent, + }, ]; diff --git a/src/app/components/signin/signin.ts b/src/app/components/signin/signin.ts index ef88229..2e4886e 100644 --- a/src/app/components/signin/signin.ts +++ b/src/app/components/signin/signin.ts @@ -7,6 +7,7 @@ import { } from '@angular/core'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { AuthService } from '../../services/auth.service'; +import { Router } from '@angular/router'; @Component({ selector: 'app-signin', imports: [ReactiveFormsModule], @@ -17,6 +18,8 @@ export class Signin { private readonly auth = inject(AuthService); private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); + readonly loading = signal(false); readonly error = signal(null); @@ -50,6 +53,7 @@ export class Signin { try { const user = await this.auth.googleSignIn(); console.log(user); + this.router.navigate(['/story']); // show toast, save user object in local storage and navigate to create page } catch (err: unknown) { this.error.set((err as Error).message); diff --git a/src/app/header/header.html b/src/app/header/header.html index 70e67be..5ba9cef 100644 --- a/src/app/header/header.html +++ b/src/app/header/header.html @@ -2,6 +2,16 @@ + + @if(authStore.isLoggedIn()){ + + Saved Stories + + + }
} } @else if (!loading()) { @@ -40,3 +48,21 @@

{{ story.name }}

} }
+ + +@if (isModalOpen()) { + + + +} \ No newline at end of file diff --git a/src/app/components/my-stories/my-stories.ts b/src/app/components/my-stories/my-stories.ts index 04195b0..ffa9c27 100644 --- a/src/app/components/my-stories/my-stories.ts +++ b/src/app/components/my-stories/my-stories.ts @@ -13,6 +13,8 @@ import { RouterLink } from '@angular/router'; export class UserStoriesComponent { userStories = signal([]); loading = signal(true); + isModalOpen = signal(false); + selectedStoryId = signal(null); private readonly storyService = inject(StorySaveService); private readonly authService = inject(AuthStore); @@ -38,4 +40,35 @@ export class UserStoriesComponent { } }); } -} + + openConfirmationModal(storyId: string): void { + this.selectedStoryId.set(storyId); + this.isModalOpen.set(true); + } + + closeConfirmationModal(): void { + this.isModalOpen.set(false); + this.selectedStoryId.set(null); + } + + async confirmUnsave(): Promise { + const storyId = this.selectedStoryId(); + const user = this.authService.user(); + + if (storyId && user?.uid) { + const result = await this.storyService.removeSavedStory( + storyId, + user.uid + ); + if (result.success) { + this.userStories.update((stories) => + stories.filter((story) => story.id !== storyId) + ); + } else { + // Optional: handle error with a user-facing message + console.error('Failed to unsave story:', result.message); + } + this.closeConfirmationModal(); + } + } +} \ No newline at end of file From f8fcf0aa301f23c1bf33034474f6865fbad67213 Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Tue, 23 Sep 2025 19:52:17 +0500 Subject: [PATCH 09/10] chore: snack bar on save and unsave --- .../components/display-story/display-story.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/app/components/display-story/display-story.ts b/src/app/components/display-story/display-story.ts index 74a4ca4..c9f2132 100644 --- a/src/app/components/display-story/display-story.ts +++ b/src/app/components/display-story/display-story.ts @@ -1,5 +1,5 @@ import { SocialShare } from '../social-share/social-share'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { StoryPartWithImg } from '../../model/story.type'; import { doc, getDoc } from 'firebase/firestore'; import { db } from '../../../../firebase'; @@ -34,6 +34,7 @@ import { generateStoryPdf } from '../../utils/pdfGenertor'; import { StoryService } from '../../services/story.service'; import { AuthStore } from '../../services/auth.store'; import { StorySaveService } from '../../services/save.story.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-display-story', @@ -49,6 +50,7 @@ import { StorySaveService } from '../../services/save.story.service'; MatBadgeModule, CommonModule, SocialShare, + MatSnackBarModule, ], }) export class DisplayStory implements OnInit, OnDestroy { @@ -72,6 +74,8 @@ export class DisplayStory implements OnInit, OnDestroy { readonly authStore = inject(AuthStore); private readonly saveStoryService = inject(StorySaveService); + private readonly snackBar = inject(MatSnackBar); + private readonly router = inject(Router); readonly isLoggedIn = this.authStore.isLoggedIn; @@ -350,11 +354,28 @@ export class DisplayStory implements OnInit, OnDestroy { handleSpeech(shouldSpeak: boolean) { this.speakingSignal.set(shouldSpeak); } - onSaveStory() { - console.log('Saving Story...'); + async onSaveStory() { const uid = this.authStore.currentUser?.uid; if (uid && this.storyId()) { - this.saveStoryService.saveStory(this.storyId(), uid); + const result = await this.saveStoryService.saveStory(this.storyId(), uid); + + if (result.success) { + const snackBarRef = this.snackBar.open(result.message, 'View Stories', { + duration: 5000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + + snackBarRef.onAction().subscribe(() => { + this.router.navigate(['/my-stories']); + }); + } else { + this.snackBar.open(result.message, 'Close', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'bottom', + }); + } } } } From e581c3c745689f065958126a8604327ba60155f6 Mon Sep 17 00:00:00 2001 From: Hashir Jamal Date: Fri, 26 Sep 2025 11:06:12 +0500 Subject: [PATCH 10/10] chore: code cleanup --- src/app/components/my-stories/my-stories.ts | 3 +-- src/app/components/signin/signin.html | 2 +- src/app/components/signup/signup.html | 2 +- src/app/services/auth.store.ts | 1 - src/app/services/save.story.service.ts | 4 ++-- src/constants/limits.constants.ts | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app/components/my-stories/my-stories.ts b/src/app/components/my-stories/my-stories.ts index ffa9c27..a87aece 100644 --- a/src/app/components/my-stories/my-stories.ts +++ b/src/app/components/my-stories/my-stories.ts @@ -31,7 +31,6 @@ export class UserStoriesComponent { const storiesArray = stories.map((v) => ({ ...v.data(), id: v.id })); this.userStories.set(storiesArray); console.log(this.userStories()); - this.loading.set(false); } catch (err) { console.error('Error loading stories:', err); } finally { @@ -71,4 +70,4 @@ export class UserStoriesComponent { this.closeConfirmationModal(); } } -} \ No newline at end of file +} diff --git a/src/app/components/signin/signin.html b/src/app/components/signin/signin.html index 469dc0f..a155bfd 100644 --- a/src/app/components/signin/signin.html +++ b/src/app/components/signin/signin.html @@ -30,7 +30,7 @@

Kidlytics