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/app.routes.ts b/src/app/app.routes.ts
index c62ab4a..1bbfd67 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -2,7 +2,9 @@ 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';
+import { UserStoriesComponent } from './components/my-stories/my-stories';
export const routes: Routes = [
{
@@ -17,4 +19,16 @@ export const routes: Routes = [
path: 'viewStory',
component: DisplayStory,
},
+ {
+ path: 'signin',
+ component: Signin,
+ },
+ {
+ path: 'signup',
+ component: Signup,
+ },
+ {
+ path: 'my-stories',
+ component: UserStoriesComponent,
+ },
];
diff --git a/src/app/components/display-story/display-story.html b/src/app/components/display-story/display-story.html
index 1f4f776..3b99374 100644
--- a/src/app/components/display-story/display-story.html
+++ b/src/app/components/display-story/display-story.html
@@ -68,13 +68,22 @@
@if(!checkFeedbackSubmitted()){
-
-
+
+
+
+
} }
diff --git a/src/app/components/display-story/display-story.ts b/src/app/components/display-story/display-story.ts
index 0bcea50..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';
@@ -32,6 +32,9 @@ import { of } from 'rxjs';
import { TestimonialDialog } from '../ui/dialog-box/testimonial-dialog';
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',
@@ -47,6 +50,7 @@ import { StoryService } from '../../services/story.service';
MatBadgeModule,
CommonModule,
SocialShare,
+ MatSnackBarModule,
],
})
export class DisplayStory implements OnInit, OnDestroy {
@@ -64,8 +68,16 @@ export class DisplayStory implements OnInit, OnDestroy {
speakingSignal = signal(false);
storyAudio = signal([]);
storyLanguage = signal('');
+ storyId = signal('');
testimonialDialog = inject(MatDialog);
+ readonly authStore = inject(AuthStore);
+
+ private readonly saveStoryService = inject(StorySaveService);
+ private readonly snackBar = inject(MatSnackBar);
+ private readonly router = inject(Router);
+
+ readonly isLoggedIn = this.authStore.isLoggedIn;
// For modal content
modalContent = signal<{
@@ -97,6 +109,7 @@ export class DisplayStory implements OnInit, OnDestroy {
return;
}
});
+ this.storyId.set(id);
if (isPlatformServer(this.platformId)) {
this.storyService.getStory(id).subscribe((storyData) => {
this.isLoading.set(false);
@@ -341,6 +354,30 @@ export class DisplayStory implements OnInit, OnDestroy {
handleSpeech(shouldSpeak: boolean) {
this.speakingSignal.set(shouldSpeak);
}
+ async onSaveStory() {
+ const uid = this.authStore.currentUser?.uid;
+ if (uid && this.storyId()) {
+ 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',
+ });
+ }
+ }
+ }
}
@Component({
diff --git a/src/app/components/my-stories/my-stories.css b/src/app/components/my-stories/my-stories.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/components/my-stories/my-stories.html b/src/app/components/my-stories/my-stories.html
new file mode 100644
index 0000000..c4680bc
--- /dev/null
+++ b/src/app/components/my-stories/my-stories.html
@@ -0,0 +1,68 @@
+
+ Your Stories
+
+
+ @if (!loading() && userStories().length > 0) { @for (story of userStories();
+ track story.id) {
+
+
+
+
+
+
+ } } @else if (!loading()) {
+
No stories saved yet.
+ } @else {
+
+ @for (_ of [1, 2]; track _) {
+
+ } }
+
+
+
+
+@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
new file mode 100644
index 0000000..a87aece
--- /dev/null
+++ b/src/app/components/my-stories/my-stories.ts
@@ -0,0 +1,73 @@
+import { Component, inject, effect, signal } from '@angular/core';
+import { StorySaveService } from '../../services/save.story.service';
+import { AuthStore } from '../../services/auth.store';
+import { CommonModule } from '@angular/common';
+import { RouterLink } from '@angular/router';
+
+@Component({
+ selector: 'app-my-stories',
+ templateUrl: './my-stories.html',
+ styleUrls: ['./my-stories.css'],
+ imports: [CommonModule, RouterLink],
+})
+export class UserStoriesComponent {
+ userStories = signal([]);
+ loading = signal(true);
+ isModalOpen = signal(false);
+ selectedStoryId = signal(null);
+
+ private readonly storyService = inject(StorySaveService);
+ private readonly authService = inject(AuthStore);
+
+ constructor() {
+ // reactively respond to auth changes
+ effect(async () => {
+ const user = this.authService.user(); // signal
+ if (user?.uid) {
+ this.loading.set(true);
+ try {
+ const stories = await this.storyService.getUserStory(user.uid);
+
+ const storiesArray = stories.map((v) => ({ ...v.data(), id: v.id }));
+ this.userStories.set(storiesArray);
+ console.log(this.userStories());
+ } catch (err) {
+ console.error('Error loading stories:', err);
+ } finally {
+ this.loading.set(false);
+ }
+ }
+ });
+ }
+
+ 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();
+ }
+ }
+}
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..a155bfd
--- /dev/null
+++ b/src/app/components/signin/signin.html
@@ -0,0 +1,61 @@
+
+
+
+
+
Kidlytics
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if(error()) {
+
Could not Sign In
+ }
+
+
diff --git a/src/app/components/signin/signin.ts b/src/app/components/signin/signin.ts
new file mode 100644
index 0000000..2e4886e
--- /dev/null
+++ b/src/app/components/signin/signin.ts
@@ -0,0 +1,64 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ inject,
+ signal,
+} 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],
+ templateUrl: './signin.html',
+ styleUrl: './signin.css',
+})
+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);
+
+ 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);
+ 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);
+ } finally {
+ this.loading.set(false);
+ }
+ }
+}
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..7ff65c9
--- /dev/null
+++ b/src/app/components/signup/signup.html
@@ -0,0 +1,54 @@
+
+
+
+
+
Kidlytics
+
+
+
+
+
+
+
+ @if(error()) {
+
Error while Sign up
+ }
+
+
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.html b/src/app/header/header.html
index 52a5716..5ba9cef 100644
--- a/src/app/header/header.html
+++ b/src/app/header/header.html
@@ -2,6 +2,16 @@
+
+ @if(authStore.isLoggedIn()){
+
+ Saved Stories
+
+
+ }
+
+ @if(authStore.isLoggedIn()){
+
+
+ }@else{
+
+
+ }
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.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);
+ }
+}
diff --git a/src/app/services/auth.store.ts b/src/app/services/auth.store.ts
new file mode 100644
index 0000000..492f6c8
--- /dev/null
+++ b/src/app/services/auth.store.ts
@@ -0,0 +1,23 @@
+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);
+ });
+ }
+
+ // useful getter
+ get currentUser(): User | null {
+ return this._user();
+ }
+}
diff --git a/src/app/services/save.story.service.ts b/src/app/services/save.story.service.ts
new file mode 100644
index 0000000..79dd057
--- /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';
+import { NO_OF_SAVED_STORIES_ALLOWED } from '../../constants/limits.constants';
+
+@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 >= NO_OF_SAVED_STORIES_ALLOWED) {
+ 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);
+ 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..13b88dc
--- /dev/null
+++ b/src/constants/limits.constants.ts
@@ -0,0 +1 @@
+export const NO_OF_SAVED_STORIES_ALLOWED = 5;