Przeglądaj źródła

ajout des agent card, chat input, chat view dashboard login et sidebar

master
trauchessec 2 tygodni temu
rodzic
commit
02ed9270f5
24 zmienionych plików z 2470 dodań i 0 usunięć
  1. +9
    -0
      src/app/components/agent-card/agent-card.html
  2. +105
    -0
      src/app/components/agent-card/agent-card.scss
  3. +23
    -0
      src/app/components/agent-card/agent-card.spec.ts
  4. +38
    -0
      src/app/components/agent-card/agent-card.ts
  5. +37
    -0
      src/app/components/chat-input/chat-input.html
  6. +198
    -0
      src/app/components/chat-input/chat-input.scss
  7. +23
    -0
      src/app/components/chat-input/chat-input.spec.ts
  8. +56
    -0
      src/app/components/chat-input/chat-input.ts
  9. +108
    -0
      src/app/components/sidebar/sidebar.html
  10. +332
    -0
      src/app/components/sidebar/sidebar.scss
  11. +23
    -0
      src/app/components/sidebar/sidebar.spec.ts
  12. +72
    -0
      src/app/components/sidebar/sidebar.ts
  13. +70
    -0
      src/app/pages/chat/chat-view.html
  14. +251
    -0
      src/app/pages/chat/chat-view.scss
  15. +23
    -0
      src/app/pages/chat/chat-view.spec.ts
  16. +79
    -0
      src/app/pages/chat/chat-view.ts
  17. +71
    -0
      src/app/pages/dashboard/dashboard.html
  18. +286
    -0
      src/app/pages/dashboard/dashboard.scss
  19. +23
    -0
      src/app/pages/dashboard/dashboard.spec.ts
  20. +188
    -0
      src/app/pages/dashboard/dashboard.ts
  21. +97
    -0
      src/app/pages/login/login.html
  22. +266
    -0
      src/app/pages/login/login.scss
  23. +23
    -0
      src/app/pages/login/login.spec.ts
  24. +69
    -0
      src/app/pages/login/login.ts

+ 9
- 0
src/app/components/agent-card/agent-card.html Wyświetl plik

@@ -0,0 +1,9 @@
<div class="agent-card" (click)="onSelect()">
<div class="agent-icon">
<fa-icon [icon]="getIcon()"></fa-icon>
</div>
<div class="agent-content">
<h3 class="agent-name">{{ agent.name }}</h3>
<p class="agent-description">{{ agent.description }}</p>
</div>
</div>

+ 105
- 0
src/app/components/agent-card/agent-card.scss Wyświetl plik

@@ -0,0 +1,105 @@
.agent-card {
display: flex;
flex-direction: row;
gap: 16px;
padding: 20px 20px 50px 20px;

background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
min-height: 100px;
align-items: flex-start;

&:hover {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.1);
transform: translateY(-2px);

.agent-icon {
background-color: #ecf9ff;
transform: scale(1.05);

fa-icon {
color: #3498db;
}
}
}

&:active {
transform: translateY(0);
}

.agent-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 10px;
background-color: #ecf9ff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;

fa-icon {
font-size: 24px;
color: #8abecf;
transition: color 0.2s ease;
}
}

.agent-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;

.agent-name {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
line-height: 1.4;
}

.agent-description {
margin: 0;
font-size: 14px;
font-weight: 400;
color: #6b7280;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
}

// Responsive
@media (max-width: 768px) {
.agent-card {
padding: 16px;

.agent-icon {
width: 40px;
height: 40px;

fa-icon {
font-size: 20px;
}
}

.agent-content {
.agent-name {
font-size: 15px;
}

.agent-description {
font-size: 13px;
}
}
}
}

+ 23
- 0
src/app/components/agent-card/agent-card.spec.ts Wyświetl plik

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { AgentCard } from './agent-card';

describe('AgentCard', () => {
let component: AgentCard;
let fixture: ComponentFixture<AgentCard>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AgentCard]
})
.compileComponents();

fixture = TestBed.createComponent(AgentCard);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 38
- 0
src/app/components/agent-card/agent-card.ts Wyświetl plik

@@ -0,0 +1,38 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {
faEdit,
faAlignLeft,
faFileAlt,
faEnvelope,
IconDefinition
} from '@fortawesome/free-solid-svg-icons';
import { Agent } from '../../models/data.model';

@Component({
selector: 'app-agent-card',
standalone: true,
imports: [CommonModule, FontAwesomeModule],
templateUrl: './agent-card.html',
styleUrls: ['./agent-card.scss']
})
export class AgentCard {
@Input() agent!: Agent;
@Output() selected = new EventEmitter<Agent>();

private iconMap: { [key: string]: IconDefinition } = {
'edit': faEdit,
'align-left': faAlignLeft,
'file-alt': faFileAlt,
'envelope': faEnvelope
};

getIcon(): IconDefinition {
return this.iconMap[this.agent.icon] || faEdit;
}

onSelect(): void {
this.selected.emit(this.agent);
}
}

+ 37
- 0
src/app/components/chat-input/chat-input.html Wyświetl plik

@@ -0,0 +1,37 @@
<div class="chat-input-container">
<div class="input-wrapper">
<textarea
#messageInput
[(ngModel)]="message"
placeholder="Nouvelle conversation..."
rows="1"
[disabled]="disabled"
(input)="onInputChange()"
></textarea>

<div class="actions-row">
<button class="attach-button" (click)="onAttach()" [attr.aria-label]="'Importer un fichier'">
<fa-icon [icon]="faPaperclip"></fa-icon>
<span>Importer un fichier</span>
</button>

