├── .editorconfig
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── README.md
├── angular.json
├── package-lock.json
├── package.json
├── public
└── images
│ ├── American-Express-Color.WebP
│ ├── amazon-pay.WebP
│ ├── cart.WebP
│ ├── error-404.WebP
│ ├── get-apple-store.WebP
│ ├── get-google-play.WebP
│ ├── mastercard.webp
│ └── paypal.WebP
├── src
├── app
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.config.server.ts
│ ├── app.config.ts
│ ├── app.routes.server.ts
│ ├── app.routes.ts
│ ├── core
│ │ ├── enums
│ │ │ ├── cart.endpoints.ts
│ │ │ └── products.endpoints.ts
│ │ ├── environment
│ │ │ └── environment.ts
│ │ ├── interceptors
│ │ │ ├── error.interceptor.spec.ts
│ │ │ ├── error.interceptor.ts
│ │ │ ├── loading.interceptor.spec.ts
│ │ │ └── loading.interceptor.ts
│ │ ├── interfaces
│ │ │ └── products.interface.ts
│ │ └── services
│ │ │ ├── flowbite
│ │ │ ├── flowbite.service.spec.ts
│ │ │ └── flowbite.service.ts
│ │ │ └── products
│ │ │ ├── products.service.spec.ts
│ │ │ └── products.service.ts
│ ├── features
│ │ ├── layouts
│ │ │ ├── footer
│ │ │ │ ├── footer.component.html
│ │ │ │ ├── footer.component.scss
│ │ │ │ ├── footer.component.spec.ts
│ │ │ │ └── footer.component.ts
│ │ │ └── navbar
│ │ │ │ ├── navbar.component.html
│ │ │ │ ├── navbar.component.scss
│ │ │ │ ├── navbar.component.spec.ts
│ │ │ │ └── navbar.component.ts
│ │ └── pages
│ │ │ ├── cart
│ │ │ ├── cart.component.html
│ │ │ ├── cart.component.scss
│ │ │ ├── cart.component.spec.ts
│ │ │ └── cart.component.ts
│ │ │ ├── not-found
│ │ │ ├── not-found.component.html
│ │ │ ├── not-found.component.scss
│ │ │ ├── not-found.component.spec.ts
│ │ │ └── not-found.component.ts
│ │ │ ├── product-details
│ │ │ ├── product-details.component.html
│ │ │ ├── product-details.component.scss
│ │ │ ├── product-details.component.spec.ts
│ │ │ └── product-details.component.ts
│ │ │ └── products
│ │ │ ├── products.component.html
│ │ │ ├── products.component.scss
│ │ │ ├── products.component.spec.ts
│ │ │ └── products.component.ts
│ └── shared
│ │ ├── components
│ │ ├── UI
│ │ │ └── products-carousel
│ │ │ │ ├── products-carousel.component.html
│ │ │ │ ├── products-carousel.component.scss
│ │ │ │ ├── products-carousel.component.spec.ts
│ │ │ │ └── products-carousel.component.ts
│ │ └── business
│ │ │ ├── search-by-name
│ │ │ ├── search-by-name.component.html
│ │ │ ├── search-by-name.component.scss
│ │ │ ├── search-by-name.component.spec.ts
│ │ │ └── search-by-name.component.ts
│ │ │ ├── sort
│ │ │ ├── sort.component.html
│ │ │ ├── sort.component.scss
│ │ │ ├── sort.component.spec.ts
│ │ │ └── sort.component.ts
│ │ │ └── theme-toggle
│ │ │ ├── theme-toggle.component.html
│ │ │ ├── theme-toggle.component.scss
│ │ │ ├── theme-toggle.component.spec.ts
│ │ │ └── theme-toggle.component.ts
│ │ ├── pipes
│ │ ├── search.pipe.spec.ts
│ │ ├── search.pipe.ts
│ │ ├── sort.pipe.spec.ts
│ │ └── sort.pipe.ts
│ │ └── styles
│ │ ├── _utilities.scss
│ │ └── _variables.scss
├── index.html
├── main.server.ts
├── main.ts
├── server.ts
└── styles.scss
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 | ij_typescript_use_double_quotes = false
14 |
15 | [*.md]
16 | max_line_length = off
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🛍️ Angular E-Commerce Platform
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 | ---
10 |
11 | ## 📝 Overview
12 |
13 | **Products Gallery** is a modern, responsive Single Page Application (SPA) e-commerce platform built using **Angular 16**, **Tailwind CSS**, and **Flowbite**. The platform offers a real-world shopping experience with product browsing, sorting, filtering, a cart system, and more.
14 |
15 | Built with modular architecture, efficient lazy loading, and clean state management using Angular's latest features and RxJS.
16 |
17 | ---
18 |
19 | ## 🌟 Features
20 |
21 | - 🔍 **Product Catalog** with real-time search & filters
22 | - 🧱 Modular & Scalable architecture (standalone components)
23 | - 🖼️ **Product Details** with images, descriptions, and ratings
24 | - 📱 **Responsive Design** for all screen sizes
25 | - ⚙️ **Lazy Loading** for performance optimization
26 | - 🔀 **Sorting & Filtering** options
27 | - 🌐 **API Integration** with error/loading state handling
28 |
29 | ---
30 |
31 | ## 🚀 Live Demo & Video
32 |
33 | 🎥 [Video Walkthrough](https://drive.google.com/file/d/1RTnSZHMBaUGW2XdtYiRE-gaKCpqV7rZ7/view?usp=drive_link)
34 |
35 | 👉 [View Demo on Vercel](https://e-commerce-ten-sigma-28.vercel.app/#/home)
36 |
37 | ---
38 |
39 | ## 🧰 Tech Stack
40 |
41 | ### 🔧 Frontend
42 |
43 | | Technology | Purpose |
44 | |------------------|-----------------------------|
45 | | Angular 16 | Core SPA Framework |
46 | | Tailwind CSS | Utility-first CSS styling |
47 | | Flowbite | Pre-built Tailwind UI kits |
48 | | RxJS | State Management |
49 | | Angular Router | Navigation & Routing |
50 | | Reactive Forms | Form Handling |
51 | | Custom Pipes | Data Transformation (Search/Sort) |
52 |
53 | ### 📡 API Integration
54 |
55 | | Technology | Purpose |
56 | |------------------|-----------------------------|
57 | | Angular HTTP | API Communication |
58 | | FakeStoreAPI | Mock Product Data |
59 |
60 | ### 🧪 Developer Tools
61 |
62 | | Tool | Purpose |
63 | |------------------|-----------------------------|
64 | | Git | Version Control |
65 | | Git-CZ | Standardized Commits |
66 | | Vercel | Deployment |
67 |
68 | ---
69 |
70 | ## 📦 Installation
71 |
72 | ### ✅ Prerequisites
73 |
74 | - Node.js v16+
75 | - Angular CLI
76 |
77 | bash
78 | npm install -g @angular/cli
79 |
80 |
81 | ### 📥 Setup Instructions
82 |
83 | bash
84 | # Clone the repository
85 | git clone https://github.com/Reham-Gomaa/E-commerce.git
86 | cd E-commerce
87 |
88 | # Install dependencies
89 | npm install
90 |
91 | # Run the development server
92 | ng serve
93 |
94 |
95 | Visit: http://localhost:4200
96 |
97 | ### 🏗️ Build for Production
98 |
99 | bash
100 | ng build
101 |
102 |
103 | ---
104 |
105 | ## 📁 Project Structure
106 |
107 | 🗂️ [Project Structure](https://uithub.com/Reham-Gomaa/E-commerce)
108 |
109 | ---
110 |
111 | ## 🔎 Key Components
112 |
113 | - products-carousel - Interactive carousel for featured items
114 | - navbar & footer - Shared layout UI
115 | - cart - Shopping cart logic & display
116 | - search-by-name - Pipe & input for dynamic filtering
117 | - sort - Component for sorting logic
118 | - theme-toggle - Light/Dark mode toggle
119 | - interceptors - Error & loading interceptors for API calls
120 |
121 | ---
122 |
123 | ## 🤝 Contribution
124 |
125 | Contributions, issues, and feature requests are welcome!
126 | Feel free to fork the repository and submit a pull request.
127 |
128 | ---
129 |
130 | ## 🧾 License
131 |
132 | This project is licensed under the [MIT License](LICENSE).
133 |
134 | ---
135 |
136 | ## 🙌 Acknowledgements
137 |
138 | - [Angular](https://angular.io/)
139 | - [Tailwind CSS](https://tailwindcss.com/)
140 | - [Flowbite](https://flowbite.com/)
141 | - [FakeStoreAPI](https://fakestoreapi.com/)
142 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "gallery": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:application",
19 | "options": {
20 | "outputPath": "dist/gallery",
21 | "index": "src/index.html",
22 | "browser": "src/main.ts",
23 | "polyfills": ["zone.js"],
24 | "tsConfig": "tsconfig.app.json",
25 | "inlineStyleLanguage": "scss",
26 | "assets": [
27 | {
28 | "glob": "**/*",
29 | "input": "public"
30 | }
31 | ],
32 | "styles": [
33 | "./node_modules/ngx-spinner/animations/ball-atom.css",
34 | "./node_modules/ngx-toastr/toastr.css",
35 | "./node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.carousel.min.css",
36 | "./node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.theme.default.min.css",
37 | "./node_modules/@fortawesome/fontawesome-free/css/all.min.css",
38 | "src/styles.scss"
39 | ],
40 | "scripts": ["./node_modules/flowbite/dist/flowbite.min.js"],
41 | "server": "src/main.server.ts",
42 | "outputMode": "server",
43 | "ssr": {
44 | "entry": "src/server.ts"
45 | }
46 | },
47 | "configurations": {
48 | "production": {
49 | "budgets": [
50 | {
51 | "type": "initial",
52 | "maximumWarning": "500kB",
53 | "maximumError": "1MB"
54 | },
55 | {
56 | "type": "anyComponentStyle",
57 | "maximumWarning": "4kB",
58 | "maximumError": "8kB"
59 | }
60 | ],
61 | "outputHashing": "all"
62 | },
63 | "development": {
64 | "optimization": false,
65 | "extractLicenses": false,
66 | "sourceMap": true
67 | }
68 | },
69 | "defaultConfiguration": "production"
70 | },
71 | "serve": {
72 | "builder": "@angular-devkit/build-angular:dev-server",
73 | "configurations": {
74 | "production": {
75 | "buildTarget": "gallery:build:production"
76 | },
77 | "development": {
78 | "buildTarget": "gallery:build:development"
79 | }
80 | },
81 | "defaultConfiguration": "development"
82 | },
83 | "extract-i18n": {
84 | "builder": "@angular-devkit/build-angular:extract-i18n"
85 | },
86 | "test": {
87 | "builder": "@angular-devkit/build-angular:karma",
88 | "options": {
89 | "polyfills": ["zone.js", "zone.js/testing"],
90 | "tsConfig": "tsconfig.spec.json",
91 | "inlineStyleLanguage": "scss",
92 | "assets": [
93 | {
94 | "glob": "**/*",
95 | "input": "public"
96 | }
97 | ],
98 | "styles": ["src/styles.scss"],
99 | "scripts": []
100 | }
101 | }
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gallery",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test",
10 | "serve:ssr:gallery": "node dist/gallery/server/server.mjs",
11 | "commit": "git-cz"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^19.1.0",
16 | "@angular/common": "^19.1.0",
17 | "@angular/compiler": "^19.1.0",
18 | "@angular/core": "^19.1.0",
19 | "@angular/forms": "^19.1.0",
20 | "@angular/platform-browser": "^19.1.0",
21 | "@angular/platform-browser-dynamic": "^19.1.0",
22 | "@angular/platform-server": "^19.1.0",
23 | "@angular/router": "^19.1.0",
24 | "@angular/ssr": "^19.1.5",
25 | "@fortawesome/fontawesome-free": "^7.0.0",
26 | "express": "^4.18.2",
27 | "flowbite": "^3.1.2",
28 | "git-cz": "^4.9.0",
29 | "ngx-owl-carousel-o": "^19.0.2",
30 | "ngx-spinner": "^19.0.0",
31 | "ngx-toastr": "^19.0.0",
32 | "rxjs": "~7.8.0",
33 | "tslib": "^2.3.0",
34 | "zone.js": "~0.15.0"
35 | },
36 | "devDependencies": {
37 | "@angular-devkit/build-angular": "^19.1.5",
38 | "@angular/cli": "^19.1.5",
39 | "@angular/compiler-cli": "^19.1.0",
40 | "@types/express": "^4.17.17",
41 | "@types/jasmine": "~5.1.0",
42 | "@types/node": "^18.18.0",
43 | "autoprefixer": "^10.4.21",
44 | "jasmine-core": "~5.5.0",
45 | "karma": "~6.4.0",
46 | "karma-chrome-launcher": "~3.2.0",
47 | "karma-coverage": "~2.2.0",
48 | "karma-jasmine": "~5.1.0",
49 | "karma-jasmine-html-reporter": "~2.1.0",
50 | "postcss": "^8.5.6",
51 | "tailwindcss": "^3.4.17",
52 | "typescript": "~5.7.2"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/public/images/American-Express-Color.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/American-Express-Color.WebP
--------------------------------------------------------------------------------
/public/images/amazon-pay.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/amazon-pay.WebP
--------------------------------------------------------------------------------
/public/images/cart.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/cart.WebP
--------------------------------------------------------------------------------
/public/images/error-404.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/error-404.WebP
--------------------------------------------------------------------------------
/public/images/get-apple-store.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/get-apple-store.WebP
--------------------------------------------------------------------------------
/public/images/get-google-play.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/get-google-play.WebP
--------------------------------------------------------------------------------
/public/images/mastercard.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/mastercard.webp
--------------------------------------------------------------------------------
/public/images/paypal.WebP:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/public/images/paypal.WebP
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Loading...
7 |
8 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | min-height: 100vh;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { AppComponent } from './app.component';
3 |
4 | describe('AppComponent', () => {
5 | beforeEach(async () => {
6 | await TestBed.configureTestingModule({
7 | imports: [AppComponent],
8 | }).compileComponents();
9 | });
10 |
11 | it('should create the app', () => {
12 | const fixture = TestBed.createComponent(AppComponent);
13 | const app = fixture.componentInstance;
14 | expect(app).toBeTruthy();
15 | });
16 |
17 | it(`should have the 'gallery' title`, () => {
18 | const fixture = TestBed.createComponent(AppComponent);
19 | const app = fixture.componentInstance;
20 | expect(app.title).toEqual('gallery');
21 | });
22 |
23 | it('should render title', () => {
24 | const fixture = TestBed.createComponent(AppComponent);
25 | fixture.detectChanges();
26 | const compiled = fixture.nativeElement as HTMLElement;
27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, gallery');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { RouterOutlet } from '@angular/router';
3 | import { FooterComponent } from './features/layouts/footer/footer.component';
4 | import { NavbarComponent } from './features/layouts/navbar/navbar.component';
5 | import { NgxSpinnerComponent } from 'ngx-spinner';
6 |
7 | @Component({
8 | selector: 'app-root',
9 | imports: [
10 | RouterOutlet,
11 | FooterComponent,
12 | NavbarComponent,
13 | NgxSpinnerComponent,
14 | ],
15 | templateUrl: './app.component.html',
16 | styleUrl: './app.component.scss',
17 | })
18 | export class AppComponent {
19 | title = 'gallery';
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/app.config.server.ts:
--------------------------------------------------------------------------------
1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2 | import { provideServerRendering } from '@angular/platform-server';
3 | import { provideServerRouting } from '@angular/ssr';
4 | import { appConfig } from './app.config';
5 | import { serverRoutes } from './app.routes.server';
6 |
7 | const serverConfig: ApplicationConfig = {
8 | providers: [
9 | provideServerRendering(),
10 | provideServerRouting(serverRoutes)
11 | ]
12 | };
13 |
14 | export const config = mergeApplicationConfig(appConfig, serverConfig);
15 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { routes } from './app.routes';
2 |
3 | import {
4 | ApplicationConfig,
5 | importProvidersFrom,
6 | provideZoneChangeDetection,
7 | } from '@angular/core';
8 | import {
9 | provideRouter,
10 | withHashLocation,
11 | withViewTransitions,
12 | } from '@angular/router';
13 | import {
14 | provideClientHydration,
15 | withEventReplay,
16 | } from '@angular/platform-browser';
17 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
18 | import {
19 | provideHttpClient,
20 | withFetch,
21 | withInterceptors,
22 | } from '@angular/common/http';
23 |
24 | import { provideToastr } from 'ngx-toastr';
25 | import { NgxSpinnerModule } from 'ngx-spinner';
26 | import { loadingInterceptor } from './core/interceptors/loading.interceptor';
27 | import { errorInterceptor } from './core/interceptors/error.interceptor';
28 |
29 | export const appConfig: ApplicationConfig = {
30 | providers: [
31 | provideZoneChangeDetection({ eventCoalescing: true }),
32 | provideRouter(routes, withViewTransitions(), withHashLocation()),
33 | provideClientHydration(withEventReplay()),
34 | provideHttpClient(
35 | withFetch(),
36 | withInterceptors([loadingInterceptor, errorInterceptor])
37 | ),
38 | importProvidersFrom(BrowserAnimationsModule, NgxSpinnerModule),
39 | provideToastr({ tapToDismiss: true, timeOut: 2000 }),
40 | ],
41 | };
42 |
--------------------------------------------------------------------------------
/src/app/app.routes.server.ts:
--------------------------------------------------------------------------------
1 | import { RenderMode, ServerRoute } from '@angular/ssr';
2 |
3 | export const serverRoutes: ServerRoute[] = [
4 | {
5 | path: '**',
6 | renderMode: RenderMode.Server,
7 | },
8 | ];
9 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 | import { ProductsComponent } from './features/pages/products/products.component';
3 |
4 | export const routes: Routes = [
5 | { path: '', redirectTo: 'home', pathMatch: 'full' },
6 | { path: 'home', component: ProductsComponent, title: 'Home' },
7 | {
8 | path: 'details/:p_id',
9 | loadComponent: () =>
10 | import('./features/pages/product-details/product-details.component').then(
11 | (c) => c.ProductDetailsComponent
12 | ),
13 | title: 'Product Details',
14 | },
15 | {
16 | path: 'cart',
17 | loadComponent: () =>
18 | import('./features/pages/cart/cart.component').then(
19 | (c) => c.CartComponent
20 | ),
21 | title: 'Cart',
22 | },
23 | {
24 | path: '**',
25 | loadComponent: () =>
26 | import('./features/pages/not-found/not-found.component').then(
27 | (c) => c.NotFoundComponent
28 | ),
29 | title: 'Not Found',
30 | },
31 | ];
32 |
--------------------------------------------------------------------------------
/src/app/core/enums/cart.endpoints.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/src/app/core/enums/cart.endpoints.ts
--------------------------------------------------------------------------------
/src/app/core/enums/products.endpoints.ts:
--------------------------------------------------------------------------------
1 | export class ProductsEndpoints {
2 | static GET_ALL_PRODUCTS = 'products';
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/core/environment/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | baseUrl: 'https://fakestoreapi.com/',
3 | };
4 |
--------------------------------------------------------------------------------
/src/app/core/interceptors/error.interceptor.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { HttpInterceptorFn } from '@angular/common/http';
3 |
4 | import { errorInterceptor } from './error.interceptor';
5 |
6 | describe('errorInterceptor', () => {
7 | const interceptor: HttpInterceptorFn = (req, next) =>
8 | TestBed.runInInjectionContext(() => errorInterceptor(req, next));
9 |
10 | beforeEach(() => {
11 | TestBed.configureTestingModule({});
12 | });
13 |
14 | it('should be created', () => {
15 | expect(interceptor).toBeTruthy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/app/core/interceptors/error.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { HttpInterceptorFn } from '@angular/common/http';
2 | import { inject } from '@angular/core';
3 | import { ToastrService } from 'ngx-toastr';
4 | import { catchError, throwError } from 'rxjs';
5 |
6 | export const errorInterceptor: HttpInterceptorFn = (req, next) => {
7 | const toastrService = inject(ToastrService);
8 | return next(req).pipe(
9 | catchError((err) => {
10 | let errorMessage = 'An unknown error occurred!';
11 | if (err.status === 0) {
12 | errorMessage = 'Network error. Check your connection.';
13 | } else if (err.status === 404) {
14 | errorMessage = 'Resource not found.';
15 | } else if (err.status >= 500) {
16 | errorMessage = 'Server error. Try again later.';
17 | } else if (err.error?.message) {
18 | errorMessage = err.error.message;
19 | }
20 |
21 | toastrService.error(err.error.message, 'Error');
22 | return throwError(() => err);
23 | })
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/app/core/interceptors/loading.interceptor.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { HttpInterceptorFn } from '@angular/common/http';
3 |
4 | import { loadingInterceptor } from './loading.interceptor';
5 |
6 | describe('loadingInterceptor', () => {
7 | const interceptor: HttpInterceptorFn = (req, next) =>
8 | TestBed.runInInjectionContext(() => loadingInterceptor(req, next));
9 |
10 | beforeEach(() => {
11 | TestBed.configureTestingModule({});
12 | });
13 |
14 | it('should be created', () => {
15 | expect(interceptor).toBeTruthy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/app/core/interceptors/loading.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { HttpInterceptorFn } from '@angular/common/http';
2 | import { inject } from '@angular/core';
3 | import { NgxSpinnerService } from 'ngx-spinner';
4 | import { finalize } from 'rxjs';
5 |
6 | export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
7 | const spinner = inject(NgxSpinnerService);
8 | spinner.show();
9 | return next(req).pipe(
10 | finalize(() => {
11 | spinner.hide();
12 | })
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/app/core/interfaces/products.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Products {
2 | id: number;
3 | title: string;
4 | price: number;
5 | description: string;
6 | category: string;
7 | image: string;
8 | rating: Rating;
9 | }
10 |
11 | export interface Rating {
12 | rate: number;
13 | count: number;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/core/services/flowbite/flowbite.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { FlowbiteService } from './flowbite.service';
4 |
5 | describe('FlowbiteService', () => {
6 | let service: FlowbiteService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(FlowbiteService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/core/services/flowbite/flowbite.service.ts:
--------------------------------------------------------------------------------
1 | import { isPlatformBrowser } from '@angular/common';
2 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
3 |
4 | @Injectable({
5 | providedIn: 'root',
6 | })
7 | export class FlowbiteService {
8 | constructor(@Inject(PLATFORM_ID) private platformId: any) {}
9 |
10 | loadFlowbite(callback: (flowbite: any) => void) {
11 | if (isPlatformBrowser(this.platformId)) {
12 | import('flowbite').then((flowbite) => {
13 | callback(flowbite);
14 | });
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/core/services/products/products.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { ProductsService } from './products.service';
4 |
5 | describe('ProductsService', () => {
6 | let service: ProductsService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(ProductsService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/core/services/products/products.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { inject, Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { environment } from '../../environment/environment';
5 | import { ProductsEndpoints } from '../../enums/products.endpoints';
6 | import { Products } from '../../interfaces/products.interface';
7 |
8 | @Injectable({
9 | providedIn: 'root',
10 | })
11 | export class ProductsService {
12 | private readonly httpClient = inject(HttpClient);
13 |
14 | allProducts: Products[] = [] as Products[];
15 |
16 | getAllProducts(): Observable {
17 | return this.httpClient.get(
18 | environment.baseUrl + ProductsEndpoints.GET_ALL_PRODUCTS
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/features/layouts/footer/footer.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/features/layouts/footer/footer.component.scss:
--------------------------------------------------------------------------------
1 | .footer-bg {
2 | background-color: var(--surface-color);
3 |
4 | .text-color {
5 | color: var(--text-secondary);
6 | }
7 |
8 | .payments {
9 | @media all and (48rem <= width <= 55.875rem) {
10 | display: flex !important;
11 | flex-direction: column !important;
12 | justify-content: space-between !important;
13 | align-items: start !important;
14 | }
15 |
16 | @media all and (min-width: 55.875rem) {
17 | display: flex !important;
18 | flex-direction: row !important;
19 | justify-content: space-between !important;
20 | align-items: center !important;
21 | }
22 | }
23 |
24 | .btn-bg {
25 | background-color: var(--active-color);
26 | }
27 |
28 | .border-gray-200 {
29 | border-color: var(--border-color) !important;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/features/layouts/footer/footer.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FooterComponent } from './footer.component';
4 |
5 | describe('FooterComponent', () => {
6 | let component: FooterComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [FooterComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(FooterComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/features/layouts/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-footer',
5 | imports: [],
6 | templateUrl: './footer.component.html',
7 | styleUrl: './footer.component.scss'
8 | })
9 | export class FooterComponent {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/features/layouts/navbar/navbar.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/features/layouts/navbar/navbar.component.scss:
--------------------------------------------------------------------------------
1 | .nav-bg {
2 | background-color: var(--surface-color);
3 | }
4 |
5 | a {
6 | color: var(--text-color);
7 | }
8 |
9 | a.btn-text,
10 | a.router-link-active {
11 | color: var(--link-active-color);
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/features/layouts/navbar/navbar.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { NavbarComponent } from './navbar.component';
4 |
5 | describe('NavbarComponent', () => {
6 | let component: NavbarComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [NavbarComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(NavbarComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/features/layouts/navbar/navbar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { RouterLink, RouterLinkActive } from '@angular/router';
3 | import { ThemeToggleComponent } from '../../../shared/components/business/theme-toggle/theme-toggle.component';
4 |
5 | @Component({
6 | selector: 'app-navbar',
7 | imports: [RouterLink, RouterLinkActive, ThemeToggleComponent],
8 | templateUrl: './navbar.component.html',
9 | styleUrl: './navbar.component.scss',
10 | })
11 | export class NavbarComponent {}
12 |
--------------------------------------------------------------------------------
/src/app/features/pages/cart/cart.component.html:
--------------------------------------------------------------------------------
1 | cart works!
2 |
--------------------------------------------------------------------------------
/src/app/features/pages/cart/cart.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/src/app/features/pages/cart/cart.component.scss
--------------------------------------------------------------------------------
/src/app/features/pages/cart/cart.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CartComponent } from './cart.component';
4 |
5 | describe('CartComponent', () => {
6 | let component: CartComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [CartComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(CartComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/features/pages/cart/cart.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-cart',
5 | imports: [],
6 | templateUrl: './cart.component.html',
7 | styleUrl: './cart.component.scss'
8 | })
9 | export class CartComponent {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/features/pages/not-found/not-found.component.html:
--------------------------------------------------------------------------------
1 | not-found works!
2 |
--------------------------------------------------------------------------------
/src/app/features/pages/not-found/not-found.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/src/app/features/pages/not-found/not-found.component.scss
--------------------------------------------------------------------------------
/src/app/features/pages/not-found/not-found.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { NotFoundComponent } from './not-found.component';
4 |
5 | describe('NotFoundComponent', () => {
6 | let component: NotFoundComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [NotFoundComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(NotFoundComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/features/pages/not-found/not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-not-found',
5 | imports: [],
6 | templateUrl: './not-found.component.html',
7 | styleUrl: './not-found.component.scss'
8 | })
9 | export class NotFoundComponent {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/features/pages/product-details/product-details.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | @for (product of products; track product.id) {
4 | @if (product.id == id) {
5 |
7 |
9 |
![]()
11 |
12 |
13 |
14 |
{{product.title}}
15 |
{{product.category}}
16 |
{{product.description}}
17 |
18 |
19 | {{product.price | currency : 'EGP'}}
20 |
21 |
23 |
24 |
25 | {{product.rating.rate}}
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/src/app/features/pages/product-details/product-details.component.scss:
--------------------------------------------------------------------------------
1 | .Products {
2 | button {
3 | background-color: var(--active-color);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/features/pages/product-details/product-details.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ProductDetailsComponent } from './product-details.component';
4 |
5 | describe('ProductDetailsComponent', () => {
6 | let component: ProductDetailsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [ProductDetailsComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(ProductDetailsComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/features/pages/product-details/product-details.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject, OnInit, DestroyRef } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 | import { Subscription } from 'rxjs';
4 | import { ProductsService } from '../../../core/services/products/products.service';
5 | import { Products } from '../../../core/interfaces/products.interface';
6 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7 | import { CurrencyPipe } from '@angular/common';
8 |
9 | @Component({
10 | selector: 'app-product-details',
11 | imports: [CurrencyPipe],
12 | templateUrl: './product-details.component.html',
13 | styleUrl: './product-details.component.scss',
14 | })
15 | export class ProductDetailsComponent implements OnInit {
16 | private readonly activatedRoute = inject(ActivatedRoute);
17 | private readonly productsService = inject(ProductsService);
18 | private readonly destroyRef = inject(DestroyRef);
19 |
20 | productID!: string;
21 | id!: number;
22 | products!: Products[];
23 |
24 | ngOnInit(): void {
25 | this.activatedRoute.paramMap.subscribe({
26 | next: (param) => {
27 | this.productID = param.get('p_id')!;
28 | },
29 | });
30 |
31 | this.id = +this.productID;
32 |
33 | this.productsService
34 | .getAllProducts()
35 | .pipe(takeUntilDestroyed(this.destroyRef))
36 | .subscribe({
37 | next: (res) => {
38 | this.products = res;
39 | },
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/features/pages/products/products.component.html:
--------------------------------------------------------------------------------
1 | @if (productsService.allProducts!= null) {
2 |
3 |
4 |
8 |
9 |
12 |
13 |
15 | @for (product of productsService.allProducts|search: searchKey|sort: currentSort ;track product.id) {
16 |
19 |
20 |
![]()
22 |
23 |
24 |
{{product.title.split(' ').slice(0,3).join(' ')}}
25 |
{{product.description.split(' ').slice(0,10).join(' ')}}
26 |
27 |
28 |
{{product.price | currency : 'EGP'}}
29 |
30 | {{product.rating.rate}}
31 |
32 |
33 |
34 |
38 |
39 |
40 | }@empty {
41 |
42 | OOPs something went wrong
43 |
44 |
47 |
48 | }
49 |
50 |
51 |
52 | }
--------------------------------------------------------------------------------
/src/app/features/pages/products/products.component.scss:
--------------------------------------------------------------------------------
1 | .product {
2 | button {
3 | background-color: var(--active-color);
4 | transform: translateY(400%);
5 | transition: transform 0.5s;
6 | }
7 | &:hover {
8 | button {
9 | transform: translateY(0);
10 | }
11 | }
12 |
13 | .border-gray-200 {
14 | border-color: var(--border-color) !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/features/pages/products/products.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ProductsComponent } from './products.component';
4 |
5 | describe('ProductsComponent', () => {
6 | let component: ProductsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [ProductsComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(ProductsComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/features/pages/products/products.component.ts:
--------------------------------------------------------------------------------
1 | import { CurrencyPipe } from '@angular/common';
2 | import { Component, DestroyRef, inject, OnInit } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { Router, RouterLink } from '@angular/router';
5 |
6 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7 |
8 | import { ProductsService } from '../../../core/services/products/products.service';
9 | import { ProductsCarouselComponent } from '../../../shared/components/UI/products-carousel/products-carousel.component';
10 | import { SearchPipe } from '../../../shared/pipes/search.pipe';
11 | import { SearchByNameComponent } from '../../../shared/components/business/search-by-name/search-by-name.component';
12 | import { finalize } from 'rxjs';
13 | import { SortComponent } from '../../../shared/components/business/sort/sort.component';
14 | import { SortPipe } from '../../../shared/pipes/sort.pipe';
15 |
16 | @Component({
17 | selector: 'app-products',
18 | imports: [
19 | CurrencyPipe,
20 | SearchPipe,
21 | SortPipe,
22 | RouterLink,
23 | ProductsCarouselComponent,
24 | SearchByNameComponent,
25 | SortComponent,
26 | ],
27 | templateUrl: './products.component.html',
28 | styleUrl: './products.component.scss',
29 | })
30 | export class ProductsComponent implements OnInit {
31 | private readonly destroyRef = inject(DestroyRef);
32 | private readonly router = inject(Router);
33 | readonly productsService = inject(ProductsService);
34 |
35 | searchKey: string = '';
36 | currentSort: string = 'name-asc';
37 |
38 | ngOnInit(): void {
39 | this.getAllProducts();
40 | }
41 |
42 | getAllProducts() {
43 | this.productsService
44 | .getAllProducts()
45 | .pipe(takeUntilDestroyed(this.destroyRef))
46 | .subscribe({
47 | next: (res) => {
48 | this.productsService.allProducts = res;
49 | },
50 | });
51 | }
52 |
53 | sortProducts(sortType: string) {
54 | this.currentSort = sortType;
55 | }
56 |
57 | navigateToDetails(productId: number) {
58 | this.router.navigate(['/details', productId]);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/shared/components/UI/products-carousel/products-carousel.component.html:
--------------------------------------------------------------------------------
1 | @if (productsService.allProducts === []) {
2 |
3 |
4 | @for (product of productsService.allProducts; track product.id) {
5 |
6 |
7 | {{product.title}}
8 |
9 | }
10 |
11 |
12 | }
--------------------------------------------------------------------------------
/src/app/shared/components/UI/products-carousel/products-carousel.component.scss:
--------------------------------------------------------------------------------
1 | .border-gray-200 {
2 | border-color: var(--border-color) !important;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/shared/components/UI/products-carousel/products-carousel.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ProductsCarouselComponent } from './products-carousel.component';
4 |
5 | describe('ProductsCarouselComponent', () => {
6 | let component: ProductsCarouselComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [ProductsCarouselComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(ProductsCarouselComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/shared/components/UI/products-carousel/products-carousel.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject } from '@angular/core';
2 |
3 | import { CarouselModule, OwlOptions } from 'ngx-owl-carousel-o';
4 | import { ProductsService } from '../../../../core/services/products/products.service';
5 |
6 | @Component({
7 | selector: 'app-products-carousel',
8 | imports: [CarouselModule],
9 | templateUrl: './products-carousel.component.html',
10 | styleUrl: './products-carousel.component.scss',
11 | })
12 | export class ProductsCarouselComponent {
13 | readonly productsService = inject(ProductsService);
14 |
15 | customOptions: OwlOptions = {
16 | loop: true,
17 | mouseDrag: false,
18 | touchDrag: false,
19 | pullDrag: false,
20 | autoplay: true,
21 | autoplayTimeout: 2000,
22 | dots: true,
23 | navSpeed: 700,
24 | navText: ['', ''],
25 | responsive: {
26 | 0: {
27 | items: 1,
28 | },
29 | 400: {
30 | items: 2,
31 | },
32 | 740: {
33 | items: 3,
34 | },
35 | 940: {
36 | items: 4,
37 | },
38 | },
39 | nav: false,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/search-by-name/search-by-name.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/search-by-name/search-by-name.component.scss:
--------------------------------------------------------------------------------
1 | :focus {
2 | border-color: var(--active-color);
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/search-by-name/search-by-name.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SearchByNameComponent } from './search-by-name.component';
4 |
5 | describe('SearchByNameComponent', () => {
6 | let component: SearchByNameComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [SearchByNameComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(SearchByNameComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/search-by-name/search-by-name.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Output, EventEmitter } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 |
4 | @Component({
5 | selector: 'app-search-by-name',
6 | imports: [FormsModule],
7 | templateUrl: './search-by-name.component.html',
8 | styleUrl: './search-by-name.component.scss',
9 | })
10 | export class SearchByNameComponent {
11 | searchKey: string = '';
12 | @Output() key: EventEmitter = new EventEmitter();
13 |
14 | search(): void {
15 | this.key.emit(this.searchKey);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/sort/sort.component.html:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
52 |
53 |
54 | @if (isDropdownOpen) {
55 |
83 | }
--------------------------------------------------------------------------------
/src/app/shared/components/business/sort/sort.component.scss:
--------------------------------------------------------------------------------
1 | .sort-bg {
2 | background-color: var(--active-color);
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/sort/sort.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SortComponent } from './sort.component';
4 |
5 | describe('SortComponent', () => {
6 | let component: SortComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [SortComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(SortComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/sort/sort.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Output } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-sort',
5 | imports: [],
6 | templateUrl: './sort.component.html',
7 | styleUrl: './sort.component.scss',
8 | })
9 | export class SortComponent {
10 | @Output() sortChange = new EventEmitter();
11 | isDropdownOpen = false;
12 |
13 | toggleDropdown() {
14 | this.isDropdownOpen = !this.isDropdownOpen;
15 | }
16 |
17 | applySort(sortType: string) {
18 | this.sortChange.emit(sortType);
19 | this.isDropdownOpen = false; // Close dropdown after selection
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/theme-toggle/theme-toggle.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/theme-toggle/theme-toggle.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Reham-Gomaa/E-commerce/dd7bf9c87ad4960fe48982ceb29a2bf027c8305d/src/app/shared/components/business/theme-toggle/theme-toggle.component.scss
--------------------------------------------------------------------------------
/src/app/shared/components/business/theme-toggle/theme-toggle.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ThemeToggleComponent } from './theme-toggle.component';
4 |
5 | describe('ThemeToggleComponent', () => {
6 | let component: ThemeToggleComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [ThemeToggleComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(ThemeToggleComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/shared/components/business/theme-toggle/theme-toggle.component.ts:
--------------------------------------------------------------------------------
1 | import { isPlatformBrowser } from '@angular/common';
2 | import { Component, inject, OnInit, PLATFORM_ID } from '@angular/core';
3 |
4 | @Component({
5 | selector: 'app-theme-toggle',
6 | imports: [],
7 | templateUrl: './theme-toggle.component.html',
8 | styleUrl: './theme-toggle.component.scss',
9 | })
10 | export class ThemeToggleComponent implements OnInit {
11 | private readonly pLATFORM_ID = inject(PLATFORM_ID);
12 | isDark!: boolean;
13 |
14 | ngOnInit(): void {
15 | if (!isPlatformBrowser(this.pLATFORM_ID)) return;
16 |
17 | document.documentElement.classList.toggle(
18 | 'dark',
19 | localStorage.getItem('theme') === 'dark' ||
20 | (!('theme' in localStorage) &&
21 | window.matchMedia('(prefers-color-scheme: dark)').matches)
22 | );
23 | this.isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
24 | }
25 |
26 | toggleTheme() {
27 | if (!isPlatformBrowser(this.pLATFORM_ID)) return;
28 |
29 | if (localStorage.getItem('theme')) {
30 | if (localStorage.getItem('theme') === 'dark') {
31 | localStorage.setItem('theme', 'light');
32 | this.isDark = !this.isDark;
33 | } else {
34 | localStorage.setItem('theme', 'dark');
35 | this.isDark = !this.isDark;
36 | }
37 | } else {
38 | if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
39 | localStorage.setItem('theme', 'light');
40 | this.isDark = !this.isDark;
41 | } else {
42 | localStorage.setItem('theme', 'dark');
43 | this.isDark = !this.isDark;
44 | }
45 | }
46 | document.documentElement.classList.toggle(
47 | 'dark',
48 | localStorage.getItem('theme') === 'dark' ||
49 | (!('theme' in localStorage) &&
50 | window.matchMedia('(prefers-color-scheme: dark)').matches)
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/search.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { SearchPipe } from './search.pipe';
2 |
3 | describe('SearchPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new SearchPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/search.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'search',
5 | })
6 | export class SearchPipe implements PipeTransform {
7 | transform(arr: any[], key: string): any[] {
8 | return arr.filter((current) => {
9 | return current.title.toLowerCase().includes(key.toLowerCase());
10 | });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/sort.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { SortPipe } from './sort.pipe';
2 |
3 | describe('SortPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new SortPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/sort.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { Products } from '../../core/interfaces/products.interface';
3 |
4 | @Pipe({
5 | name: 'sort',
6 | pure: false,
7 | })
8 | export class SortPipe implements PipeTransform {
9 | transform(products: Products[], sortKey: string): Products[] {
10 | if (!products || !sortKey) return products;
11 |
12 | return [...products].sort((a, b) => {
13 | switch (sortKey) {
14 | case 'name-asc':
15 | return a.title.localeCompare(b.title);
16 | case 'name-desc':
17 | return b.title.localeCompare(a.title);
18 | case 'price-asc':
19 | return a.price - b.price;
20 | case 'price-desc':
21 | return b.price - a.price;
22 | default:
23 | return 0;
24 | }
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/shared/styles/_utilities.scss:
--------------------------------------------------------------------------------
1 | @use "./variables";
2 |
3 | .sr-only {
4 | position: absolute;
5 | width: 1px;
6 | height: 1px;
7 | padding: 0;
8 | overflow: hidden;
9 | clip: rect(0, 0, 0, 0);
10 | white-space: nowrap;
11 | border-width: 0;
12 | inset: 0;
13 | }
14 |
15 | body {
16 | font-family: var(--font-family-1);
17 | background-color: var(--bg-color);
18 | color: var(--text-color);
19 | }
20 | html,
21 | body,
22 | .nav-bg,
23 | .footer-bg {
24 | transition: background-color 0.3s ease, color 0.3s ease;
25 | }
26 | .mycontainer {
27 | @apply w-[90%] lg:w-[85%] mx-auto;
28 | }
29 | .active-text {
30 | color: var(--active-color);
31 | }
32 | .btn-text {
33 | color: var(--link-active-color);
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/shared/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
2 |
3 | :root {
4 | // Font
5 | --font-family-1: "Roboto", sans-serif;
6 |
7 | /* Light mode */
8 | --bg-color: #ffffff;
9 | --surface-color: #f8f8f8;
10 | --text-color: #131921;
11 | --text-secondary: #555555;
12 | --active-color: #f8ab38;
13 | --link-active-color: #ffce12;
14 | --border-color: #e5e7eb;
15 | }
16 |
17 | .dark {
18 | /* Dark mode */
19 | --bg-color: #0d1117;
20 | --surface-color: #161b22;
21 | --text-color: #f5f5f5;
22 | --text-secondary: #c9d1d9;
23 | --border-color: #30363d;
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Gallery
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main.server.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { AppComponent } from './app/app.component';
3 | import { config } from './app/app.config.server';
4 |
5 | const bootstrap = () => bootstrapApplication(AppComponent, config);
6 |
7 | export default bootstrap;
8 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { appConfig } from './app/app.config';
3 | import { AppComponent } from './app/app.component';
4 |
5 | bootstrapApplication(AppComponent, appConfig).catch((err) =>
6 | console.error(err)
7 | );
8 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AngularNodeAppEngine,
3 | createNodeRequestHandler,
4 | isMainModule,
5 | writeResponseToNodeResponse,
6 | } from '@angular/ssr/node';
7 | import express from 'express';
8 | import { dirname, resolve } from 'node:path';
9 | import { fileURLToPath } from 'node:url';
10 |
11 | const serverDistFolder = dirname(fileURLToPath(import.meta.url));
12 | const browserDistFolder = resolve(serverDistFolder, '../browser');
13 |
14 | const app = express();
15 | const angularApp = new AngularNodeAppEngine();
16 |
17 | /**
18 | * Example Express Rest API endpoints can be defined here.
19 | * Uncomment and define endpoints as necessary.
20 | *
21 | * Example:
22 | * ```ts
23 | * app.get('/api/**', (req, res) => {
24 | * // Handle API request
25 | * });
26 | * ```
27 | */
28 |
29 | /**
30 | * Serve static files from /browser
31 | */
32 | app.use(
33 | express.static(browserDistFolder, {
34 | maxAge: '1y',
35 | index: false,
36 | redirect: false,
37 | }),
38 | );
39 |
40 | /**
41 | * Handle all other requests by rendering the Angular application.
42 | */
43 | app.use('/**', (req, res, next) => {
44 | angularApp
45 | .handle(req)
46 | .then((response) =>
47 | response ? writeResponseToNodeResponse(response, res) : next(),
48 | )
49 | .catch(next);
50 | });
51 |
52 | /**
53 | * Start the server if this module is the main entry point.
54 | * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
55 | */
56 | if (isMainModule(import.meta.url)) {
57 | const port = process.env['PORT'] || 4000;
58 | app.listen(port, () => {
59 | console.log(`Node Express server listening on http://localhost:${port}`);
60 | });
61 | }
62 |
63 | /**
64 | * The request handler used by the Angular CLI (dev-server and during build).
65 | */
66 | export const reqHandler = createNodeRequestHandler(app);
67 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @use "./app/shared/styles/variables" as vars;
2 | @use "./app/shared/styles/utilities" as utils;
3 |
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: "selector",
4 | content: ["./src/**/*.{html,ts}", "./node_modules/flowbite/**/*.js"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [require("flowbite/plugin")],
9 | };
10 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/app",
7 | "types": [
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "src/main.ts",
13 | "src/main.server.ts",
14 | "src/server.ts"
15 | ],
16 | "include": [
17 | "src/**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "compileOnSave": false,
5 | "compilerOptions": {
6 | "outDir": "./dist/out-tsc",
7 | "strict": true,
8 | "noImplicitOverride": true,
9 | "noPropertyAccessFromIndexSignature": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "skipLibCheck": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "experimentalDecorators": true,
16 | "moduleResolution": "bundler",
17 | "importHelpers": true,
18 | "target": "ES2022",
19 | "module": "ES2022"
20 | },
21 | "angularCompilerOptions": {
22 | "enableI18nLegacyMessageIdFormat": false,
23 | "strictInjectionParameters": true,
24 | "strictInputAccessModifiers": true,
25 | "strictTemplates": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/spec",
7 | "types": [
8 | "jasmine"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.spec.ts",
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------