diff --git a/angular.json b/angular.json index 8999310..c94fd04 100644 --- a/angular.json +++ b/angular.json @@ -47,7 +47,13 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] } }, "defaultConfiguration": "production" diff --git a/package-lock.json b/package-lock.json index 723f5fa..7db7a8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "bootstrap": "^5.3.8", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -3240,6 +3241,17 @@ "license": "MIT", "optional": true }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", @@ -4630,6 +4642,25 @@ "dev": true, "license": "ISC" }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", diff --git a/package.json b/package.json index 1bf77d0..5dc1b2f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "bootstrap": "^5.3.8", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -43,4 +44,4 @@ "typescript": "~5.9.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/src/app/app.html b/src/app/app.html index e0118a1..67e7bd4 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,342 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..18fc308 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,25 @@ import { Routes } from '@angular/router'; +import { Login } from '@pages/auth/login/login'; +import { Signup } from '@pages/auth/signup/signup'; +import { AppLayout } from '@layouts/app-layout/app-layout'; +import { authGuard } from '@guards/auth-guard'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + component: AppLayout, + canActivate: [authGuard] + }, +{ + path: 'login', + component: Login, + }, + { + path: 'sign-up', + component: Signup, + }, + { + path: '**', + redirectTo: '/', + }, +]; diff --git a/src/app/guards/auth-guard.spec.ts b/src/app/guards/auth-guard.spec.ts new file mode 100644 index 0000000..0ffeaf0 --- /dev/null +++ b/src/app/guards/auth-guard.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { CanActivateFn } from '@angular/router'; + +import { authGuard } from './auth-guard'; + +describe('authGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => authGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/src/app/guards/auth-guard.ts b/src/app/guards/auth-guard.ts new file mode 100644 index 0000000..a798f2c --- /dev/null +++ b/src/app/guards/auth-guard.ts @@ -0,0 +1,14 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { Authorization } from '@services/authorization/authorization'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(Authorization); + const router = inject(Router); + if (authService.isLoggedIn()) { + return true; // Allow access + } else { + router.navigate(['/login']); // Redirect to login + return false; // Deny access + } +}; diff --git a/src/app/layouts/app-layout/app-layout.css b/src/app/layouts/app-layout/app-layout.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/layouts/app-layout/app-layout.html b/src/app/layouts/app-layout/app-layout.html new file mode 100644 index 0000000..f08ea58 --- /dev/null +++ b/src/app/layouts/app-layout/app-layout.html @@ -0,0 +1 @@ +

app-layout works!

diff --git a/src/app/layouts/app-layout/app-layout.spec.ts b/src/app/layouts/app-layout/app-layout.spec.ts new file mode 100644 index 0000000..93ba684 --- /dev/null +++ b/src/app/layouts/app-layout/app-layout.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppLayout } from './app-layout'; + +describe('AppLayout', () => { + let component: AppLayout; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppLayout] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AppLayout); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/layouts/app-layout/app-layout.ts b/src/app/layouts/app-layout/app-layout.ts new file mode 100644 index 0000000..50d5c15 --- /dev/null +++ b/src/app/layouts/app-layout/app-layout.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-app-layout', + imports: [], + templateUrl: './app-layout.html', + styleUrl: './app-layout.css', +}) +export class AppLayout { + +} diff --git a/src/app/models/user.ts b/src/app/models/user.ts new file mode 100644 index 0000000..636915a --- /dev/null +++ b/src/app/models/user.ts @@ -0,0 +1,4 @@ +export type User = { + id: number; + email: string; +}; \ No newline at end of file diff --git a/src/app/pages/auth/login/login.css b/src/app/pages/auth/login/login.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/auth/login/login.html b/src/app/pages/auth/login/login.html new file mode 100644 index 0000000..38aab61 --- /dev/null +++ b/src/app/pages/auth/login/login.html @@ -0,0 +1,41 @@ +
+ @if(error()) { + + } +
+
+

Sign In

+
+
+ + + @if(isInvalid(email)) { +

Invalid Email

+ } +
+
+ + + @if(isInvalid(password)) { +

Password Required

+ } +
+ +

+ Don't have an account yet? + Sign Up +