<div class="right-actions">
<button class="voice-button" (click)="onVoice()" [attr.aria-label]="'Message vocal'">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-soundwave" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8.5 2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-1 0v-11a.5.5 0 0 1 .5-.5m-2 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5m4 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5m-6 1.5A.5.5 0 0 1 5 6v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m8 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m-10 1A.5.5 0 0 1 3 7v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5m12 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5"/>
</svg>
</button>

<button
class="send-button"
(click)="onSend()"
[disabled]="!message.trim() || disabled"
[attr.aria-label]="'Envoyer le message'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5"/>
</svg> </button>
</div>
</div>
</div>
</div>

+ 198
- 0
src/app/components/chat-input/chat-input.scss Wyświetl plik

@@ -0,0 +1,198 @@
.chat-input-container {
width: 100%;
padding: 16px 24px;
background: transparent;
display: flex;
justify-content: center;

.input-wrapper {
width: 100%;
max-width: 1000px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;

&:focus-within {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.12);
}

textarea {
width: 100%;
border: none;
outline: none;
resize: none;
font-size: 15px;
font-family: inherit;
color: #1f2937;
background: transparent;
min-height: 24px;
max-height: 200px;
line-height: 1.5;
padding: 0;

&::placeholder {
color: #9ca3af;
}

&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}

.actions-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 4px;

.attach-button {
display: flex;
align-items: center;
gap: 8px;
padding: 0;
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: color 0.2s ease;

&:hover {
color: #3498db;
}

fa-icon {
font-size: 16px;
}

span {
@media (max-width: 480px) {
display: none;
}
}
}

.right-actions {
display: flex;
align-items: center;
gap: 8px;

.voice-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease;

&:hover {
background: #f3f4f6;
color: #1f2937;
}

fa-icon {
font-size: 18px;
}
}

.send-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: #f63e63;
border: none;
color: #ffffff;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease;

&:hover:not(:disabled) {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}

&:active:not(:disabled) {
transform: scale(0.98);
}

&:disabled {
background: #e5e7eb;
cursor: not-allowed;
opacity: 0.6;

fa-icon {
color: #9ca3af;
}
}

fa-icon {
font-size: 16px;
}
}
}
}
}
}

// Responsive
@media (max-width: 768px) {
.chat-input-container {
padding: 12px 16px;

.input-wrapper {
padding: 12px 16px;
border-radius: 14px;

textarea {
font-size: 14px;
}

.actions-row {
.attach-button {
font-size: 12px;

fa-icon {
font-size: 14px;
}
}

.right-actions {
gap: 6px;

.voice-button,
.send-button {
width: 32px;
height: 32px;

fa-icon {
font-size: 15px;
}
}
}
}
}
}
}

// Auto-expand textarea
@media (min-width: 769px) {
.chat-input-container .input-wrapper textarea {
overflow-y: auto;
}
}

+ 23
- 0
src/app/components/chat-input/chat-input.spec.ts Wyświetl plik

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ChatInput } from './chat-input';

describe('ChatInput', () => {
let component: ChatInput;
let fixture: ComponentFixture<ChatInput>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChatInput]
})
.compileComponents();

fixture = TestBed.createComponent(ChatInput);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 56
- 0
src/app/components/chat-input/chat-input.ts Wyświetl plik

