// --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? "></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } }

Bioinformatics

Oct 13, 2024

The goal is to input data from a diagnosis, such as blood tests with specific markers, or use a multimodal approach that captures various aspects of a presenting phenotype. A black box model can then predict a combination of drugs and therapeutics to address the problem associated with the phenotype.

Biology aims to provide the black box with extensive, multimodal information to enhance its accuracy for each cell type in an individual, making the approach personalized. However, constructing different models for each person poses a challenge. Machine learning and deep learning models have been used to train on populations and make individual predictions, but these models often fail to generalize to individuals, relying instead on population-based predictions.

Future bioinformatics should focus on integrating all available information to predict at an individual level. Beyond prediction, the field needs to synthesize data from various modalities to create a comprehensive storyline for each patient. Viewing each patient as a time series model with synthesized data for each modality can provide a detailed narrative. This approach contrasts with targeting single genes based on presenting phenotypes, which has not achieved the desired outcomes despite numerous attempts with single-cell inference models.

Each patient requires a storyline that encompasses all body parts and the different modalities present. This comprehensive approach will improve the accuracy and effectiveness of personalized treatments.

George Weale