+
+
+
+
\ No newline at end of file diff --git a/src/app/pages/auth/login/login.spec.ts b/src/app/pages/auth/login/login.spec.ts new file mode 100644 index 0000000..8d4c63e --- /dev/null +++ b/src/app/pages/auth/login/login.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Login } from './login'; + +describe('Login', () => { + let component: Login; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Login] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Login); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/auth/login/login.ts b/src/app/pages/auth/login/login.ts new file mode 100644 index 0000000..d82f5d6 --- /dev/null +++ b/src/app/pages/auth/login/login.ts @@ -0,0 +1,27 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormsModule, NgForm } from '@angular/forms'; +import { Router, RouterLink } from "@angular/router"; +import { FormUtility } from '@utilities/form-utility'; +import { Authorization } from '@services/authorization/authorization'; + +@Component({ + selector: 'app-login', + imports: [FormsModule, RouterLink], + templateUrl: './login.html', + styleUrl: './login.css', +}) +export class Login extends FormUtility { + private authService = inject(Authorization); + private router = inject(Router); + + error = signal(null); + + + onSubmit(form: NgForm) { + console.log(form.value); + this.authService.login(form.value.email, form.value.password).subscribe({ + error: (err) => this.error.set(err.message), + complete: () => this.router.navigate(['/']), + });; + } +} diff --git a/src/app/pages/auth/signup/signup.css b/src/app/pages/auth/signup/signup.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/auth/signup/signup.html b/src/app/pages/auth/signup/signup.html new file mode 100644 index 0000000..c9ce9ad --- /dev/null +++ b/src/app/pages/auth/signup/signup.html @@ -0,0 +1 @@ +

signup works!