@@ -0,0 +1,56 @@
import { Component, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {

faPaperclip,

} from '@fortawesome/free-solid-svg-icons';

@Component({
selector: 'app-chat-input',
standalone: true,
imports: [CommonModule, FormsModule, FontAwesomeModule],
templateUrl: './chat-input.html',
styleUrls: ['./chat-input.scss']
})
export class ChatInput {
@Input() disabled = false;
@Output() messageSent = new EventEmitter<string>();
@Output() fileAttached = new EventEmitter<void>();
@Output() voiceActivated = new EventEmitter<void>();
@ViewChild('messageInput') messageInput!: ElementRef<HTMLTextAreaElement>;

message = '';
faPaperclip = faPaperclip;

onSend(): void {
if (this.message.trim() && !this.disabled) {
this.messageSent.emit(this.message.trim());
this.message = '';
this.adjustTextareaHeight();
}
}
onInputChange(): void {
const textarea = this.messageInput.nativeElement;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}

onAttach(): void {
this.fileAttached.emit();
}

onVoice(): void {
this.voiceActivated.emit();
}

private adjustTextareaHeight(): void {
if (this.messageInput) {
const textarea = this.messageInput.nativeElement;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
}
}
}

+ 108
- 0
src/app/components/sidebar/sidebar.html Wyświetl plik

@@ -0,0 +1,108 @@
<div class="sidebar" [class.collapsed]="isCollapsed">
<div class="sidebar-header">
<div class="logo" *ngIf="!isCollapsed">
<h1>AIT</h1>
</div>
<button class="toggle-button" (click)="onToggleSidebar()"
[attr.aria-label]="isCollapsed ? 'Ouvrir la barre latérale' : 'Réduire la barre latérale'">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-layout-sidebar"
viewBox="0 0 16 16">
<path
d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5-1v12h9a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1zM4 2H2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2z"/>
</svg>
</button>
</div>

<!-- Bouton nouvelle discussion -->
<button class="new-chat-button" (click)="onNewChat()" [class.collapsed]="isCollapsed"
[attr.aria-label]="'Nouvelle discussion'">
<fa-icon [icon]="faComments"></fa-icon>
<span *ngIf="!isCollapsed">Nouvelle discussion</span>
</button>

<!-- Section Discussions passées -->
<div class="sidebar-section">
<h3 class="section-title" *ngIf="!isCollapsed">Discussions passées</h3>

<div class="conversations-section" *ngIf="!isCollapsed">
<div class="conversations-list">
<div
*ngFor="let conversation of conversations"
class="conversation-item"
[class.active]="currentConversationId === conversation.id"
(click)="onSelectConversation(conversation.id)"
>
<span class="conversation-title">{{ conversation.title }}</span>
</div>
</div>
</div>

<!-- Icône en mode réduit
<button class="sidebar-icon-button" *ngIf="isCollapsed" [attr.aria-label]="'Discussions passées'">
<fa-icon [icon]="faComments"></fa-icon>
</button>-->
</div>

<!-- Section Mes projets -->
<div class="sidebar-section">
<h3 class="section-title" *ngIf="!isCollapsed">Mes projets</h3>

<div class="projects-section" *ngIf="!isCollapsed">
<div class="projects-list">
<div *ngFor="let project of projects" class="project-wrapper">
<div class="project-item" (click)="onToggleProject(project.id)">
<fa-icon [icon]="faFolder" class="project-icon"></fa-icon>
<span class="project-name">{{ project.name }}</span>
<fa-icon
[icon]="project.expanded ? faChevronDown : faChevronRight"
class="expand-icon"
></fa-icon>
</div>

<div class="project-conversations"
*ngIf="project.expanded && project.conversations && project.conversations.length > 0">
<div
*ngFor="let conversation of project.conversations"
class="conversation-item sub-item"
[class.active]="currentConversationId === conversation.id"
(click)="onSelectConversation(conversation.id); $event.stopPropagation()"
>
<span class="conversation-title">{{ conversation.title }}</span>
</div>
</div>

<div class="project-empty"
*ngIf="project.expanded && (!project.conversations || project.conversations.length === 0)">
<span>Aucune conversation</span>
</div>
</div>
</div>
</div>

<!-- Icône en mode réduit
<button class="sidebar-icon-button" *ngIf="isCollapsed" [attr.aria-label]="'Mes projets'">
<fa-icon [icon]="faFolder"></fa-icon>
</button>-->
</div>
@if (isCollapsed) {
<div class="sidebar-footer sidebar-footer-colapsed">
<button class="footer-button" [attr.aria-label]="'Aide'">
<fa-icon [icon]="faQuestionCircle"></fa-icon>
</button>
<button class="footer-button" [attr.aria-label]="'Paramètres'">
<fa-icon [icon]="faCog"></fa-icon>
</button>
</div>

} @else {

<div class="sidebar-footer">
<button class="footer-button" [attr.aria-label]="'Aide'">
<fa-icon [icon]="faQuestionCircle"></fa-icon>
</button>
<button class="footer-button" [attr.aria-label]="'Paramètres'">
<fa-icon [icon]="faCog"></fa-icon>
</button>
</div>
}
</div>

+ 332
- 0
src/app/components/sidebar/sidebar.scss Wyświetl plik

@@ -0,0 +1,332 @@
.sidebar {
width: 280px;
height: 100vh;
background: #243b5d;
color: #ffffff;
display: flex;
flex-direction: column;
transition: width 0.3s ease;
position: relative;
overflow-x: hidden;

&.collapsed {
width: 64px;

.sidebar-section {
margin-bottom: 8px;
}
}

// Header
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
min-height: 64px;

.logo {
flex: 1;

h1 {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #3498db;
}
}

.toggle-button {
background: transparent;
border: none;
color: #ffffff;
cursor: pointer;
padding: 8px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;

&:hover {
background-color: rgba(255, 255, 255, 0.1);
}

fa-icon {
font-size: 20px;
}
}
}

// Bouton nouvelle discussion
.new-chat-button {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin: 16px;
background: #2984a1;
border: none;
border-radius: 8px;
color: #ffffff;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;

&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.4);
}

&.collapsed {
justify-content: center;
padding: 12px;
margin: 16px 8px;

fa-icon {
margin: 0;
}
}

fa-icon {
font-size: 18px;
}
}

// Sections
.sidebar-section {
margin-bottom: 24px;
max-height: 35vh;
}

.section-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
padding: 0 16px;
margin: 0 0 12px 0;
letter-spacing: 0.5px;
}

// Icônes en mode réduit
.sidebar-icon-button {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 14px 0;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.2s ease;
position: relative;

&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 0;
background: #3498db;
transition: height 0.2s ease;
}

&:hover {
background-color: rgba(255, 255, 255, 0.05);
color: #ffffff;

&::before {
height: 60%;
}
}

fa-icon {
font-size: 20px;
}
}

// Conversations
.conversations-section {
max-height: 300px;
overflow-y: auto;
padding: 0 8px;

&::-webkit-scrollbar {
width: 4px;
}

&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}

&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;

&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}

.conversations-list {
display: flex;
flex-direction: column;
gap: 4px;
}

.conversation-item {
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;

&:hover {
background-color: rgba(255, 255, 255, 0.1);
}

&.active {
background-color: rgba(52, 152, 219, 0.3);
border-left: 3px solid #3498db;
}

&.sub-item {
margin-left: 20px;
padding: 8px 12px;
font-size: 13px;
}

.conversation-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
}

// Projets
.projects-section {
max-height: 400px;
overflow-y: auto;
padding: 0 8px;

&::-webkit-scrollbar {
width: 4px;
}

&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}

&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;

&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}

.projects-list {
display: flex;
flex-direction: column;
gap: 4px;
}

