// --- 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 } } }

Photos

Big Sea Bass

Big Sea Bass

Undergrad graduation

Undergrad graduation

Senior Design Project

Senior Design Project

Professor Bowers

Professor Bowers

Heavenly Ski Trip

Heavenly Ski Trip

Reactions

Reactions

4th of July

4th of July

PCT Trail

PCT Trail

Rick's beach

Rick's beach

George Weale