diff --git a/src/app/pages/auth/signup/signup.spec.ts b/src/app/pages/auth/signup/signup.spec.ts new file mode 100644 index 0000000..1e81683 --- /dev/null +++ b/src/app/pages/auth/signup/signup.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Signup } from './signup'; + +describe('Signup', () => { + let component: Signup; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Signup] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Signup); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/auth/signup/signup.ts b/src/app/pages/auth/signup/signup.ts new file mode 100644 index 0000000..828eb7e --- /dev/null +++ b/src/app/pages/auth/signup/signup.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-signup', + imports: [], + templateUrl: './signup.html', + styleUrl: './signup.css', +}) +export class Signup { + +} diff --git a/src/app/services/authorization/authorization-call.base.ts b/src/app/services/authorization/authorization-call.base.ts new file mode 100644 index 0000000..da326ce --- /dev/null +++ b/src/app/services/authorization/authorization-call.base.ts @@ -0,0 +1,10 @@ +import { User } from "@models/user"; +import { Observable } from "rxjs"; + +export abstract class AuthorizationCallServiceBase { + + abstract login(username: string, password: string): Observable; + + abstract register(username: string, password: string, email: string): Observable; + +} \ No newline at end of file diff --git a/src/app/services/authorization/authorization-call.fake.ts b/src/app/services/authorization/authorization-call.fake.ts new file mode 100644 index 0000000..cfae681 --- /dev/null +++ b/src/app/services/authorization/authorization-call.fake.ts @@ -0,0 +1,42 @@ +import { Observable } from "rxjs"; +import { AuthorizationCallServiceBase } from "./authorization-call.base"; +import { User } from "@models/user"; + +export class AuthorizationCallFakeService extends AuthorizationCallServiceBase { + + + private users: User[] = [ + { id: 1, email: 'testuser@example.com' }, + ]; + + login(email: string, password: string): Observable { + return new Observable((observer) => { + const user = this.users.find(u => u.email === email); + if (!user || password !== 'password') { + observer.error(new Error('Invalid username or password')); + return; + } + observer.next(user); + observer.complete(); + }); + } + + register(username: string, password: string, email: string): Observable { + return new Observable((observer) => { + // Simulate a successful registration response + if (this.users.find(user => user.email === email)) { + observer.error(new Error('Email already exists')); + return; + } + this.users.push({ id: this.users.length + 1, email }); + observer.next(); + observer.complete(); + }); + } + + + + + + + } \ No newline at end of file diff --git a/src/app/services/authorization/authorization-call.ts b/src/app/services/authorization/authorization-call.ts new file mode 100644 index 0000000..8c805cb --- /dev/null +++ b/src/app/services/authorization/authorization-call.ts @@ -0,0 +1,20 @@ +import { HttpClient } from "@angular/common/http"; +import { AuthorizationCallServiceBase } from "./authorization-call.base"; +import { inject } from "@angular/core"; +import { User } from "@models/user"; +import { Observable } from "rxjs"; + +export class AuthorizationCallService extends AuthorizationCallServiceBase { + + private readonly httpClient = inject(HttpClient); + + override login(username: string, password: string): Observable { + throw new Error("Method not implemented."); + } + override register(username: string, password: string, email: string): Observable { + throw new Error("Method not implemented."); + } + + + +} \ No newline at end of file diff --git a/src/app/services/authorization/authorization.spec.ts b/src/app/services/authorization/authorization.spec.ts new file mode 100644 index 0000000..c93d062 --- /dev/null +++ b/src/app/services/authorization/authorization.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { Authorization } from './authorization'; + +describe('Authorization', () => { + let service: Authorization; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(Authorization); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/authorization/authorization.ts b/src/app/services/authorization/authorization.ts new file mode 100644 index 0000000..9d1fd47 --- /dev/null +++ b/src/app/services/authorization/authorization.ts @@ -0,0 +1,41 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { User } from '@models/user'; +import { AuthorizationCallServiceBase } from './authorization-call.base'; +import { environment } from '@environment'; +import { take, tap } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class Authorization { + + private readonly apiUrl = environment.apiUrl + '/auth'; + + + authCall: AuthorizationCallServiceBase = new environment.authorizationCallService(); + + user = signal(null); + + login(username: string, password: string) { + return this.authCall.login(username, password).pipe( + take(1), + tap(res => { + this.user.set(res); + }) + ); + } + + + logout() { + this.user.set(null); + } + + isLoggedIn(): boolean { + return this.user() !== null; + } + + getUsername(): string | null { + return this.user()?.email || null; + } + +} diff --git a/src/app/utilities/form-utility.ts b/src/app/utilities/form-utility.ts new file mode 100644 index 0000000..7e495f5 --- /dev/null +++ b/src/app/utilities/form-utility.ts @@ -0,0 +1,7 @@ +import { NgModel } from "@angular/forms"; + +export class FormUtility { + isInvalid(formControl: NgModel) { + return formControl.touched && formControl.dirty && formControl.invalid; + } +} diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts new file mode 100644 index 0000000..5c6a7a9 --- /dev/null +++ b/src/environments/environment.development.ts @@ -0,0 +1,7 @@ +import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake"; + +export const environment = { + production: false, + apiUrl: 'http://localhost:8080', + authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..f31dbab --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,7 @@ +import { AuthorizationCallService } from "@services/authorization/authorization-call"; + +export const environment = { + apiUrl: '/api', + production: true, + authorizationCallService: AuthorizationCallService, +}; diff --git a/src/styles.css b/src/styles.css index 35eb9c2..8c4819d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,3 +1,89 @@ /* You can add global styles to this file, and also import other style files */ -@import "tailwindcss"; +@import 'tailwindcss'; + +@theme { + /* Dark mode */ + --dark-mode-selector: .dark; + + /* Estensioni */ + --color-primary-50: #eff6ff; + --color-primary-100: #dbeafe; + --color-primary-200: #bfdbfe; + --color-primary-300: #93c5fd; + --color-primary-400: #60a5fa; + --color-primary-500: #3b82f6; + --color-primary-600: #2563eb; + --color-primary-700: #1d4ed8; + --color-primary-800: #1e40af; + --color-primary-900: #1e3a8a; + --color-primary-950: #172554; + + /* Font families */ + --font-body: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', + Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + + --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', + Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; +} + +body { + @apply bg-gray-50 dark:bg-gray-900; +} + +.container-centered { + @apply flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0; +} + +.input { + @apply bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500; +} + +.card-container { + @apply w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700; +} +.card { + @apply p-6 space-y-4 md:space-y-6 sm:p-8; +} +.label { + @apply block mb-2 text-sm font-medium text-gray-900 dark:text-white; +} + +.title { + @apply text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white; +} + +.button { + @apply text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800; +} + +.button:disabled { + @apply bg-primary-500 opacity-40; +} +.link { + @apply font-medium text-primary-600 hover:underline dark:text-primary-500; +} +.link-active { + @apply text-purple-600 dark:text-white; +} + +.form-control { + @apply flex flex-col gap-0.5; +} + +.form-group { + @apply flex flex-col gap-4; +} + +.helper-text { + @apply text-sm font-light text-gray-500 dark:text-gray-400; +} + +.nav-header { + @apply bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800; +} +.inner-nav { + @apply flex flex-wrap justify-center gap-4 items-center max-w-7xl; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2ab7442..bd9e69c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,16 @@ { "compileOnSave": false, "compilerOptions": { + "baseUrl": "./src", + "paths": { + "@pages/*" : ["app/pages/*"], + "@layouts/*" : ["app/layouts/*"], + "@services/*" : ["app/services/*"], + "@environment" : ["environments/environment"], + "@models/*" : ["app/models/*"], + "@utilities/*" : ["app/utilities/*"], + "@guards/*" : ["app/guards/*"] + }, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true,