.project-wrapper {
margin-bottom: 4px;
}

.project-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
background-color: rgba(255, 255, 255, 0.1);
}

.project-icon {
font-size: 16px;
}

.project-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 500;
}

.expand-icon {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
}
}

.project-conversations {
margin-top: 4px;
display: flex;
flex-direction: column;
gap: 2px;
}

.project-empty {
padding: 8px 12px 8px 42px;
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
font-style: italic;
}

// Footer
.sidebar-footer {
margin-top: auto;
padding: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: center;
gap: 12px;

.footer-button {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
padding: 10px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;

&:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
}

fa-icon {
font-size: 18px;
}
}
}
.sidebar-footer-colapsed{
display: flex;
flex-direction: column;
}
}

+ 23
- 0
src/app/components/sidebar/sidebar.spec.ts Wyświetl plik

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { Sidebar } from './sidebar';

describe('Sidebar', () => {
let component: Sidebar;
let fixture: ComponentFixture<Sidebar>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Sidebar]
})
.compileComponents();

fixture = TestBed.createComponent(Sidebar);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 72
- 0
src/app/components/sidebar/sidebar.ts Wyświetl plik

@@ -0,0 +1,72 @@
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {
faBars,
faComments,
faFolder,
faChevronDown,
faChevronRight,
faQuestionCircle,
faCog
} from '@fortawesome/free-solid-svg-icons';
import { Conversation, Project } from '../../models/data.model';
import { DataService } from '../../services/data.service';

@Component({
selector: 'app-sidebar',
standalone: true,
imports: [CommonModule, FontAwesomeModule],
templateUrl: './sidebar.html',
styleUrls: ['./sidebar.scss']
})
export class Sidebar implements OnInit {
@Input() isCollapsed = false;
@Output() newChat = new EventEmitter<void>();
@Output() conversationSelected = new EventEmitter<string>();
@Output() toggleSidebar = new EventEmitter<void>();

conversations: Conversation[] = [];
projects: Project[] = [];
currentConversationId: string | null = null;

faComments = faComments;
faFolder = faFolder;
faChevronDown = faChevronDown;
faChevronRight = faChevronRight;
faQuestionCircle = faQuestionCircle;
faCog = faCog;

constructor(private dataService: DataService) {}

ngOnInit(): void {
this.dataService.getConversations().subscribe(conversations => {
this.conversations = conversations;
});

this.dataService.getProjects().subscribe(projects => {
this.projects = projects;
});

this.dataService.getCurrentConversation().subscribe(conversation => {
this.currentConversationId = conversation?.id || null;
});
}

onNewChat(): void {
this.newChat.emit();
}

onSelectConversation(id: string): void {
console.log(id);
this.conversationSelected.emit(id);
}

onToggleSidebar(): void {
this.toggleSidebar.emit();
}

onToggleProject(projectId: string): void {
this.dataService.toggleProject(projectId);
}
}

+ 70
- 0
src/app/pages/chat/chat-view.html Wyświetl plik

@@ -0,0 +1,70 @@
<div class="chat-view" [class.active]="conversation">
<div class="messages-wrapper">
<div class="messages-container" #messagesContainer>
<div
*ngFor="let message of conversation?.messages; let isLast = last"
class="message"
[class.user-message]="message.sender === 'user'"
[class.agent-message]="message.sender === 'agent'"
>
<!-- Message de l'utilisateur -->
<div class="message-content" *ngIf="message.sender === 'user'">
<div class="message-bubble user-bubble">
<p>{{ message.content }}</p>
</div>
</div>

<!-- Message de l'agent -->
<div class="message-content" *ngIf="message.sender === 'agent'">
<div class="message-bubble agent-bubble">
<p>{{ message.content }}</p>
</div>

<!-- Actions pour les messages de l'agent -->
<div class="message-actions">
<button
class="action-button"
(click)="onCopyMessage(message.content)"
[attr.aria-label]="'Copier le message'"
>
<fa-icon [icon]="faCopy"></fa-icon>
</button>
<button
class="action-button"
(click)="onRegenerateMessage()"
[attr.aria-label]="'Régénérer la réponse'"
*ngIf="isLast"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
<path
d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
</svg>
</button>
</div>
</div>
</div>

<!-- Indicateur de saisie -->
<div class="message agent-message" *ngIf="isTyping">
<div class="message-content">
<div class="message-bubble agent-bubble typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>

<!-- Input de chat -->
<div class="chat-input-container">
<app-chat-input
(messageSent)="onMessageSent($event)"
(attach)="onAttach()"
(voice)="onVoice()"
></app-chat-input>
</div>
</div>

+ 251
- 0
src/app/pages/chat/chat-view.scss Wyświetl plik

