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

Hi, I'm George!

I am going to be joining Google this summer as a Software Engineer working on Machine Learning and Cloud Ai. I am currently a masters student at the Columbia University studying Machine Learning and Bioinformatics in Professor Kam Leong's Lab. I did my undergrad at the University of California, Santa Barbara where I obtained a BS in Chemical Engineering and a BS in Data Science and Statistics.

George Weale