// --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } } // --- theme.service.ts --- import { Injectable, signal, effect, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class ThemeService { private storageKey = 'theme-preference'; currentTheme = signal<'light' | 'dark'>('light'); constructor(@Inject(PLATFORM_ID) private platformId: Object) { if (isPlatformBrowser(this.platformId)) { const initialTheme = this.getColorPreference(); this.setTheme(initialTheme, false); // Apply initial theme without saving effect(() => { const theme = this.currentTheme(); this.reflectPreference(theme); localStorage.setItem(this.storageKey, theme); }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', this.handleSystemThemeChange); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleSystemThemeChange); } } private getColorPreference(): 'light' | 'dark' { const storedPreference = localStorage.getItem(this.storageKey); if (storedPreference) { return storedPreference as 'light' | 'dark'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } private reflectPreference(theme: 'light' | 'dark') { document.documentElement.classList.remove('light', 'dark'); document.documentElement.classList.add(theme); // Assuming 'light'/'dark' classes are applied to <html> } setTheme(theme: 'light' | 'dark', savePreference = true) { this.currentTheme.set(theme); if (savePreference) { localStorage.setItem(this.storageKey, theme); } } toggleTheme() { this.currentTheme.update(current => (current === 'light' ? 'dark' : 'light')); } private handleSystemThemeChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? 'dark' : 'light'; // Only update if no explicit preference is stored if (!localStorage.getItem(this.storageKey)) { this.setTheme(newTheme, false); } }; } // --- theme-switch.component.ts --- import { Component, inject } from '@angular/core'; import { ThemeService } from './theme.service'; @Component({ selector: 'app-theme-switch', standalone: true, template: `<button id="theme-toggle" aria-label="Toggle theme" (click)="toggleTheme()" class="flex items-center justify-center transition-opacity duration-300 hover:opacity-90"> <!-- Replace with Angular icon library or SVG --> <span [textContent]="themeService.currentTheme() === 'light' ? '🌙' : '☀️'"></span> </button>` }) export class ThemeSwitchComponent { themeService = inject(ThemeService); toggleTheme() { this.themeService.toggleTheme(); } } // --- image.model.ts --- export interface Image { src: string; alt: string; href?: string; } // --- image-grid.component.ts --- import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Image } from './image.model'; @Component({ selector: 'app-image-grid', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-{{ columns }} gap-6 max-w-[1500px] mx-auto relative"> <div *ngFor="let image of images; let i = index" class="relative"> <div class="relative aspect-[16/9] w-full cursor-pointer" (click)="handleImageClick(i)"> <div class="relative h-full w-full"> <img [alt]="image.alt" [src]="image.src" class="rounded-lg object-cover transition-all duration-300 hover:opacity-90 w-full h-full" /> </div> </div> <p *ngIf="showCaption && expandedIndex === null" class="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400"> {{ image.alt }} </p> </div> </div> <!-- Modal --> <div *ngIf="expandedIndex !== null" class="fixed inset-0 bg-black/80 z-40 cursor-pointer" (click)="closeModal()"></div> <div *ngIf="expandedIndex !== null" class="fixed inset-0 z-50 flex items-center justify-center" (click)="closeModal()"> <div class="relative w-[90vw] h-[80vh] max-w-[1800px]"> <img *ngIf="images[expandedIndex]" [alt]="images[expandedIndex].alt" [src]="images[expandedIndex].src" class="object-contain rounded-lg shadow-2xl w-full h-full" /> </div> </div> `, styles: [':host { display: block; }'] }) export class ImageGridComponent { @Input() images: Image[] = []; @Input() columns: 2 | 3 | 4 = 3; @Input() showCaption: boolean = false; expandedIndex: number | null = null; handleImageClick(index: number) { this.expandedIndex = index; } closeModal() { this.expandedIndex = null; } } // --- chatbot.component.ts --- import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-chatbot', standalone: true, imports: [FormsModule, CommonModule], template: `<section> <h1 class="mb-8 text-2xl font-medium tracking-tight">Chatbot</h1> <div class="prose prose-neutral dark:prose-invert"> <p> Welcome to my chatbot! This is <a href="https://github.com/GWeale/ChatBot" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600">my own pico-scale chatbot</a> that you can interact with. Feel free to ask questions about my research, work, and projects. Sorry, but I discontinued the chatbot because it was too expensive to keep running. :( </p> </div> <div *ngIf="error" class="mt-4 p-3 bg-red-100 text-red-700 rounded-md"> {{ error }} </div> <form (ngSubmit)="handleSubmit()" class="mt-6"> <div class="flex gap-2"> <input type="text" [(ngModel)]="message" name="message" placeholder="Type your message here..." class="flex-1 p-2 border rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-white" /> <button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"> Send </button> </div> </form> <div class="mt-8 flex flex-col items-center"> <h2 class="text-xl font-medium mb-4">Example Output</h2> <img src="/photos/chatbot-screenshot.png" alt="Chatbot example output" class="rounded-lg shadow-md w-full max-w-2xl" /> </div> </section>` }) export class ChatbotComponent { message: string = ""; error: string = ""; handleSubmit() { if (this.message.trim()) { this.error = "API is no longer connected. Please try again later."; this.message = ""; } else { this.error = ""; // Clear error if message is empty } } }

Photos

First Big Sea Bass

First Big Sea Bass

Chem E's graduating

Chem E's graduating

Senior Design Project Presentation

Senior Design Project Presentation

Professor Bowers!

Professor Bowers!

Heavenly Ski Trip

Heavenly Ski Trip

Reactions Class

Reactions Class

George Weale