@@ -0,0 +1,251 @@
.chat-view {
display: none;
height: 100%;
flex-direction: column;
background: #f5f7fa;
opacity: 0;
transform: translateY(20px);
transition: all 0.4s ease;

&.active {
display: flex;
opacity: 1;
transform: translateY(0);
}

.messages-wrapper {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;

&::-webkit-scrollbar {
width: 6px;
}

&::-webkit-scrollbar-track {
background: transparent;
}

&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;

&:hover {
background: #94a3b8;
}
}

.messages-container {
max-width: 800px;
margin: 0 auto;
padding: 32px 24px;
display: flex;
flex-direction: column;
gap: 24px;

.message {
display: flex;
flex-direction: column;
animation: messageSlideIn 0.3s ease;

&.user-message {
align-items: flex-end;

.message-content {
align-items: flex-end;
}
}

&.agent-message {
align-items: flex-start;

.message-content {
align-items: flex-start;
}
}

.message-content {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 85%;

@media (min-width: 768px) {
max-width: 75%;
}

.message-bubble {
padding: 16px 20px;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;

&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

p {
margin: 0;
line-height: 1.6;
font-size: 15px;
word-wrap: break-word;
white-space: pre-wrap;
color: #1f2937;
}

&.user-bubble {
background: white;
border-bottom-right-radius: 4px;


}

&.agent-bubble {
//background: #ffffff;
//border: 1px solid #e5e7eb;
//border-bottom-left-radius: 4px;
}

&.typing-indicator {
display: flex;
gap: 6px;
padding: 20px 24px;
justify-content: center;
min-width: 80px;

span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #94a3b8;
animation: typingDot 1.4s infinite;

&:nth-child(2) {
animation-delay: 0.2s;
}

&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}

.message-actions {
display: flex;
gap: 4px;
transform: translateY(-4px);
transition: all 0.2s ease;
padding-left: 4px;


.action-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: 1px solid transparent;
border-radius: 8px;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;

&:hover {
background: #f3f4f6;
border-color: #e5e7eb;
color: #374151;
}

&:active {
transform: scale(0.95);
}

fa-icon {
font-size: 14px;
}
}
}
}

&:hover .message-actions {
opacity: 1;
transform: translateY(0);
}
}
}
}

.chat-input-container {
padding: 16px 24px 24px;
background: #f5f7fa;
border-top: 1px solid #e5e7eb;

app-chat-input {
max-width: 800px;
margin: 0 auto;
display: block;
}
}
}

// Animations
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes typingDot {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-8px);
opacity: 1;
}
}

// Responsive
@media (max-width: 768px) {
.chat-view {
.messages-wrapper {
.messages-container {
padding: 24px 16px;
gap: 20px;

.message {
.message-content {
max-width: 90%;

.message-bubble {
padding: 14px 16px;
font-size: 14px;
}

.message-actions {
.action-button {
width: 28px;
height: 28px;
font-size: 12px;
}
}
}
}
}
}

.chat-input-container {
padding: 12px 16px 16px;
}
}
}

+ 23
- 0
src/app/pages/chat/chat-view.spec.ts Wyświetl plik

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ChatList } from './chat-list';

describe('ChatList', () => {
let component: ChatList;
let fixture: ComponentFixture<ChatList>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChatList]
})
.compileComponents();

fixture = TestBed.createComponent(ChatList);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 79
- 0
src/app/pages/chat/chat-view.ts Wyświetl plik

@@ -0,0 +1,79 @@
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faCopy, faThumbsUp, faThumbsDown, faRotateRight } from '@fortawesome/free-solid-svg-icons';
import { Conversation } from '../../models/data.model';
import {ChatInput} from '../../components/chat-input/chat-input';

@Component({
selector: 'app-chat-view',
standalone: true,
imports: [CommonModule, FontAwesomeModule, ChatInput],
templateUrl: './chat-view.html',
styleUrls: ['./chat-view.scss']
})
export class ChatView implements AfterViewChecked {
@ViewChild('messagesContainer') messagesContainer!: ElementRef;

@Input() conversation: Conversation | null = null;
@Input() isTyping = false;
@Input() agentName = 'Assistant';

@Output() messageSent = new EventEmitter<string>();
@Output() attach = new EventEmitter<void>();
@Output() voice = new EventEmitter<void>();
@Output() copyMessage = new EventEmitter<string>();
@Output() regenerateMessage = new EventEmitter<void>();

// Icons
faCopy = faCopy;


private shouldScrollToBottom = false;

ngAfterViewChecked(): void {
if (this.shouldScrollToBottom) {
this.scrollToBottom();
this.shouldScrollToBottom = false;
}
}

onMessageSent(message: string): void {
this.messageSent.emit(message);
this.shouldScrollToBottom = true;
}

onAttach(): void {
this.attach.emit();
}

onVoice(): void {
this.voice.emit();
}

onCopyMessage(content: string): void {
navigator.clipboard.writeText(content);
this.copyMessage.emit(content);
}

onRegenerateMessage(): void {
this.regenerateMessage.emit();
}

private scrollToBottom(): void {
try {
const messagesElement = this.messagesContainer?.nativeElement;
if (messagesElement) {
setTimeout(() => {
messagesElement.scrollTop = messagesElement.scrollHeight;
}, 100);
}
} catch (err) {
console.error('Error scrolling to bottom:', err);
}
}

markScrollNeeded(): void {
this.shouldScrollToBottom = true;
}
}

+ 71
- 0
src/app/pages/dashboard/dashboard.html Wyświetl plik

@@ -0,0 +1,71 @@
<div class="dashboard">
<app-sidebar
[isCollapsed]="sidebarCollapsed"
(newChat)="onNewChat()"
(conversationSelected)="onConversationSelected($event)"
(toggleSidebar)="onToggleSidebar()"
></app-sidebar>

<div class="main-content">

<div class="content-header">
<button
class="back-button"
*ngIf="currentConversation"
(click)="onNewChat()"
[attr.aria-label]="'Retour'"
>
<fa-icon [icon]="faArrowLeft"></fa-icon>
</button>
<div class="header-spacer"></div>
<button class="share-button" [attr.aria-label]="'Partager'">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
</svg> </button>
<button class="login-button">Se connecter</button>
</div>


