Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
16 changes: 15 additions & 1 deletion src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand All @@ -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,
},
];
23 changes: 16 additions & 7 deletions src/app/components/display-story/display-story.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,22 @@ <h1 class="font-bold text-2xl md:text-3xl text-center">

<!-- Only display button if feedback is not submitted -->
@if(!checkFeedbackSubmitted()){

<button
class="btn btn-primary block my-4 m-auto"
(click)="openTestimonialDialog()"
>
Give Your Feedback
</button>
<div class="flex justify-center gap-4">
<button
class="btn btn-primary my-4"
[disabled]="!isLoggedIn()"
(click)="onSaveStory()"
[matTooltipDisabled]="isLoggedIn()"
matTooltip="Please Log In to save the story"
matTooltipPosition="above"
>
<mat-icon class="mx-auto block"> save</mat-icon>
Save Story
</button>
<button class="btn btn-primary my-4" (click)="openTestimonialDialog()">
Give Your Feedback
</button>
</div>
} }
</div>

Expand Down
39 changes: 38 additions & 1 deletion src/app/components/display-story/display-story.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -47,6 +50,7 @@ import { StoryService } from '../../services/story.service';
MatBadgeModule,
CommonModule,
SocialShare,
MatSnackBarModule,
],
})
export class DisplayStory implements OnInit, OnDestroy {
Expand All @@ -64,8 +68,16 @@ export class DisplayStory implements OnInit, OnDestroy {
speakingSignal = signal(false);
storyAudio = signal<string[]>([]);
storyLanguage = signal<string>('');
storyId = signal<string>('');

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<{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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({
Expand Down
Empty file.
68 changes: 68 additions & 0 deletions src/app/components/my-stories/my-stories.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<section class="py-12 px-6 bg-base-100">
<h2 class="text-center text-3xl font-bold mb-10">Your Stories</h2>

<div class="max-w-3xl mx-auto flex flex-col gap-6">
@if (!loading() && userStories().length > 0) { @for (story of userStories();
track story.id) {
<div class="card bg-base-200 shadow-xl min-h-48">
<figure class="relative aspect-video overflow-hidden">
<img
[src]="story.storyParts[0].imageUri"
alt="{{ story.name }}"
class="absolute inset-0 w-full h-full object-cover"
/>
</figure>
<div class="card-body text-center items-center">
<h3 class="card-title text-lg h-16 text-white">{{ story.name }}</h3>
<div class="card-actions justify-center gap-2">
<a
class="btn btn-primary"
[routerLink]="'/viewStory'"
[queryParams]="{ id: story.id }"
>
Read Story
</a>
<button
class="btn btn-secondary"
(click)="openConfirmationModal(story.id)"
>
Unsave
</button>
</div>
</div>
</div>
} } @else if (!loading()) {
<p class="text-center text-gray-400">No stories saved yet.</p>
} @else {
<!-- Skeleton loader -->
@for (_ of [1, 2]; track _) {
<div class="card bg-base-200 shadow-xl min-h-48 animate-pulse">
<figure class="h-48 bg-base-300">
<div class="skeleton w-full h-full"></div>
</figure>
<div class="card-body text-center items-center space-y-4">
<div class="skeleton h-6 w-3/4"></div>
<div class="skeleton h-10 w-24"></div>
</div>
</div>
} }
</div>
</section>

<!-- Confirmation Modal -->
@if (isModalOpen()) {
<dialog class="modal modal-open modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="font-bold text-lg">Confirm Unsave</h3>
<p class="py-4">
Are you sure you want to unsave this story? This action cannot be undone.
</p>
<div class="modal-action">
<button class="btn btn-ghost" (click)="closeConfirmationModal()">
Cancel
</button>
<button class="btn btn-error" (click)="confirmUnsave()">Unsave</button>
</div>
</div>
</dialog>
}
73 changes: 73 additions & 0 deletions src/app/components/my-stories/my-stories.ts
Original file line number Diff line number Diff line change
@@ -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<any[]>([]);
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace 'any[]' with a proper type definition. Consider creating an interface for the story data structure to improve type safety.

Copilot uses AI. Check for mistakes.
loading = signal(true);
isModalOpen = signal(false);
selectedStoryId = signal<string | null>(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);
Comment on lines +36 to +37
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate loading.set(false) calls. The loading state is set to false both inside the try block (line 34) and in the finally block (line 38). Remove the one from line 34 to avoid redundancy.

Copilot uses AI. Check for mistakes.
}
}
});
}

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<void> {
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();
}
}
}
Empty file.
61 changes: 61 additions & 0 deletions src/app/components/signin/signin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<div
class="min-h-screen flex items-center justify-center bg-dracula text-dracula-foreground"
>
<div class="w-full max-w-md p-8 rounded-2xl shadow-lg bg-dracula-selection">
<!-- Branding -->
<div class="text-center mb-6">
<h1 class="text-3xl font-bold text-primary">Kidlytics</h1>
<p class="text-sm text-dracula-comment mt-1">AI Powered Story Teller</p>
</div>

<!-- Login Form -->
<form
[formGroup]="form"
(ngSubmit)="onEmailLogin()"
class="flex flex-col gap-4"
>
<input
class="p-3 rounded-lg border border-dracula-purple bg-dracula text-dracula-foreground focus:ring-2 focus:ring-primary transition"
type="email"
formControlName="email"
placeholder="Email"
/>

<input
class="p-3 rounded-lg border border-dracula-purple bg-dracula text-dracula-foreground focus:ring-2 focus:ring-primary transition"
type="password"
formControlName="password"
placeholder="Password"
/>

<button
type="submit"
class="btn-primary p-3 rounded-lg text-white font-bold transition hover:scale-105 disabled:opacity-50 cursor-pointer border border-5 shadow-lg"
[disabled]="form.invalid || loading()"
>
{{ loading() ? 'Logging in...' : 'Login' }}
</button>
</form>

<!-- Divider -->
<div class="flex items-center my-6">
<div class="flex-grow border-t border-dracula-purple"></div>
<span class="px-3 text-dracula-comment text-sm">or</span>
<div class="flex-grow border-t border-dracula-purple"></div>
</div>

<!-- Google login -->
<button
class="btn btn-sm btn-primary w-full hover:btn-ghost cursor-pointer"
(click)="onGoogleLogin()"
[disabled]="loading()"
>
Continue with Google
</button>

<!-- Error -->
@if(error()) {
<p class="text-sm text-red-400 mt-3 text-center">Could not Sign In</p>
}
</div>
</div>
Loading