Frontend part of the "RS App Clone" application, developed with Angular, using modern practices and tools.
This project is the frontend part of the "RS App Clone" application, developed with Angular. It includes user authentication, data management, and other features, demonstrating modern Angular development approaches.
- Node.js 22+
- Angular CLI 20+
- Firebase project setup
- Install dependencies
pnpm i
- Start the development server
pnpm run start
- Run tests
pnpm run test
pnpm run test:e2e
- Build for production
pnpm run build
This project uses environment files to store configuration for different environments.
To run the project, you need to create src/environments/environment.ts
and src/environments/environment.prod.ts
files from their templates.
src/environments/environment.ts.template
src/environments/environment.prod.ts.template
In your terminal, run the following commands to copy the files:
cp src/environments/environment.ts.template src/environments/environment.ts
cp src/environments/environment.prod.ts.template src/environments/environment.prod.ts
After creating the files, you must fill in the placeholder values (e.g., YOUR_API_KEY
) with a valid Firebase project configuration.
You have two options:
Option A: Use Your Own Firebase Project (Recommended)
This is the best approach for isolated development, allowing you to experiment safely. You can use the free "Spark" plan.
- Go to the Firebase Console and create a new project.
- In your project, go to Project Settings (click the โ๏ธ gear icon in the sidebar).
- In the General tab, scroll down to Your apps and click the Web icon (
</>
) to register a new web app. - After registration, Firebase will provide a
firebaseConfig
object containing your keys. - Copy the values from this object into your
environment.ts
andenvironment.prod.ts
files.
Option B: Use the Shared Development Environment
If you need to work with the project's shared database for team collaboration, you can get the keys from the project owner.
- Contact the project owner via Telegram at https://t.me/pestler to request the keys.
Important: Never commit your environment.ts
or environment.prod.ts
files to Git. They are already listed in .gitignore
to prevent accidental exposure of secret keys.
For CI/CD pipelines, you can create the environment files during the build process using environment variables or secrets.
For example, you can have a script that creates the environment.ts
file:
echo "export const environment = { production: false, firebaseConfig: { apiKey: '${FIREBASE_API_KEY}', authDomain: '${FIREBASE_AUTH_DOMAIN}', projectId: '${FIREBASE_PROJECT_ID}', storageBucket: '${FIREBASE_STORAGE_BUCKET}', messagingSenderId: '${FIREBASE_MESSAGING_SENDER_ID}', appId: '${FIREBASE_APP_ID}' }, apiUrl: 'http://localhost:3000/api' };" > src/environments/environment.ts
The application follows a modular architecture with a clear separation of concerns:
src/app/layout/
- Layout components and shared UIsrc/app/pages/
- Page components for different routessrc/app/shared/
- Shared services, components, and utilitiessrc/app/core/
- Core application logic, services, models, and data storessrc/app/core/models/
- TypeScript interfaces and types
- Modern Angular Patterns: Usage of Signals, computed values, effects
- State Management: Using services and reactive approaches for application state management.
- Authentication: Integration with Firebase Auth for route protection.
- Responsive Design: Support for various devices and screen sizes.
- Testing: Unit and E2E test coverage.
- Performance: Lazy loading of modules, image optimization.
- Accessibility: Improving accessibility for users with disabilities.
- Error Handling: User-friendly error messages and monitoring.
The application utilizes Firestore as its primary database. Below is an overview of the main collections and their typical document structures:
Stores user profile information. Each document represents a UserProfile
.
- Document ID: User's GitHub ID.
- Fields:
githubId
: (string) Unique GitHub identifier.id
: (number) Internal user ID.displayName
: (string) User's display name.primaryEmail
: (string) Primary email address.firstName
: (string) User's first name.lastName
: (string) User's last name.location
: (string) User's general location.active
: (boolean) Indicates if the user is active.cityName
: (string) City of residence.countryName
: (string) Country of residence.epamEmail
: (string, optional) EPAM email address.courses
: (string[], optional) List of course aliases the user is associated with.roles
: (object) User roles (e.g.,student
,mentor
,admin
).
Stores information about each course. Each document represents a Course
.
- Document ID: Course alias (e.g., 'angular-2025q3').
- Fields:
id
: (number) Unique course ID.name
: (string) Full name of the course.startDate
: (string) Start date of the course.logo
: (string) Path or name of the course logo.alias
: (string) Unique alias for the course.usePrivateRepositories
: (boolean) Indicates if private repositories are used for the course.maxCourseScore
: (number, optional) Maximum possible score for the course.completed
: (boolean) Indicates if the course is completed.
Stores student-specific data for a particular course. Each document represents ScoreData
.
- Document ID: Student's GitHub ID.
- Fields:
id
: (string) Student's internal ID.name
: (string) Student's name.githubId
: (string) Student's GitHub ID.active
: (boolean) Indicates if the student is active in this course.cityName
: (string) Student's city.countryName
: (string) Student's country.rank
: (number) Student's rank in the course.totalScore
: (number) Student's total score in the course.mentor
: (object, optional) Mentor details for the student.totalScoreChangeDate
: (string) Date of last score change.crossCheckScore
: (number) Score from cross-checks.repositoryLastActivityDate
: (string, optional) Date of last repository activity.repository
: (string, optional) URL of the student's repository.
Stores results for individual tasks completed by a student in a course. Each document represents TaskResultDoc
.
- Document ID: Task ID.
- Fields:
id
: (string) Task ID.score
: (number) Score obtained for the task.
Stores aggregated statistics about courses. Each document represents CourseStatistics
.
- Document ID: Typically a single document (e.g., a fixed ID like 'ZlY12vO9qy29M4a9v03l').
- Fields:
studentsCountries
: (object) Statistics on student countries.studentsStats
: (object) Overall student statistics (e.g.,activeStudentsCount
).mentorsCountries
: (object) Statistics on mentor countries.mentorsStats
: (object) Overall mentor statistics.courseTasks
: (array) List of tasks associated with courses.studentsCertificatesCountries
: (object) Statistics on student certificate countries.
The application primarily interacts with Firebase Firestore as its backend. Below is an overview of how the application's services interact with Firestore, serving as its API.
- The application uses Firebase Firestore as its backend.
- Data interaction is primarily handled through
FirestoreService
and other domain-specific services (AuthService
,UserService
,CourseService
,DashboardService
).
Provides generic methods for interacting with Firestore collections and documents.
getCollection(path, converter)
: Fetches a collection of documents.getDoc(path, docId, converter)
: Fetches a single document.setDoc(path, docId, data)
: Sets or updates a document.getCollectionCount(path)
: Retrieves the count of documents in a specified collection.getFilteredCollectionCount(path, field, value)
: Retrieves the count of documents in a collection based on a filter.
Handles user authentication and retrieves user-specific course data.
getScoreData(courseAlias)
: Retrieves a student's score data for a specific course from thecourses/{courseAlias}/students/{githubId}
path.
Manages user profile data.
getUserProfile(githubId)
: Retrieves a user's profile from theusers/{githubId}
path.saveUserProfile(githubId, data)
: Saves or updates a user's profile.doesUserProfileExist(githubId)
: Checks for the existence of a user profile.
Manages course-related data.
getCourses()
: Fetches all available courses from thecourses
collection.
Aggregates and processes data for the student dashboard.
getDashboardData(student, courseAlias)
: Gathers various data points for the student dashboard, fetching information fromcourses/{courseAlias}/students/{githubId}/taskResults
,courses/{courseAlias}/tasks
, andcourseStatistics
.
Imagine your app speaks TypeScript (with its nice, structured objects) and Firestore speaks its own database language (simple key-value pairs). Converters are like a special translator that helps these two talk to each other smoothly!
What they do: When you send data from your app to Firestore, the converter takes your TypeScript object and turns it into something Firestore understands. When you get data back from Firestore, the converter takes that raw data and turns it back into your familiar TypeScript object.
Why use them?
- No more manual mapping: You don't have to manually pick out each piece of data. The converter does it automatically.
- Keeps your data safe: It makes sure the data you read and write always matches the types you defined in your TypeScript code.
- Handles the ID magic: Firestore gives each document a unique ID. Converters make it easy to automatically add this ID to your TypeScript object when you read it, and remove it when you save (because Firestore manages the ID itself).
- Custom transformations: Need to change a date format or set a default value? Converters let you do that during the translation process.
How they work (the two main parts):
Each converter has two important methods:
-
toFirestore(yourObject)
: Sending data to Firestore- This method runs when you're saving or updating data in Firestore.
- It takes your TypeScript object (e.g., a
ScoreData
object) and prepares it for storage. - Example: You might tell it to ignore the
id
property because Firestore will generate its own.
-
fromFirestore(snapshot)
: Getting data from Firestore- This method runs when you're reading data from Firestore.
- It takes the raw data from a Firestore document (
snapshot
) and rebuilds your TypeScript object. - Example: This is where you'd typically grab the document's
id
fromsnapshot.id
and add it to yourScoreData
object.
Let's see an example with ScoreData
:
// 1. Your TypeScript data model (e.g., in dashboard.models.ts)
export interface ScoreData {
id: string; // This will be the Firestore document ID
totalScore: number;
// ... other fields like githubId, rank, etc.
}
// 2. Your Converter (e.g., also in dashboard.models.ts)
// This tells Firestore how to translate ScoreData objects
export const scoreDataConverter: FirestoreDataConverter<ScoreData> = {
// When sending ScoreData to Firestore:
toFirestore: (scoreData: ScoreData): DocumentData => {
// We don't want to save the 'id' property *in-side* the document,
// because Firestore uses it as the document's unique identifier.
const { id, ...rest } = scoreData; // So, we separate 'id' from the rest of the data
return rest; // And send only the 'rest' of the properties
},
// When getting data from Firestore:
fromFirestore: (snapshot: QueryDocumentSnapshot, options: SnapshotOptions): ScoreData => {
const data = snapshot.data(options); // Get the raw data from the document
return {
id: snapshot.id, // Take the document's ID and put it into our 'id' property
totalScore: data['totalScore'],
// ... map other fields from 'data' to your ScoreData object
} as ScoreData; // Tell TypeScript this is a ScoreData object
},
};
// 3. How you'd use it in a service (e.g., in auth.service.ts)
// When you get a reference to a Firestore document, you "attach" the converter to it.
// Now, any read/write operations on this 'studentDocRef' will use scoreDataConverter!
//
// const studentDocRef = doc(this.firestore, `courses/${courseAlias}/students/${githubId}`).withConverter(scoreDataConverter);
// return docData(studentDocRef); // This will automatically give you a ScoreData object!