<div class="content-body" #contentBody [class.chat-active]="currentConversation">
<!-- Welcome Screen -->
<div class="welcome-screen" [class.fade-out]="currentConversation">
<div class="welcome-header">
<h1 class="welcome-title">Bonjour !</h1>
<p class="welcome-subtitle">Qu'est-ce qu'on fait aujourd'hui ?</p>
</div>

<div class="center-input-wrapper" [class.animate-down]="currentConversation">
<app-chat-input
(messageSent)="onMessageSent($event)"
(attach)="onAttach()"
(voice)="onVoice()"
></app-chat-input>
</div>

<div class="agents-grid">
<app-agent-card
*ngFor="let agent of agents"
[agent]="agent"
(selected)="onAgentSelected($event)"
></app-agent-card>
</div>
</div>

<!-- Chat View Component -->
@if (currentConversation) {
<app-chat-view
[conversation]="currentConversation"
[isTyping]="isTyping"
[agentName]="currentAgentName"
(messageSent)="onMessageSent($event)"
(attach)="onAttach()"
(voice)="onVoice()"
(copyMessage)="onCopyMessage($event)"
(regenerateMessage)="onRegenerateMessage()"
></app-chat-view>
}

</div>
</div>
</div>

+ 286
- 0
src/app/pages/dashboard/dashboard.scss Wyświetl plik

@@ -0,0 +1,286 @@
.dashboard {
display: flex;
height: 100vh;
overflow: hidden;
background: #f5f7fa;

.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;

// dashboard.scss (extrait - partie header avec back button)
.content-header {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
padding: 16px 24px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid #e5e7eb;
z-index: 10;

.back-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: transparent;
border: none;
border-radius: 10px;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
margin-right: auto;

&:hover {
background: #f3f4f6;
border-color: #d1d5db;
color: #1f2937;
}

fa-icon {
font-size: 18px;
}
}

.header-spacer {
flex: 1;
}

.share-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
background: transparent;
border-radius: 10px;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
background: #f3f4f6;
border-color: #d1d5db;
color: #1f2937;
}

fa-icon {
font-size: 18px;
}
}

.login-button {
padding: 10px 20px;
background: #ffffff;
border-radius: 10px;
border: none;

color: #1f2937;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
background: #f9fafb;
border-color: #d1d5db;
}
}

}

.content-body {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;

&::-webkit-scrollbar {
width: 8px;
}

&::-webkit-scrollbar-track {
background: #f1f1f1;
}

&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;

&:hover {
background: #94a3b8;
}
}

// Welcome Screen
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 24px;
opacity: 1;
transition: opacity 0.3s ease;
overflow-y: auto;

&.fade-out {
opacity: 0;
pointer-events: none;
}

.welcome-header {
text-align: center;
margin-bottom: 40px;
animation: fadeInDown 0.6s ease;

.welcome-title {
font-size: 48px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px 0;
line-height: 1.2;

@media (max-width: 768px) {
font-size: 36px;
}
}

.welcome-subtitle {
font-size: 20px;
color: #6b7280;
margin: 0;
font-weight: 400;

@media (max-width: 768px) {
font-size: 18px;
}
}
}

.center-input-wrapper {
width: 100%;
max-width: 800px;
margin-bottom: 48px;
animation: fadeInUp 0.6s ease 0.2s both;
position: relative;
z-index: 5;

&.animate-down {
animation: slideDown 0.6s ease forwards;
}
}

.agents-grid {
width: 100%;
max-width: 1000px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 16px;
animation: fadeInUp 0.6s ease 0.4s both;

@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
}

// Le chat-view prend tout l'espace quand actif
app-chat-view {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}

&.chat-active {
.welcome-screen {
display: none;
}
}
}
}
}

// Animations
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes slideDown {
0% {
transform: translateY(0);
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
transform: translateY(calc(100vh - 200px));
opacity: 0;
}
}

// Responsive
@media (max-width: 768px) {
.dashboard {
.main-content {
.content-header {
padding: 12px 16px;

.share-button {
width: 36px;
height: 36px;

fa-icon {
font-size: 16px;
}
}

.login-button {
padding: 8px 16px;
font-size: 13px;
}
}

.content-body {
.welcome-screen {
padding: 24px 16px;

.welcome-header {
margin-bottom: 32px;
}

.center-input-wrapper {
margin-bottom: 32px;
}
}
}
}
}
}

+ 23
- 0
src/app/pages/dashboard/dashboard.spec.ts Wyświetl plik

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { Dashboard } from './dashboard';

describe('Dashboard', () => {
let component: Dashboard;
let fixture: ComponentFixture<Dashboard>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Dashboard]
})
.compileComponents();

fixture = TestBed.createComponent(Dashboard);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 188
- 0
src/app/pages/dashboard/dashboard.ts Wyświetl plik

@@ -0,0 +1,188 @@
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import {faArrowLeft, faShare} from '@fortawesome/free-solid-svg-icons';
import { Sidebar } from '../../components/sidebar/sidebar';
import { AgentCard } from '../../components/agent-card/agent-card';
import { ChatInput } from '../../components/chat-input/chat-input';
import { DataService } from '../../services/data.service';
import { Agent, Conversation } from '../../models/data.model';
import {ChatView} from '../chat/chat-view';

@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
CommonModule,
FontAwesomeModule,
Sidebar,
AgentCard,
ChatInput,
ChatView
],
templateUrl: './dashboard.html',
styleUrls: ['./dashboard.scss']
})
export class Dashboard implements OnInit {
@ViewChild('contentBody') contentBody!: ElementRef;
@ViewChild(ChatView) chatView!: ChatView;

agents: Agent[] = [];
currentConversation: Conversation | null = null;
sidebarCollapsed = false;
isTyping = false;

// Icons
faShare = faShare;

private animatingInput = false;

constructor(private dataService: DataService) {}

ngOnInit(): void {
this.dataService.getAgents().subscribe(agents => {
this.agents = agents;
});

this.dataService.getCurrentConversation().subscribe(conversation => {
const hadConversation = this.currentConversation !== null;
this.currentConversation = conversation;

if (hadConversation && conversation && this.chatView) {
this.chatView.markScrollNeeded();
}

if (conversation !== null) {
this.isTyping = false;
}
});
}

onNewChat(): void {
this.dataService.resetCurrentConversation();
this.animatingInput = false;

setTimeout(() => {
if (this.contentBody?.nativeElement) {
this.contentBody.nativeElement.scrollTop = 0;
}
}, 100);
}

onAgentSelected(agent: Agent): void {
this.animatingInput = true;

setTimeout(() => {
this.dataService.createNewConversation(agent.id);
this.animatingInput = false;
}, 600);
}

onConversationSelected(conversationId: string): void {
this.dataService.selectConversation(conversationId);
}

onMessageSent(message: string): void {
if (!this.currentConversation) {
this.dataService.createNewConversation('default');

setTimeout(() => {
this.sendMessage(message);
}, 100);
} else {
this.sendMessage(message);
}
}

onToggleSidebar(): void {
this.sidebarCollapsed = !this.sidebarCollapsed;
}

onAttach(): void {
console.log('Attach file clicked');
}

onVoice(): void {
console.log('Voice input clicked');
}

onCopyMessage(content: string): void {
console.log('Message copied:', content);
}

onRegenerateMessage(): void {
if (!this.currentConversation || this.currentConversation.messages.length === 0) {
return;
}

const lastUserMessage = [...this.currentConversation.messages]
.reverse()
.find(msg => msg.sender === 'user');

if (lastUserMessage) {
// Supprimer la dernière réponse de l'agent
const messages = this.currentConversation.messages;
if (messages[messages.length - 1].sender === 'agent') {
messages.pop();
}

this.isTyping = true;
if (this.chatView) {
this.chatView.markScrollNeeded();
}

setTimeout(() => {
this.dataService.simulateAgentResponse(
this.currentConversation!.agentId,
lastUserMessage.content
);

this.isTyping = false;
if (this.chatView) {
this.chatView.markScrollNeeded();
}
}, 1500);
}
}

private sendMessage(message: string): void {
if (!this.currentConversation) return;

this.dataService.addMessage({
content: message,
sender: 'user',
});

this.isTyping = true;
if (this.chatView) {
this.chatView.markScrollNeeded();
}

setTimeout(() => {
this.dataService.simulateAgentResponse(
this.currentConversation!.agentId,
message
);

this.isTyping = false;
if (this.chatView) {
this.chatView.markScrollNeeded();
}
}, 1500);
}

get hasActiveConversation(): boolean {
return this.currentConversation !== null &&
this.currentConversation.messages &&
this.currentConversation.messages.length > 0;
}

get currentAgentName(): string {
if (!this.currentConversation) return '';

const agent = this.agents.find(a => a.id === this.currentConversation?.agentId);
return agent?.name || 'Assistant';
}

protected readonly faArrowLeft = faArrowLeft;
}

+ 97
- 0
src/app/pages/login/login.html Wyświetl plik

@@ -0,0 +1,97 @@
<div class="login-page-container">
<div class="login-card">
<div class="login-header">
<div class="logo">AIT</div>
<h1 class="login-title">Connectez-vous à votre espace</h1>
<p class="login-subtitle">Entrez vos identifiants pour continuer.</p>
</div>

<div class="login-content">
<form [formGroup]="loginForm" (ngSubmit)="login()" class="login-form">

<!-- Email Field -->
<div class="form-field">
<label for="email" class="form-label">Email</label>
<div class="input-wrapper">
<input
id="email"
type="email"
formControlName="username"
class="form-input"
placeholder="votre.email@exemple.com"
[class.error]="loginForm.get('email')?.invalid && loginForm.get('email')?.touched"
>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</div>
<span class="error-message" *ngIf="loginForm.get('email')?.invalid && loginForm.get('email')?.touched">
Veuillez entrer une adresse email valide.
</span>
</div>

<!-- Password Field -->
<div class="form-field">
<label for="password" class="form-label">Mot de passe</label>
<div class="input-wrapper">
<input
id="password"
[type]="showPassword ? 'text' : 'password'"
formControlName="password"
class="form-input"
placeholder="••••••••"
[class.error]="loginForm.get('password')?.invalid && loginForm.get('password')?.touched"
>
<svg class="input-icon clickable"
(click)="togglePasswordVisibility()"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path *ngIf="!showPassword" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle *ngIf="!showPassword" cx="12" cy="12" r="3"></circle>
<path *ngIf="showPassword" d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line *ngIf="showPassword" x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</div>
<span class="error-message" *ngIf="loginForm.get('password')?.hasError('required') && loginForm.get('password')?.touched">
Le mot de passe est requis.
</span>
<span class="error-message" *ngIf="loginForm.get('password')?.hasError('minlength') && loginForm.get('password')?.touched">
Le mot de passe doit contenir au moins 6 caractères.
</span>
</div>

<!-- Form Options -->
<div class="form-options">
<label class="checkbox-wrapper">
<input type="checkbox" formControlName="rememberMe">
<span class="checkbox-label">Se souvenir de moi</span>
</label>
<a href="#" class="forgot-password-link">Mot de passe oublié ?</a>
</div>

<!-- Submit Button -->
<button
type="submit"
[disabled]="loginForm.invalid"
class="login-btn"
>
<span >Se connecter</span>
</button>
</form>

</div>

<!-- Footer -->
<div class="login-footer">
<p>Vous n'avez pas de compte ? <a href="#" class="register-link">S'inscrire</a></p>
</div>
</div>
</div>

+ 266
- 0
src/app/pages/login/login.scss Wyświetl plik

@@ -0,0 +1,266 @@
.sidebar {
width: 280px;
height: 100vh;
background: linear-gradient(180deg, #1e3a8a 0%, #312e81 100%);
display: flex;
flex-direction: column;
color: white;
overflow-y: auto;
transition: width 0.3s ease;

&.collapsed {
width: 70px;

.sidebar-header {
justify-content: center;
}

.new-chat-button,
.section-title,
.conversation-title,
.project-name {
display: none;
}
}
}

.sidebar-header {
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}

.logo h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
letter-spacing: 2px;
}

.toggle-button {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background 0.2s ease;

&:hover {
background: rgba(255, 255, 255, 0.1);
}

fa-icon {
font-size: 20px;
}
}

.new-chat-button,
.new-chat-button-icon {
margin: 20px;
padding: 14px 20px;
background: rgba(59, 130, 246, 0.2);
border: 2px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 500;
transition: all 0.2s ease;
border: none;

&:hover {
background: rgba(59, 130, 246, 0.3);
transform: translateY(-2px);
}

fa-icon {
font-size: 18px;
}
}

.new-chat-button-icon {
justify-content: center;
padding: 14px;
width: calc(100% - 40px);
}

.conversations-section,
.projects-section {
padding: 0 20px;
margin-bottom: 24px;
flex-shrink: 0;
}

.section-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 12px 0;
opacity: 0.7;
}

.conversations-list {
display: flex;
flex-direction: column;
gap: 4px;
}

.conversation-item {
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

&:hover {
background: rgba(255, 255, 255, 0.1);
}

&.active {
background: rgba(59, 130, 246, 0.3);
font-weight: 500;
}

&.sub-item {
margin-left: 20px;
font-size: 13px;
opacity: 0.9;
}
}

.conversation-title {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.projects-list {
display: flex;
flex-direction: column;
gap: 4px;
}

.project-wrapper {
display: flex;
flex-direction: column;
}

.project-item {
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;

&:hover {
background: rgba(255, 255, 255, 0.1);
}
}

.project-icon {
opacity: 0.7;
flex-shrink: 0;
}

.project-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.expand-icon {
opacity: 0.5;
font-size: 12px;
color: #60a5fa;
flex-shrink: 0;
transition: transform 0.2s ease;
}

.project-conversations {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 4px;
animation: slideDown 0.2s ease;
}

.project-empty {
padding: 8px 12px 8px 44px;
font-size: 12px;
opacity: 0.5;
font-style: italic;
}

@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.sidebar-footer {
margin-top: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}

.footer-button {
width: 100%;
padding: 12px;
background: none;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: white;
cursor: pointer;
transition: all 0.2s ease;

&:hover {
background: rgba(255, 255, 255, 0.1);
}

fa-icon {
font-size: 18px;
}
}

/* Scrollbar styling */
.sidebar::-webkit-scrollbar {
width: 6px;
}

.sidebar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}

.sidebar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;

&:hover {
background: rgba(255, 255, 255, 0.3);
}
}

+ 23
- 0
src/app/pages/login/login.spec.ts Wyświetl plik

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { Login } from './login';

describe('Login', () => {
let component: Login;
let fixture: ComponentFixture<Login>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Login]
})
.compileComponents();

fixture = TestBed.createComponent(Login);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 69
- 0
src/app/pages/login/login.ts Wyświetl plik

@@ -0,0 +1,69 @@
import {Component, ElementRef, inject, OnInit, signal, viewChild} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import { Router } from '@angular/router';
import {NgIf} from '@angular/common';
import {LoginService} from './login.service';
import {AccountService} from '../../core/auth/account.service';

@Component({
selector: 'app-login',
templateUrl: './login.html',
imports: [
ReactiveFormsModule,
NgIf
],
styleUrl: './login.scss'
})
export class Login implements OnInit {
username = viewChild.required<ElementRef>('username');
authenticationError = signal(false);


loginForm = new FormGroup({
username: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
password: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(4)] }),
rememberMe: new FormControl(false, { nonNullable: true, validators: [Validators.required] }),
});

private readonly accountService = inject(AccountService);
private readonly loginService = inject(LoginService);
private readonly router = inject(Router);

protected showPassword: boolean = false;


ngOnInit(): void {
// if already authenticated then navigate to home page
/* this.accountService.identity().subscribe(() => {
if (this.accountService.isAuthenticated()) {
this.router.navigate(['']);
}
});*/
}


/**
* Bascule la visibilité du mot de passe
*/
togglePasswordVisibility(): void {
this.showPassword = !this.showPassword;
}

/**
* Gère la soumission du formulaire
*/
login(): void {
this.loginService.login(this.loginForm.getRawValue()).subscribe({
next: () => {
this.authenticationError.set(false);
if (!this.router.getCurrentNavigation()) {
// There were no routing during login (eg from navigationToStoredUrl)
this.router.navigate(['chantier/dashboard']);
}
},
error: () => this.authenticationError.set(true),
});
}


}

Ładowanie…
Anuluj
Zapisz